This post arose out of a curiosity I had with regards to database connection/session management while using Slick, but I think what I ended up learning is uesful in a general sense.
First, Some Background
Slick’s main pattern for accessing a database uses Session
s, which are passed implicitly to many of its methods, and are generally obtained using a pattern like this:
database.withSession { implicit session =>
// run queries, etc
}
We also abstracted most of our database interaction logic into an “access layer” that looked something like this:
trait AccessLayer {
def getUser(name: String)(implicit session: Session) = ...
def lookupThing(id: ThingId)(implicit session: Session) = ...
...
}
And eventually we implemented a caching wrapper for the access layer
class CachingAccessLayer(delegate: AccessLayer) extends AccessLayer {
def getUser(name: String)(implicit session: Session) = {
cache.getOrElseUpdate(name, {delegate getUser name })
}
...
}
Room For Improvement
This system led me to wonder, “would it be possible to interact with the caching access layer without necessarily opening a database connection (session)?” If there’s going to be a cache hit, no connection would be necessary. On the other hand, if the cache is going to miss, you do need a connection. I also wouldn’t want to change the access layer to manage its own sessions, because sometimes many tasks from multiple access layers need to be done in a single transaction.
To meet these requirements, I set about designing a system that could open a session on demand (like database.withSession
), but that wouldn’t open a new session if one were already available.
The Solution
The key to my implementation is the fact that implicit arguments in Scala can have default arguments. If the implicit instance isn’t available in the scope where it is required, the default value will be passed instead.
// to demonstrate implicit default arguments
def hasImplicit[T](implicit t: T = null): Boolean = {
if(t == null) false else true
}
Using this principle, I can make a method that returns different values depending on the availability of an implicit Session
. The type of these values will be named SessionManager
:
trait SessionManager {
def withSession[T](f: Session => T): T
}
Note that the SessionManager#withSession
method has the same signature as database.withSession
. Now I’ll define two implementations of the trait.
object DefaultSM extends SessionManager {
def withSession[T](f: Session => T): T = {
// Just delegate to `database`.
// This opens and closes a connection.
database withSession f
}
}
class ExistingSM(s: Session) extends SessionManager {
def withSession[T](f: Session => T): T = {
// Pass the existing session to f.
// Don't open or close anything.
f(s)
}
}
Now, write an implicit method that will return one or the other, depending on whether there is an implicit Session
in scope.
object SessionManager {
implicit def getSM(implicit s: Session = null): SessionManager = {
if(s == null) DefaultSM else ExistingSM(s)
}
}
Defining the implicit getSM
method in the SessionManager
companion object guarantees the availability of an implicit SessionManager
without the need to import anything.
From here, I just needed to refactor the AccessLayer
to accept an implicit SessionManager
instead of Session
, and use that SessionManager
‘s withSession
method when it actually came time to use the Slick API.
object SlickAccessLayer extends AccessLayer {
def getUser(name: String)(implicit sm: SessionManager) = {
sm.withSession { implicit session =>
// run queries etc
}
}
}
class CachingAccessLayer(delegate: AccessLayer) extends AccessLayer {
val cache = ...
def getUser(name: String)(implicit sm: SessionManager) = {
cache.getOrElseUpdate(name, { delegate getUser name })
}
}
val accessLayer = new CachingAccessLayer(SlickAccessLayer)
Results
Now, calling accessLayer.getUser
outside of a session will automatically open one up once the implementation needs a session, but calling the same method within a session will simply reuse that session.
// cache miss: a session is opened in the SlickAccessLayer
accessLayer.getUser("dylemma")
// cache hit: no session is opened
accessLayer.getUser("dylemma")
// a connection is opened, and reused for each access call
database.withSession { implicit session =>
accessLayer.getUser("dylemma")
accessLayer.getUser("bob")
accessLayer.getUser("jane")
}
To see the whole picture, I created a gist with a working example.