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:
SomeDAO.makeChanges()
and ends in SomeOtherDAO.makeChanges()
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?
Ended up doing this:
First: define inline
function that accepts a task (lamda), opens the session, executes given task, commits the transaction and closes the session (note suspend
keyword is required for my specific use case and is not required for the genereal approach!)
suspend inline fun executeInSession(task: suspend (session: Session) -> Any) : Any {
val session = sessionFactory.openSession()
session.beginTransaction()
val taskOutcome = task(session)
session.transaction.commit()
session.close()
return taskOutcome
}
Second: define functionality in handling function (e.g. doStuff()
) and pass it to executeInSession()
:
suspend fun doStuff(call: ApplicationCall) {
HibernateDBA.executeInSession {
SomeDAO(session).makeChanges(someObj)
SomeOtherDAO(session).makeChanges(someOtherObj)
}
}
Edit - some comments:
doStuff()
and the passed task()
require the suspend
keyword because they're handling
io.ktor.application.ApplicationCalls
, which are coroutinesexecuteInSession()
needs to be suspend
ed because
it's inlining suspend
-functionstask
and the
executeInSession()
function return Any
. Should you only pass
self-contained tasks, you can ommit the return type definition from
executeInSession()
and change the lambda's return type to Unit