virtuslab / unicorn   1.4.1

GitHub

Small Slick library for type-safe id handling

Scala versions: 2.13 2.12 2.11 2.10

Scala Slick type-safe ids

Join the chat at https://gitter.im/VirtusLab/unicorn Coverage Status

Slick (the Scala Language-Integrated Connection Kit) is a framework for type-safe, composable data access in Scala. This library adds tools to use type-safe IDs for your classes so you can no longer join on bad id field or mess up order of fields in mappings. It also provides a way to create data access layer with methods (like querying all, querying by id, saving or deleting) for all classes with such IDs in just 4 lines of code.

Idea for type-safe ids was derived from Slick creator's presentation on ScalaDays 2013.

This library is used in Advanced play-slick Typesafe Activator template.

Unicorn is Open Source under Apache 2.0 license.

Contributors

Feel free to use it, test it and to contribute! For some helpful tips'n'tricks, see contribution guide.

Getting unicorn

For core latest version (Scala 2.12.x and Slick 3.3.x) use:

libraryDependencies += "org.virtuslab" %% "unicorn-core" % "1.3.3"

For play version (Scala 2.12.x, Slick 3.3.x, Play 2.7.x):

libraryDependencies += "org.virtuslab" %% "unicorn-play" % "1.3.3"

Or see our Maven repository.

For Slick 3.3.x and play 2.7 see version 1.3.3

For Slick 3.2.x and play 2.6 see version 1.3.2

For Slick 3.2.x and play 2.5 see version 1.2.x

For Slick 3.1.x and play 2.5 see version 1.1.x

For Slick 3.1.x and play 2.4 see version 1.0.x

For Slick 3.0.x see version 0.7.x

For Slick 2.1.x see version 0.6.x

For Slick 2.0.x see version 0.5.x.

For Slick 1.x see version 0.4.x.

Migration from older versions

See our migration guide.

Play Examples

From version 0.5.0 forward dependency on Play! framework and play-slick library is no longer necessary.

If you are using Play! anyway, examples below show how to make use of unicorn then.

Defining entities

package model

import org.virtuslab.unicorn.{BaseId, WithId}
import org.virtuslab.unicorn.LongUnicornPlayIdentifiers._

/** Id class for type-safe joins and queries. */
case class UserId(id: Long) extends AnyVal with BaseId[Long]

/** Companion object for id class, extends IdCompanion
  * and brings all required implicits to scope when needed.
  */
object UserId extends IdCompanion[UserId]

/** User entity.
  *
  * @param id user id
  * @param email user email address
  * @param lastName lastName
  * @param firstName firstName
  */
case class UserRow(id: Option[UserId],
                   email: String,
                   firstName: String,
                   lastName: String) extends WithId[Long, UserId]

Defining composable repositories

package repositories
/**
  * A place for all objects directly connected with database.
  *
  * Put your user queries here.
  * Having them in separate in this trait keeps `UserRepository` neat and tidy.
  */
trait UserBaseRepositoryComponent {
  self: UnicornWrapper[Long] =>
  import unicorn._
  import unicorn.driver.api._


  /** Table definition for users. */
  class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, "USERS") {

    /** By definition id column is inserted as lowercase 'id',
      * if you want to change it, here is your setting.
      */
    protected override val idColumnName = "ID"

    def email = column[String]("EMAIL")

    def firstName = column[String]("FIRST_NAME")

    def lastName = column[String]("LAST_NAME")

    override def * = (id.?, email, firstName, lastName) <> (UserRow.tupled, UserRow.unapply)

  }

  class UserBaseRepository
    extends BaseIdRepository[UserId, UserRow, Users](TableQuery[Users])

  val userBaseRepository = new UserBaseRepository

}
@Singleton()
class UserRepository @Inject() (val unicorn: LongUnicornPlayJDBC)
  extends UserBaseRepositoryComponent with UnicornWrapper[Long] {

  import unicorn.driver.api._
  
  def save(user: UserRow): DBIO[UserId] = {
    userBaseRepository.save(user)
  }
}

Usage

package repositories

import model.UserRow

import scala.concurrent.ExecutionContext.Implicits.global

class UsersRepositoryTest extends BasePlayTest with UserBaseRepositoryComponent {

