fib1123 / unicorn

Small Slick library for type-safe id handling

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Scala Slick type-safe ids

Join the chat at https://gitter.im/VirtusLab/unicorn Build Status 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 service 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 for 0.6.4-SNAPSHOT.

Unicorn is Open Source under Apache 2.0 license.

Contributors

Authors:

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 2.1.x) use:

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

For play version (Scala 2.10.x/2.11.x, Slick 2.1.x, Play 2.3.x):

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

Or see our Maven repository.

For Slick 2.0.x see version 0.5.x.

For Slick 1.x see version 0.4.x.

Migration form 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 shows how to make use of unicorn then.

Defining entities

package model

import org.virtuslab.unicorn.LongUnicornPlay._
import org.virtuslab.unicorn.LongUnicornPlay.driver.simple._
import scala.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.
  *
  * @param id user id
  * @param email user email address
  * @param lastName lastName
  * @param firstName firstName
  */
case class User(id: Option[UserId],
                email: String,
                firstName: String,
                lastName: String) extends WithId[UserId]

/** Table definition for users. */
class Users(tag: Tag) extends IdTable[UserId, User](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", O.NotNull)

  def firstName = column[String]("FIRST_NAME", O.NotNull)

  def lastName = column[String]("LAST_NAME", O.NotNull)

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

Defining repositories

package repositories

import model._
import org.virtuslab.unicorn.LongUnicornPlay._
import org.virtuslab.unicorn.LongUnicornPlay.driver.simple._

/**
 * Repository for users.
 *
 * It brings all base service 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, User, Users](TableQuery[Users])

Usage

package repositories

import model.User
import org.virtuslab.unicorn.BasePlayTest

class UsersRepositoryTest extends BasePlayTest {

  "Users repository" should "save and query users" in rollback { implicit session =>
    // setup
    val repository = new UsersRepository
    repository.create

    val user = User(None, "test@email.com", "Krzysztof", "Nowak")
    val userId = repository save user
    val userOpt = repository findById userId

    userOpt.map(_.email) shouldEqual Some(user.email)
    userOpt.map(_.firstName) shouldEqual Some(user.firstName)
    userOpt.map(_.lastName) shouldEqual Some(user.lastName)
    userOpt.flatMap(_.id) shouldNot 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):

package com.example

import org.virtuslab.unicorn.{HasJdbcDriver, LongUnicornCore}
import scala.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 com.example.tables

import package com.example.Unicorn._
import package com.example.Unicorn.driver.simple._

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

/** Companion object for id class and ordering fpr Id */
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[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", O.NotNull)

  def firstName = column[String]("FIRST_NAME", O.NotNull)

  def lastName = column[String]("LAST_NAME", O.NotNull)

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

Defining repositories

package com.example.repositories

import com.example.tables._
import play.api.db.slick.Config.driver.simple._
import org.virtuslab.unicorn.repositories._

/**
 * Repository for users.
 *
 * It brings all base service methods with it from [[service.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 com.example.test

import com.example.repositories.UserRepository

class UsersRepositoryTest extends BaseTest {

  val userRepository = new UserRepository

  "Users Service" should "save and query users" in rollback { implicit session =>
    // setup
    usersQuery.ddl.create

    val user = UserRow(None, "test@email.com", "Krzysztof", "Nowak")
    val userId = userRepository save user
    val userOpt = userRepository findById userId

    userOpt.map(_.email) shouldEqual Some(user.email)
    userOpt.map(_.firstName) shouldEqual Some(user.firstName)
    userOpt.map(_.lastName) shouldEqual Some(user.lastName)
    userOpt.flatMap(_.id) shouldNot be(None)
  }
}

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.

About

Small Slick library for type-safe id handling

License:Other


Languages

Language:Scala 100.0%