Scala Slick type-safe ids
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.
ScalaDoc API:
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.10.x/2.11.x and Slick 3.0.x) use:
libraryDependencies += "org.virtuslab" %% "unicorn-core" % "1.0.0"
For play version (Scala 2.10.x/2.11.x, Slick 2.1.x, Play 2.3.x):
libraryDependencies += "org.virtuslab" %% "unicorn-play" % "1.0.0"
Or see our Maven repository.
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.LongUnicornPlay._
import org.virtuslab.unicorn.LongUnicornPlay.driver.api._
import slick.lifted.Tag
/** Id class for type-safe joins and queries. */
case class UserId(id: Long) extends AnyVal with BaseId
/** 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[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 org.virtuslab.unicorn.LongUnicornPlay._
import org.virtuslab.unicorn.LongUnicornPlay.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 BasePlayTest {
val usersRepository: UsersRepository = new UsersRepository
"Users Service" should "save and query users" in runWithRollback {
val user = UserRow(None, "test@email.com", "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
}
}
}
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):
package infra
import org.virtuslab.unicorn.{HasJdbcDriver, LongUnicornCore}
import slick.driver.H2Driver
object Unicorn extends LongUnicornCore with HasJdbcDriver {
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.Unicorn._
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
/** 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[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, "test@email.com", "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
object StringPlayUnicorn extends UnicornPlay[String]
Core example
object StringUnicorn extends UnicornCore[String] with HasJdbcDriver {
override 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.