  "Users Repository" should "save and query users" in runWithRollback {

    val user = UserRow(None, "[email protected]", "Krzysztof", "Nowak")
    val action = for {
      _ <- userBaseRepository.create
      userId <- userBaseRepository.save(user)
      userOpt <- userBaseRepository.findById(userId)
    } yield userOpt

    action.map { userOpt =>
      userOpt.map(_.email) shouldEqual Some(user.email)
      userOpt.map(_.firstName) shouldEqual Some(user.firstName)
      userOpt.map(_.lastName) shouldEqual Some(user.lastName)
      userOpt.flatMap(_.id) should not be (None)
    }
  }
}

Core Examples

If you do not want to include Play! but still want to use unicorn, unicorn-core will make it available for you.

Preparing Unicorn to work

First you have to bake your own cake to provide unicorn with proper driver (in example case H2), as also build new object for Long ID support in entities:

package infra

object LongUnicornIdentifiers extends Identifiers[Long] {
  override def ordering: Ordering[Long] = implicitly[Ordering[Long]]

  override type IdCompanion[Id <: BaseId[Long]] = CoreCompanion[Id]
}

object Unicorn
    extends LongUnicornCore
    with HasJdbcDriver {

  override lazy val driver = H2Driver
}

Then you can use that cake to import driver and types provided by unicorn as shown in next sections.

Defining entities

package model

import infra.LongUnicornIdentifiers._
import infra.Unicorn.driver.api._
import slick.lifted.Tag

/** Id class for type-safe joins and queries. */
case class UserId(id: Long) extends AnyVal with BaseId[Long]

/** Companion object for id class, extends IdMapping
  * and brings all required implicits to scope when needed.
  */
object UserId extends IdCompanion[UserId]

/** User entity.  */
case class UserRow(id: Option[UserId],
                email: String,
                firstName: String,
                lastName: String) extends WithId[Long, UserId]

/** Table definition for users. */
class Users(tag: Tag) extends IdTable[UserId, UserRow](tag, "USERS") {

  // use this property if you want to change name of `id` column to uppercase
  // you need this on H2 for example
  override val idColumnName = "ID"

  def email = column[String]("EMAIL")

  def firstName = column[String]("FIRST_NAME")

  def lastName = column[String]("LAST_NAME")

  override def * = (id.?, email, firstName, lastName) <> (UserRow.tupled, UserRow.unapply)
}

Defining repositories

package repositories

import infra.Unicorn._
import infra.Unicorn.driver.api._
import model._

/**
 * Repository for users.
 *
 * It brings all base repository methods with it from [[BaseIdRepository]], but you can add yours as well.
 *
 * Use your favourite DI method to instantiate it in your application.
 */
class UsersRepository extends BaseIdRepository[UserId, UserRow, Users](TableQuery[Users])

Usage

package repositories

import model.UserRow
import scala.concurrent.ExecutionContext.Implicits.global


class UsersRepositoryTest extends BaseTest[Long] {

  val usersRepository: UsersRepository = new UsersRepository

  "Users Service" should "save and query users" in runWithRollback {
    val user = UserRow(None, "[email protected]", "Krzysztof", "Nowak")

    val actions = for {
      _ <- usersRepository.create
      userId <- usersRepository.save(user)
      user <- usersRepository.findById(userId)
    } yield user

    actions map { userOpt =>
      userOpt shouldBe defined

      userOpt.value should have(
        'email(user.email),
        'firstName(user.firstName),
        'lastName(user.lastName)
      )
      userOpt.value.id shouldBe defined
    }
  }
}

Defining custom underlying type

All reviews examples used Long as underlying Id type. From version 0.6.0 there is possibility to define own.

Let's use String as our type for id. So we should bake unicorn with String parametrization.

Play example

@Singleton()
class StringUnicornPlay @Inject() (databaseConfigProvider: DatabaseConfigProvider)
  extends UnicornPlay[String](databaseConfigProvider.get[JdbcProfile])


object StringUnicornPlayIdentifiers extends PlayIdentifiersImpl[String] {
  override val ordering: Ordering[String] = implicitly[Ordering[String]]
  override type IdCompanion[Id <: BaseId[String]] = PlayCompanion[Id]
}

Core example

object StringUnicornIdentifiers extends Identifiers[String] {
  override def ordering: Ordering[String] = implicitly[Ordering[String]]

  override type IdCompanion[Id <: BaseId[Long]] = CoreCompanion[Id]
}

object Unicorn
    extends UnicornCore[String]
    with HasJdbcDriver {

  override lazy val driver = H2Driver
}

Usage is same as in Long example. Main difference is that you should import classes from self-baked cake. The only concern is that id is auto-increment so we can't use arbitrary type there. We plan to solve this problem in next versions.