db session mngmnt in go: typo + addition

This commit is contained in:
Wouter Groeneveld 2024-06-12 19:42:26 +02:00
parent f29a1268d9
commit 5d13699271
1 changed files with 5 additions and 3 deletions

View File

@ -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.