Common Model Fields With Slick 3 (Part I)

Since Slick isn't an ORM it doesn't provide some common features that we expect to have out of the box. One of those features is the ability to have common fields for all our models. For the sake of demonstration let's assume we want to all our models to have three common fields: id, createdAt and active. The first two are self-explanatory and the active field can be used to avoid issuing DELETEs.

We want to be able to easily share these three common fields while minimizing the amount of boilerplate and/or code duplication. The ideal solution should make it easy to share common database functionality: paging, sorting and filtering as a few examples; and minimize the use of exotic Slick facilities.

case class CommonFields(
  id: String = UUID.randomUUID.toString,
  createdAt: Timestamp = new Timestamp(System.currentTimeMillis),
  active: Boolean = true
)

case class CommonColumns(
  id: Rep[String],
  createdAt: Rep[Timestamp],
  active: Rep[Boolean]
)

implicit object CommonShape extends CaseClassShape(
  CommonColumns.tupled, CommonFields.tupled
)

CommonFields defines the common fields we want on every model and CommonColumns has those fields "lifted" into columns. Finally we have a CaseClassShape that provides a way pack and unpack the fields.

Let's give this a spin and define a simple User model that has these common fields. We'll also create a small trait to get access to the common fields as if they were defined on the model directly.

trait CommonModel {
  def commonFields: CommonFields

  def id = commonFields.id

  def createdAt = commonFields.createdAt

  def active = commonFields.active
}

case class User(
  firstName: String,
  lastName: String,
  commonFields: CommonFields = CommonFields()
) extends CommonModel

Now that we've got a DRY-er approach to model creation we want to apply the same principles to the Table and TableQuery instances we'll need for our User model.

abstract class CommonTable[Model <: CommonModel](
  tag: Tag,
  tableName: String
) extends Table[Model](tag, tableName) {
  def id = column[String]("id", O.PrimaryKey)
  def createdAt = column[Timestamp]("createdAt")
  def active = column[Boolean]("active")
}

class Users(tag: Tag) extends CommonTable[User](tag, "users") {
  def firstName = column[String]("firstName")
  def lastName = column[String]("lastName")

  def * = (
    firstName, lastName, CommonColumns(id, createdAt, active)
  ) <> (User.tupled, User.unapply)
}

In defining the table we avoid having to rewrite all the common column definitions and only need to reference them when creating the default table projection. Although we have a nested case class we get to interact and treat the common fields as though they were all fields defined directly on our model. As one example look at the SQL generated for creating the table by Slick:

scala> TableQuery[Users].schema.create.statements.foreach(println)
create table "users" (
    "firstName" VARCHAR NOT NULL,
    "lastName" VARCHAR NOT NULL,
    "id" VARCHAR NOT NULL PRIMARY KEY,
    "createdAt" TIMESTAMP NOT NULL,
    "active" BOOLEAN NOT NULL
)

All of our columns were added to the schema as we would expect and they would if we had defined them directly on the model/table. In a future post I'm going to go into more detail about how you can share common querying by building on the constructs described in this post.

Built With: Slick 3.1.1, PostgreSQL JDBC Driver 4 9.4.1207, Scala 2.10.5