ActiveSlick is a library that offers CRUD operations for Slick 3.x projects. The main goal is to provide some basic operations to manage the lifecycle of persisted objects (new/persisted/deleted/stale) and enable the implementation of the Active Record Pattern on Slick mapped case classes.

Main features

  • Basic CRUD and auxiliary methods - add/update/save, delete, findById, count and fetchAll (backed by Reactive Streams).

  • Generic Id type.

  • Optimistic locking my means of incremental version.

  • Before insert and update hooks.

  • ActiveRecord to enable the Active Record Pattern on mapped case classes via class extensions (pimp-my-library style)

Project artifact

The artifacts are published to Sonatype Repository. Simply add the following to your build.sbt.

As of version 0.3.x we don’t support Slick 2.0 anymore. The differences between Slick 2.x and Slick 3.x are so huge that it makes impossible to support two versions.

libraryDependencies += "io.strongtyped" %% "active-slick" % "0.3.5"

Source code for version 0.3.5 can be found at: https://github.com/strongtyped/active-slick/tree/v0.3.5

The version supporting Slick 2.0 is still available on Sonatype Repo. However, this documentation only covers the current series (i.e.: 0.3.x).

libraryDependencies += "io.strongtyped" %% "active-slick" % "0.2.2"

Source code for version 0.2.2 can be found at: https://github.com/strongtyped/active-slick/tree/v0.2.2

Motivation

Slick is able to map result sets to case classes or Tuples because of its isomorphism. This is done thanks to built-in Scala features. However, there is no direct link between case class fields and database columns. Everything is done based on isomorphic projections.

As a consequence, managing of Entities IDs must be done by hand, over and over again. One needs to save a model, ask Slick to return the generated ID and add it explicitly to the case class.

The following code fragment demonstrates how this is typically done in a Slick application.

  import slick.jdbc.H2Profile.api._

  case class Coffee(name: String, id: Option[Int] = None) // (1)

  class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") {
    def name = column[String]("NAME")
    def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) // (2)
    def * = (name, id.?) <>(Coffee.tupled, Coffee.unapply)
  }

  val Coffees = TableQuery[CoffeeTable]

  val coffee = Coffee("Colombia")
  val insertAction = Coffees.returning(Coffees.map(_.id)) += coffee // (3)
1 Both Coffee and CoffeeTable have an Id representing the primary key. Coffee has a field of type Option[Int]
2 CoffeeTable has a method returning a Rep[Int].
3 However, in order to inserta new Coffee we need some boilerplate to get back the generated ID.

If we manage to connect both ID we can provide some generic functionality to manage Entities. Inserting a new Entity is only one of the possible use cases. With a well known ID, we can distinguish if an Entity has been already persisted or not, we can easilly implement byId methods (deleteById, findById) and we can add extensions methods like: save, udpate, delete.

History

The first intention of this project was to implement Slick DAOs or Repositories using only Scala features and the Scala compiler (no code generation).

In the time of Slick 2.0, this library required the usage of the Cake Pattern to wire together all the parts wihtout pre-defining a driver. Since Slick 3.0 imposes a huge refactoring, we decided to turn ActiveSlick inside out and eliminate the need of the Cake Pattern.

As a result, all special traits/classes like TableId, TableWithIdQuery, EntityTableQuery, etc, are gone. These classes could only compile if a driver was in scope. However, only the final user of the library can decide which driver to use. To overcome that situation we had to define everything inside components and wire them all together in a cake.

This approach was way to complex and hard to document. Users had to understand how to compose the traits and which classes to implement and/or mixin in order the get the desired effect.

From v.0.3.x onwards, one can build a DAO/Repository and enable an ActiveRecord extension using plain old Slick mappings. Some configuration is needed, but it’s far more obvious to configure it than to compose and bake a cake.

EntityActions

EntityActions is the main class we can use to build a DAO/Repository. It offers default implmentation for the following methods.

  /** Returns total table count */
  def count: DBIO[Int]

  /** Insert or Update a Model
    * Insert `Model` if not yet persisted, otherwise update it.
    * @return DBIO[Model] for a `Model` as persisted in the table.
    */
  def save(entity: Model)(implicit exc: ExecutionContext): DBIO[Model]

  /** Update a `Model`.
    * @return DBIO[Model] for a `Model` as persisted in the table.
    */
  def update(entity: Model)(implicit exc: ExecutionContext): DBIO[Model]

  /** Delete a `Model`.
    * @return DBIO[Int] with the number of affected rows
    */
  def delete(entity: Model)(implicit exc: ExecutionContext): DBIO[Int]

  /** Fetch all elements from a table.
    * @param fetchSize - the number of row to fetch, defaults to 100
    * @return StreamingDBIO[Seq[Model], Model]
    */
  def fetchAll(fetchSize: Int = 100)
              (implicit exc: ExecutionContext): StreamingDBIO[Seq[Model], Model]


  /** Insert a new `Entity`
    * @return DBIO[Id] for the generated `Id`
    */
  def insert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Id]

  /** Delete a `Entity` by `Id`
    * @return DBIO[Int] with the number of affected rows
    */
  def deleteById(id: Id)(implicit exc: ExecutionContext): DBIO[Int]

  /** Finds `Entity` referenced by `Id`.
    * May fail if no `Entity` is found for passed `Id`
    * @return DBIO[Entity] for the `Entity`
    */
  def findById(id: Id): DBIO[Entity]

  /** Finds `Entity` referenced by `Id` optionally.
    * @return DBIO[Option[Entity]] for the `Entity`
    */
  def findOptionById(id: Id): DBIO[Option[Entity]]
