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. 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 ```java
@Transactional @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. 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]: 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. 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.