diff --git a/content/post/2024/06/database-session-management-in-go.md b/content/post/2024/06/database-session-management-in-go.md index 979ccfc1..792723e5 100644 --- a/content/post/2024/06/database-session-management-in-go.md +++ b/content/post/2024/06/database-session-management-in-go.md @@ -9,7 +9,7 @@ categories: For a Go project we're working on, the ever-increasing boilerplate code yet again started getting on my nerves. This seems to be a recurring story in enterprise software projects written in Go, but I digress. This time, the question is: how to do proper database transaction management, the idiomatic Go way? The thing we were trying to solve must have been solved more than a thousand times before, by others using Go, but also by others using classic object-oriented languages such as C# and Java. -In Java, we simply rely on Spring and JPA and Hibernate andWhatever-Its-Called-Now to conjure up a piece of middleware that automagically opens up and commits or rolls back the transaction. This is typically done using Aspect Oriented Programming. If the framework works---and that's usually the point of a frame _work_---all the programmer has to do is to add `@Transactional` on top of a method: +In Java, we simply rely on Spring and JPA and Hibernate and Whatever-Its-Called-Now to conjure up a piece of middleware that automagically opens up and commits or rolls back the transaction. This is typically done using Aspect Oriented Programming. If the framework works---and that's usually the point of a frame _work_---all the programmer has to do is to add `@Transactional` on top of a method: ```java @Transactional @@ -25,7 +25,7 @@ public void SaveInvoice(Invoice invoice) { If the method fails, the transaction is automatically rolled back. If `SaveInvoice()` is called upon multiple times in parallel, the behind-the-scenes transaction manager that the annotation `@Transactional` uses still works: it usually relies on a thread-static session that's unique for each incoming request. -This seems so simple and obvious, but let's still ask the question: how do you do this in Go? Use an unexported global variable? Create something new for each request? Rely on `context.Context`? +This seems so simple and obvious, but let's still ask the question: how do you do this in Go? Use an unexported global variable? Create something new for each request? Leverage the current `context.Context`? We rely on [GORM](https://gorm.io/docs/transactions.html), an ORM framework that could be compared to Hibernate except that it's much more lightweight---and as a result, leaves the transaction management up to you. The GORM way to open a transaction is perhaps weird: it returns a new pointer with the same type of `*gorm.DB`[^er]: @@ -92,7 +92,9 @@ func (c *invoiceCmd) Save(invoice Invoice) error { } ``` -This allows us to use GORM's `Transaction()` convenience wrapper in true [Transaction Script](https://martinfowler.com/eaaCatalog/transactionScript.html) style as Martin Fowler likes to call it---or, if we really need to, manage transactions manually. +This allows us to use GORM's `Transaction()` convenience wrapper in true [Transaction Script](https://martinfowler.com/eaaCatalog/transactionScript.html) style as Martin Fowler likes to call it---or, if we really need to, manage transactions manually[^trs]. + +[^trs]: Although it is technically possible to use a thread-static variable bound to the current goroutine that was spun up with the request, there is no way to resolve the current thread using something like Java's `Thread.currentThread()`. Again, that's [considered bad code design](https://pkg.go.dev/github.com/davecheney/junk/id?utm_source=godoc). Still, it just bugs me that (1) this code is much more cluttered (thank you [Go error handling](/post/2024/03/error-handling-no-goes-in-go/)) and (2) you have to pass in `tx`, although that can be solved by creating your repositories within the `Transaction()` func. What about unit testability then? I suppose writing an integration test against an in-memory SQLite database is your only option then, as there's no way to inject other behaviour.