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 Sessions, 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.