Prior to start coding in Go, I’ve implemented multiple web applications in C# and Java. There is a particular pattern that is very common on these languages that is hard to find any mentions about in the Go community.
The pattern is called Session per Request and is particularly useful to decouple business components from database transaction management. This post will drive you through what it is, how to implement, pros/cons and some examples written in Go.
The idea behind this pattern is to open a new database transaction at the very beginning of the HTTP request and commit or rollback it before the response is sent back to client. By doing so, we can avoid having to open/close transactions explicitly for each batch of database operations and just use the already created transaction.
The following code is an example of the traditional and most common way of handling a database transaction, not only in Go, but any language.
func index(w http.ResponseWriter, r *http.Request) {
tx, err := db.Begin()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Printf(err)
return
}
var id int
err = tx.QueryRow("SELECT id FROM pages WHERE url = $1", r.URL.Path).Scan(&id)
if err != nil && err != sql.ErrNoRows {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
if id == 0 {
err := tx.QueryRow("INSERT INTO pages (url, visitors) VALUES ($1, 0) RETURNING id", r.URL.Path).Scan(&id)
if err != nil {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
}
_, err = tx.Exec("UPDATE pages SET visitors = visitors + 1 WHERE id = $1", id)
if err != nil {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
_, err = tx.Exec("INSERT INTO page_visitors (page_id, ip, datetime) VALUES ($1, $2, $3)", id, r.RemoteAddr, time.Now())
if err != nil {
tx.Rollback()
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
err = tx.Commit()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
w.Write([]byte(fmt.Sprintf("Thanks for visiting '%s'", r.URL.Path)))
return
}
What it does is basically open a new transaction and run a couple of commands. If any error occurs, at any time, the transaction is rolled back and a 500 response is returned. If everything goes well, transaction is committed and a 200 response is returned with a simple message.
On this example we’ve basically mixed transaction management with a couple of business commands. It’s also a bit bigger because of all the error handling statements. This is a very common requirement for proper database consistency management, right? Now imagine how messy this would become if we need to do the same in multiple places.
Let’s have a look on how we can make this better.
As I wrote before, the pattern consists of moving the transaction management code to another layer that can execute code before and after all the handlers. Go – and many other languages – can achieve this by using HTTP middlewares.
We’ll create a middleware that opens a transaction before any handler is executed and it’ll commit or rollback the transaction after handler finishes. By doing so, we can remove any transaction specific code from our handler.
The following code implements the same feature as code above, but using middlewares and Session per Request pattern.
type CustomHandler func(http.ResponseWriter, *http.Request) error
type contextKey int
const (
txContextKey contextKey = iota
)
func transaction(next CustomHandler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tx, err := db.Begin()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Printf("Open transaction failed: %s \n", err.Error())
return
}
r = r.WithContext(context.WithValue(r.Context(), txContextKey, tx))
defer func() {
if r := recover(); r != nil {
var err error
switch r := r.(type) {
case error:
err = r
default:
err = fmt.Errorf("%v", r)
}
w.WriteHeader(http.StatusInternalServerError)
fmt.Printf("Transaction is being rolled back: %s \n", err.Error())
tx.Rollback()
return
}
}()
err = next(w, r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Printf("Transaction is being rolled back: %s \n", err.Error())
tx.Rollback()
return
}
err = tx.Commit()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Printf("Transaction commit failed: %s \n", err.Error())
} else {
fmt.Println("Transaction has been committed")
}
})
}
The middleware creates a new context with recently created transaction and send it down to the pipeline. Once the execution comes back to the middleware, the transaction is either committed or rolled back.
func inder(w http.ResponseWriter, r *http.Request) error {
tx := r.Context().Value(txContextKey).(*sql.Tx)
var id int
err := tx.QueryRow("SELECT id FROM pages WHERE url = $1", r.URL.Path).Scan(&id)
if err != nil && err != sql.ErrNoRows {
return err
}
if id == 0 {
err := tx.QueryRow("INSERT INTO pages (url, visitors) VALUES ($1, 0) RETURNING id", r.URL.Path).Scan(&id)
if err != nil {
return err
}
}
_, err = tx.Exec("UPDATE pages SET visitors = visitors + 1 WHERE id = $1", id)
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO page_visitors (page_id, ip, datetime) VALUES ($1, $2, $3)", id, r.RemoteAddr, time.Now())
if err != nil {
return err
}
w.Write([]byte(fmt.Sprintf("Thanks for visiting '%s'", r.URL.Path)))
return nil
}
The index
handler code is much smaller and simple now. All it does is get an active transaction from the context and start using it. If something goes wrong, just return an error
to the middleware. The middleware, of course, looks a bit scary at first, but it’s something you write once and just use it.
This pattern also comes with a couple of benefits, such as:
On the other hand, you might want to keep your HTTP requests as fast as possible. It’s never good to have lots of transactions hanging for a long time 😃.
This pattern can be easily implemented in bare Go code (like example above) or any other web framework.
If you’re looking for some inspiration, you can check my current OSS project called Fider where I’m using this pattern and plenty more things.
I’d like hear back from you. What do you think? Is this useful? Did I miss something?
Cheers!