EntityActions implements two basic traits: CrudActions and EntityActionsLike. All methods involving a Model are Id agnostic and are defined in CrudActions. Methods involving an Entity are defined by EntityActionsLike

Mapping using ActiveSlick

The following code fragment illustrates how we can use ActiveSlick EntityActions to bind the Entity Id field and the Table Id column.

import io.strongtyped.active.slick._
import slick.ast.BaseTypedType
import io.strongtyped.active.slick.Lens._
import scala.language.postfixOps
import scala.concurrent.ExecutionContext.Implicits.global

object MappingWithActiveSlick {

  case class Coffee(name: String, id: Option[Int] = None)



  object CoffeeRepo extends EntityActions with H2ProfileProvider {

    import jdbcProfile.api._ // (1)
    val baseTypedType = implicitly[BaseTypedType[Id]] // (2)

    type Entity = Coffee // (3)
    type Id = Int // (4)
    type EntityTable = CoffeeTable // (5)

    val tableQuery = TableQuery[CoffeeTable] // (6)

    def $id(table: CoffeeTable): Rep[Id] = table.id // (7)

    val idLens = lens { coffee: Coffee => coffee.id  } // (8)
                      { (coffee, id) => coffee.copy(id = id) }

    class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") { // (9)
      def name = column[String]("NAME")
      def id = column[Id]("ID", O.PrimaryKey, O.AutoInc)
      def * = (name, id.?) <>(Coffee.tupled, Coffee.unapply)
    }

    def findByName(name:String): DBIO[Seq[Coffee]] = {
      tableQuery.filter(_.name === name).result
    }

}

  implicit class EntryExtensions(val model: Coffee) extends ActiveRecord(CoffeeRepo)

  val saveAction = Coffee("Colombia").save()
}

The mapping is exactly the same. However, we define it inside CoffeeRepo which implements EntityActions.

Building blocks

The previous code fragment shows all necessary building blocks we need:

1 The first thing to note is the import jdbcProfile.api._. As said before Slick requires jdbc driver. If this line is not included many of the necessary Slick traits, classes and extension methods won’t be available.
2 Define a BaseTypedType for the Entity’s Id by looking up on the implicit scope. This is probably the most intriging one. This is necessary to build queries involving the `Id. Note that you don’t need to make it implicit yourself. This is already done by the internals of EntityActions implementation.
3 Type Member for the Entity type. We choose to use Type Member instead of Type Parameters because it make the code a little bit cleaner when composing with other traits. (see OptimisticLocking trait and SupplierDao in Schema.scala).
4 Type Member for the Entity’s Id
5 Type Member pointing to the Entity’s Table. This is required by EntityActions as the predefined methods must know the type of the table.
6 The ubiquitous Slick TableQuery. This is required by EntityActions in order to implement the basic methods.
7 A method returning the Id column (ie: Rep[Id]). This is how we indicate which table column must be used for all operations involving the primary key.
8 Finally a Lens to get and set the Id on the Entity.

We have now a well known column for our primary key. The next step is to define the method to extract the id from our model and to add a generated id back into the model.

Transactions

As of version 0.3.0 and the introduction of DBIO in Slick 3.0, all methods return DBIO (not Futures). In Slick 3.0 the DB sessions and the transactional sessions are not passed as implicit parameters therefore it is the user that have to manage the sessions and transactions.

If ActiveSlick were returning Futures instead, then the transactions will have to be managed internally by ActiveSlick which is of course not desirable.

Optimistic Locking

Optimistic locking is supported by means for a version column of type Long.

  case class Coffee(name: String, version: Long = 0, id: Option[Int] = None)

  object CoffeeRepo extends EntityActions with OptimisticLocking with H2ProfileProvider {

    import jdbcProfile.api._

    def $version(table: CoffeeTable): Rep[Long] = table.version // (1)
    def versionLens = lens { coffee:Coffee => coffee.version }  // (2)
                           { (coffee, vers) => coffee.copy(version = vers) }


    class CoffeeTable(tag: Tag) extends Table[Coffee](tag, "COFFEE") {
      def name = column[String]("NAME")
      def id = column[Id]("ID", O.PrimaryKey, O.AutoInc)
      def version = column[Long]("VERSION")
      def * = (name, version, id.?) <>(Coffee.tupled, Coffee.unapply)
    }
    ...
  }

The OptimisticLocking requires two new methods to implement in order to glue the version field to the version column.

1 A method returning the version column (ie: Rep[Long]). This is how we indicate which table column must be used store the version information.
2 A Lens to get and set the version on the Entity.

Note that we can mixin the trait OptimisticLocking without specifying the type of the Entity nor the Id type. This is due to the usage of Type Members.

If we were using Type Parameters, we’ll be forced to repeat the types each time as in: EntityActions[Coffee, Int](H2Driver) with OptimisticLocking[Coffee, Int]

Before Insert and Update hooks

EntityActions provide two hooks to modify Entity data before insert and update. One can overwrite these methods to add extra checks or modify the Entity just before insert and updates.

A possible usage could be to automatically update lastUpdated field for audit purposes. (see EntityActionsBeforeInsertUpdateTest.scala for a concrete example)

In both cases, the returned DBIO is combined will be combined with the corresponding DBIO (from insert or update methods) and executed in the same transaction.

def beforeInsert(entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
    // default implementation does nothing
    DBIO.successful(entity)
}

def beforeUpdate(id: Id, entity: Entity)(implicit exc: ExecutionContext): DBIO[Entity] = {
    // default implementation does nothing
    DBIO.successful(entity)
}