Using raw types such as Long
, UUID
, and Option[Long]
for database ids invites errors.
Scala developers should instead use the Id
and HasId
wrapper types provided by this project
because of the type safety they provide over raw types.
Id
and HasId
are database-agnostic.
Both auto-increment Id
s and Id
s whose value is defined before persisting them are supported.
Id
can wrap Long
, UUID
and String
values, and any of them can be optional.
The supported flavors of Id
are:
Id[Long]
– maps to PostgresBIGINT
orBIGSERIAL
Id[UUID]
– do not misuseId[String]
Id[Option[Long]]
– commonly used with autoincrement columns such asBIGSERIAL
Id[Option[UUID]]
Id[Option[String]]
Id
is a Scala value object, which means there is little or no runtime cost for using it as compared to the value that it wraps.
In other words, there is no penalty for boxing and unboxing.
For convenience, the following types are defined in model.persistence.Types
:
OptionLong
–Option[Long]
OptionString
–Option[String]
OptionUuid
–Option[UUID]
IdLong
–Id[Long]
IdString
–Id[String]
IdUuid
–Id[UUID]
IdOptionLong
–Id[Option[Long]
IdOptionString
–Id[Option[String]]
IdOptionUuid
–Id[Option[UUID]]
Id
s define a special value, called empty
.
Each Id
flavor has a unique value for empty
.
FYI, the values for empty
are:
IdUuid.empty == new UUID(0, 0)
IdLong.empty == 0L
IdString.empty == ""
IdOptionUuid.empty = None
IdOptionLong.empty = None
IdOptionString.empty = None
Depending on the context, you might need to provide type ascription when using Id.empty
.
For example, IdUuid.empty
or IdOptionLong.empty
.
You can use the Id.toOption
method to convert from an IdLong
or IdUuid
to IdOptionLong
or IdOptionUuid
.
scala> import model.persistence._
import model.persistence._
scala> Id(Option(123L)).toOption
res2: model.persistence.Id[_ >: Option[Long] with Option[Option[Long]]] = 123
Be sure to cast the result to the desired Id
subtype, otherwise you'll get a weird unhelpful type:
scala> Id(Option(123L)).toOption.asInstanceOf[IdLong]
res3: model.persistence.Id[Long] = 123
scala> import java.util.UUID
import java.util.UUID
scala> Id(Option(UUID.randomUUID)).toOption.asInstanceOf[IdUuid]
res3: model.persistence.Id[java.util.UUID] = b4570530-14d0-47d6-9d8b-af3b58ed075a
Each case class that uses Id
to represent the persisted record id in the database must extend HasId
.
HasId
is a parametric type with two type parameters:
- The first type parameter must match the name of the case class
- The second type parameter must match the type of the
Id
for the case class. For example: HasId[MyCaseClass, Long]
HasId[MyCaseClass, UUID]
HasId[MyCaseClass, String]
HasId[MyCaseClass, OptionLong]
– Most commonly used flavorHasId[MyCaseClass, OptionUuid]
HasId[MyCaseClass, OptionString]
Here are examples of using Id
and HasId
:
/** A person can have at most one Dog.
* Because their Id is based on `OptionUuid`, those `Id`s do not always have `Some` value */
case class Person(
age: Int,
name: String,
dogId: IdOptionLong,
override val id: IdUuid = Id(UUID.randomUUID) // Id type (UUID) matches the HasId type (also UUID)
) extends HasId[Person, UUID]
/** Dogs are territorial. They ensure that no other Dogs are allowed near their FavoriteTrees.
* Because the Ids for Dog and FavoriteTree are based on Option[Long] and not UUID,
* those Ids might have value None until they are persisted */
case class Dog(
species: String,
color: String,
override val id: IdOptionLong = Id.empty
) extends HasId[Dog, OptionLong]
Subclasses of HasId
subclasses should be parametric.
In the following example, Rateable
is an abstract class that subclasses HasId
.
Notice that Rateable
is parametric in T
, and HasId
's first type parameter is also T
:
abstract class Rateable[T](override val id: IdOptionLong) extends HasId[T, OptionLong]
The following two Rateable
subclasses provide values for T
that match the names of the derived classes:
case class Inquiry(
title: String,
body: String,
userId: IdOptionLong,
lectureId: IdOptionLong,
override val id: IdOptionLong = Id.empty
) extends Rateable[Inquiry](id)
case class Recording(
ohSlotId: IdOptionLong,
transcript: String,
active: Boolean = false,
override val id: IdOptionLong = Id.empty
) extends Rateable[Recording](id)
See the unit tests
for more code examples and documentation.
For an example of has-id
in a real Scala project, see play-authenticated.
Both Scala 2.11 and Scala 2.12 are supported.
Add this to your project's build.sbt
:
resolvers += "micronautics/scala on bintray" at "https://dl.bintray.com/micronautics/scala"
libraryDependencies += "com.micronautics" %% "has-id" % "1.2.8" withSources()
This project is sponsored by Micronautics Research Corporation, the company that delivers online Scala and Play training via ScalaCourses.com. You can learn how this project works by taking the Introduction to Scala, and Intermediate Scala courses.
This software is published under the Apache 2.0 License.