2. Agenda
• Intro to Slick
• How we use it at FortyTwo
• Wrapping Sessions
• Generic Access Layer
3. About Slick
Database query library for Scala
"select * from users where id=?"
or
def get(id: Long): User =
(for(u <- UserTable if u.id is id) yield u).first
or
def get(id: Id[M]): M =
(for(f <- table if f.id is id) yield f).first
Can do: insert, update, delete, DDL and more
4. More About Slick
• Using Options to represent NULLs
• Creates DDLs (tests without Play)
• Type safe in and out
• Syntax safe
• Custom types
• Can use regular sql
• With type safe wrapping
5. Type Safe Concurrency
abstract class RSession(roSession: => Session) extends SessionWrapper(roSession)
class ROSession(roSession: => Session) extends RSession(roSession)
class RWSession(rwSession: Session) extends RSession(rwSession)
class Database @Inject() (val db: DataBaseComponent) {
import DBSession._
private def rawSession = db.handle.createSession()
def readOnly[T](f: ROSession => T): T = {
val s = rawSession.forParams(rsConcurrency = ResultSetConcurrency.ReadOnly)
try f(new ROSession(s)) finally s.close()
}
def readWrite[T](f: RWSession => T): T = {
val s = rawSession.forParams(rsConcurrency = ResultSetConcurrency.Updatable))
try {
rw.withTransaction { f(new RWSession(s)) }
} finally s.close()
}
}
6. Using Sessions
db.readOnly { implicit s =>
// only reading from db - not in a transaction
val users = userRepo.getUsersByLastName(“Smith”)
accountRepo.getAccounts(users)
}
db.readWrite { implicit s =>
// reading and writing to db in a transaction
val users = userRepo.getUsersByLastName(“Smith”)
accountRepo.deleteAccounts(users)
userRepo.markUsersAsInactive(users)
}
db.readOnly { implicit s =>
val user = userRepo.get(userId)
emailRepo.updateEmail(user, newMail) <-- FAIL
}
7. DB Repo
trait Repo[M <: Model[M]] {
def get(id: Id[M])(implicit session: RSession): M
def all()(implicit session: RSession): Seq[M]
def save(model: M)(implicit session: RWSession): M
def count(implicit session: RSession): Int.
def page(page: Int = 0, size: Int = 20)
(implicit session: RSession): Seq[M]
}
8. DB Repo Impl
trait DbRepo[M <: Model[M]] extends Repo[M] {
protected def table: RepoTable[M]
//you don’t have to run the whole Play Application to get the db setup!
def descTable(): String = db.handle.withSession {
table.ddl.createStatements mkString "n"
}
def count(implicit session: RSession): Int = Query(table.length).first
def get(id: Id[M])(implicit session: RSession): M =
(for(f <- table if f.id is id) yield f).first
def all()(implicit session: RSession): Seq[M] = table.map(t => t).list
def page(page: Int = 0, size: Int = 20)(implicit session: RSession): Seq[M] = {
val q = for {
t <- table
} yield t
q.sortBy(_.id desc).drop(page * size).take(size).list
}
}
9. DB Repo Impl - Persisting
def save(model: M)(implicit session: RWSession): M = try {
val toUpdate = model.withUpdateTime(clock.now)
val result = model.id match {
case Some(id) => update(toUpdate)
case None => toUpdate.withId(insert(toUpdate))
}
} catch {
case t: SQLException => throw new SQLException(s"error persisting $model", t)
}
private def insert(model: M)(implicit session: RWSession) =
table.autoInc.insert(model)
private def update(model: M)(implicit session: RWSession) = {
val target = for(t <- table if t.id === model.id.get) yield t
val count = target.update(model)
assert(1 == count)
model
}
10. RepoTable
abstract class RepoTable[M <: Model[M]]
(db: DataBaseComponent, name: String) extends Table[M]
(db.entityName(name)) with TableWithDDL {
//standardizing the following columns for all entities
def id = column[Id[M]]("ID", O.PrimaryKey, O.Nullable, O.AutoInc)
def createdAt = column[DateTime]("created_at", O.NotNull)
def updatedAt = column[DateTime]("updated_at", O.NotNull)
def autoInc = * returning id
//H2 likes its column names in upper case where mysql does not mind.
//the db component should figure it out
override def column[C : TypeMapper](name: String, options: ColumnOption[C]*) =
super.column(db.entityName(name), options:_*)
}
}
11. Example - User
case class User(
id: Option[Id[User]] = None,
createdAt: DateTime,
updatedAt: DateTime,
firstName: String,
lastName: String
) extends Model[User] {
def withName(firstName: String, lastName: String) =
copy(firstName = firstName, lastName = lastName)
}