Cookbook
This section of the manual is for solutions to common questions.
Mapping more than 22 fields
Problem
You have a table with more than 22 fields, and the Scala 2 compiler has told you:
too many elements for tuple: 23, allowed: 22
Solution
Switch from using a tuple in your default projection to using an HList.
First, add the HList imports:
sourceimport slick.collection.heterogeneous._
import slick.collection.heterogeneous.syntax._
Your case class and table definition are as unchanged, but your default projection ( the *
method) changes:
sourcecase class Row(id: Int, name: String /* ... as many as you like */)
class MyTable(tag: Tag) extends Table[Row](tag, "ROW") {
def id = column[Int]("ID", O.PrimaryKey)
def name = column[String]("NAME")
/* ... as many as you like */
def * = (id :: name /* as many as you like... */ :: HNil).mapTo[Row]
}
Track the Number of Query Compilations
Problem
Query compilation can be expensive. We can use the Compiled
construct to avoid re-compiling queries. However, it can also be easy to accidentally forget to use this construct or revert its usage. When this happens in a high-traffic deployment, query times and CPU usage can increase drastically.
To identify this type of regression, we’d like to track the number of query compilations. We might then expose this count as an application metric and setup an alert which triggers when an abnormally high number of query compilations occur.
Solution
To track the number of query compilations, we can override the computeQueryCompiler
method in our profile. The new QueryCompiler
will have an additional phase, which simply increments a counter.
sourceimport slick.jdbc.JdbcProfile
import slick.compiler._
import java.util.concurrent.atomic.AtomicLong
trait MyProfile extends JdbcProfile {
// This counter can be accessed from an instance of `MyProfile`
// and exposed as an application metric.
final val queryCompilationCounter: AtomicLong = new AtomicLong(0)
// This method gets called to setup the QueryCompiler.
// We'll attach a CompilerPhase that just increments the compilation counter
// and leave the compiler otherwise unchanged.
override def computeQueryCompiler: QueryCompiler = {
val phase = new Phase {
override type State = this.type
override val name: String = "IncrementQueryCompilationCounter"
override def apply(state: CompilerState): CompilerState = {
// When this Phase gets applied, it just increments the counter and
// passes the CompilerState through unchanged.
queryCompilationCounter.incrementAndGet()
state
}
}
super.computeQueryCompiler + phase
}
}
Distinguish DBIOActions by Effect type at runtime
Problem
When using a replicated database with a primary (read/write) node and one or more replica (read-only) nodes, we have to maintain separate database handles and run each DBIOAction
on the appropriate database handle. If a DBIOAction
containing a write goes to the replica, it will fail. If a read-only DBIOAction
goes to the primary, we lose the benefit of using a replicated database. In a large application, it’s tedious to remember to use the right handle for every DBIOAction
, and it’s easy to mix them up.
Ideally we would have a single method like run
that takes a DBIOAction
and determines whether to run it on the primary or the secondary.
Slick provides a phantom Effect
type parameter in each DBIOAction
, but it’s not immediately obvious how to use at runtime. For example, we cannot pattern-match on the Effect
type parameter, as this parameter is erased at runtime due to type erasure.
Solution
This solution shows one way that we can introspect the Effect
type in order to determine whether the DBIOAction
should go to the primary or the secondary. We define a type class called EffectInfo.ReadOnly
, which has a single member isReadOnly: Boolean
. We provide instances of the type class for the various subtypes of Effect
, such that isReadOnly
is true only for Effect
types Read
and Read with Transactional
. For any other subtypes, isReadOnly
is false. We implement a run
method which takes a DBIOAction
and an implicit instance of the EffectInfo.ReadOnly
type class for the DBIOAction
’s Effect
type. We summon the type class instance and check the isReadOnly
member to determine whether the given DBIOAction
should go to the primary or to the secondary.
sourceimport slick.dbio._
object EffectInfo {
/**
* Type class which can be summoned to determine if a given DBIOAction is read-only,
* e.g., to let us determine whether to run the DBIOAction on the primary or the secondary.
*/
sealed trait ReadOnly[E <: Effect] {
def isReadOnly: Boolean
}
/**
* Base trait provides an instance of ReadOnly where isReadOnly = false.
* This is extended below by the ReadOnly companion object, which overrides the instance of ReadOnly for effect
* types where isReadOnly should be true.
*/
sealed trait ReadOnlyFalse {
implicit def readOnlyFalse[E <: Effect]: ReadOnly[E] = new ReadOnly[E] {
val isReadOnly: Boolean = false
}
}
/**
* Companion object providing implementations of ReadOnly for specific types where isReadOnly = true.
*/
object ReadOnly extends ReadOnlyFalse {
// When Effect.Read is used by itself, isReadOnly is true.
implicit object Read extends ReadOnly[Effect.Read] {
val isReadOnly: Boolean = true
}
// When Effect.Read is used with Effect.Transactional, isReadOnly is true,
// as a transactional read is still a read.
implicit object ReadWithTransactional extends ReadOnly[Effect.Read with Effect.Transactional] {
val isReadOnly: Boolean = true
}
}
}
// For brevity, this run method just demonstrates that we can distinguish the effect type at runtime.
// In a real application, we could place this method in a class that contains a handle to the primary and a handle
// to the secondary. Then the callers just call `run(action)` and don't worry about distinguishing the two handles.
def run[A, E <: Effect: EffectInfo.ReadOnly](action: DBIOAction[A, NoStream, E]): Unit =
if (implicitly[EffectInfo.ReadOnly[E]].isReadOnly) println("Action is read-only, run it on the secondary")
else println("Action is not read-only, run it on the primary")
// We define a generic action and narrow it to specific effect types below.
val action = DBIO.successful(42)
// Read-only actions.
// Each of these prints "Action is read-only, run it on the secondary"
run(action: DBIOAction[Int, NoStream, Effect.Read])
run(action: DBIOAction[Int, NoStream, Effect.Read with Effect.Transactional])
run(action: DBIOAction[Int, NoStream, Effect with Effect.Read with Effect.Transactional])
run(action: DBIOAction[Int, NoStream, Effect.Transactional with Effect.Read with Effect])
// Other actions.
// "Action is not read-only, run it on the primary"
run(action: DBIOAction[Int, NoStream, Effect])
run(action: DBIOAction[Int, NoStream, Effect.Write])
run(action: DBIOAction[Int, NoStream, Effect.Schema])
run(action: DBIOAction[Int, NoStream, Effect.All])