Support per-field update with Scala Slick

I want to show you how I support per-field update with Slick + Postgres.

Note

This blog serves as a documentation for PlayFast, an opinionated production-ready PlayFramework template that makes you productive.

It bothered me for a long while that I had to make an update function for each use case. While it's not a bad thing, I find it to be too verbose for my taste. I just wanted a single update function that updates the chosen fields.

Designing API

Since we want a single function that can updates some fields and ignore the others, we need a model that signifies whether a field should be updated; if so, which value should be the new value?

We'll call the model UpdateField, and it is very similar to Option with a different semantics. Here's how it looks like:

sealed trait UpdateField[+T] {
  def toOption: Option[T] = this match {
    case UpdateField.Update(u) => (Some(u): Option[T])
    case UpdateField.NoUpdate  => None
  }
}

object UpdateField {
  case class Update[T](value: T) extends UpdateField[T]
  case object NoUpdate extends UpdateField[Nothing]
}

Next we make a model for the update itself:

case class UpdateData(
  name: UpdateField[String] = NoUpdate,
  address: UpdateField[Option[String]] = NoUpdate
)

As you can see, the above model defaults every field to NoUpdate. If you want a field to be updated, then you can set it to: Update("SomeValue").

Next we make a method to execute the update:

def update(id: String, data: UpdateData): Future[Unit] = {
    ...
}

With the above method, you can select fields to update as shown below:

update(id, UpdateData(name = Update("New name"))
update(id, UpdateData(address = Update(Some("New address")))
update(id, UpdateData(name = Update("New name"), address = Update(None))

This will support updating any field selected by you!

Implementation

Since we use Postgres, we can utilize the transaction functionality and issue multiple SQLs in order to make the code as succinct as possible. Slick supports transaction, so this is quite elegant.

Here's how it looks like:

val query: TableQuery[UserTable] = TableQuery[UserTable]
...

def update(id: String, data: UpdateData): Future[Unit] = {
  val base = query.filter(_.id === id)

  val updates = Seq(
    data.name.toOption.map { v => base.map(_.name).update(v) },
    data.address.toOption.map { v => base.map(_.address).update(v) },
  ).flatten

  db.run(DBIO.sequence(updates).transactionally).map(_ => ())
}

And that is it.

Future improvements

  1. I probably want a mechanism to guarantee every field is set to updated if I choose to. This will be helpful when a refactoring occurs e.g. adding a new field where the compilation will fail if the new field isn't set to update. It's an annoying problem but not enough for me to solve it.
  2. Regarding data.name.toOption and data.address, this is the most succinct form I can come up with. I wish there would be a way to make that part more succinct.

Some day I'll sit down and make these improvements.

Anyway, if you are interested in a working code, you can go to PlayFast, an opinionated production-ready PlayFramework template that makes you productive.

Thank you!

Subscribe to tanin

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe