Search code examples
hibernatekotlinsessiontransactionsktor

Hibernate: how to improve handling of Sessions and Transactions?


I'm currently developing a server-application with Kotlin/ktor and Hibernate (no Spring!), and I'm not quite satisfied with how I handle Hibernate sessions and transactions. So I'm looking for some pointers on how to improve my setup or (if justified) some reassurance that what I'm doing isn't actually that bad.

Any call that is made will be forwarded to a handler function, which is called in ktors routing feature.

route("foo"){
    get {
        FooHandler.doStuff(call)
    }
    //...
}

Omitting handling request and response, these functions (e.g. doStuff()) might then look like this:

suspend fun doStuff(call: ApplicationCall) {
    val session = HibernateDBA.sessionFactory.openSession()
    session.beginTransaction()

    SomeDAO(session).makeChanges(someObj)
    SomeOtherDAO(session).makeChanges(someOtherObj)

    session.transaction.commit()
    session.close()
}

As you can see, I pass the session to the DAOs' constructors, because makeChanges() requires the session (e.g. for session.persist(someObj)). What botheres me here is that I have to include the same 4 lines of code in each and every handler function.

I would also like to point out that I'm intentionally including more than one DAO in my example, as this is a common occurance in my application, and pretty much the core reason why I'm struggling to find a satisfying solution. If this is dumb, feel free to point it out, as I'm still learning to work with Databases and Hibernate specifically. In that case, I'd be very grateful for advice on how to avoid this.

My initial idea was to intercept each call to create my session at Routing.RoutingCallStarted and close it at Routing.RoutingCallFinished. That would remove 2 of the lines in each function, but force me to somehow manage the relationship between call and session (in a map or something), which seems like overkill to me.

Another approach was to link my DAOs where needed and either open the session on constructor call or pass the existing session to the constructor if needed, which could look something like this:

class SomeDAO (val session: Session = HibernateDBA.sessionFactory.openSession()) {
    fun makeChanges(someObj : SomeClass, someOtherObj : SomeOtherClass) {
        session.persist(someObj)
        SomeOtherDAO(session).makeChanges(someOtherObj)
    }
}

DomeOtherDAO would have the same layout, including the constructor.

Then my handler would only have to call SomeDAO().makeChanges(someObj, someOtherObj), instead of the six lines of code in the second block. Sounds great, except it doesn't:

  • Now I pass someOtherObj to a SomeDAO, which is obviously not right
  • I still haven't solved Transaction handling. I didn't forget about it, I just couldn't even come up with a pseudo-solution in this example. I'd have to know that a transaction alway begins in SomeDAO.makeChanges() and ends in SomeOtherDAO.makeChanges()
  • Also, while reading into that idea, I came across multiple statements reassuring me that linking DAOs is just generally bad practice

That's where I'm stuck right now. I have some idea how not to do it, but I still don't feel closer to where I want to get. So my concluding question would be: where (and maybe how) should I manage my sessions and transactions in order to be able to work across multiple DAOs?


Solution

  • Although the accepted answer is correct, you may want to rollback the transaction on failures:

    suspend inline fun <T> transactional(crossinline task: suspend (Session) -> T): T {
        val session = sessionFactory.openSession()
        session.beginTransaction()
    
        return try {
            val result = task(session)
            session.transaction.commit()
            result
        } catch (e: Exception) {
            session.transaction.rollback()
            throw e
        } finally {
            session.close()
        }
    }
    

    Then wrap database calls as next:

    route("/some-route") {
        get {
            transactional {
                // Database operations here
            }
        }
    }