A transaction manager based on sqlx(github.com/jmoiron/sqlx) for Golang. Since it is based on sqlx, the query methods in gotx(GetOne, Select, Insert, etc.) are similar to those in sqlx.
GoTx manages logical transactions binded to a goroutine in a group when the propagation type is set to PropagationRequired
. These logical transactions are then binded to one underlying database transaction. GoTx uses goroutine ID to track logical transactions in a group.
When the propagation type is set to PropagationNew
. Each logical transaction is binded to one underlying database transaction.
In the case of PropagationRequired
, GoTx supports nested logical transactions so please feel free to call other transaction wrapped functions/methods from a transaction function/method in your service layer. With a nested transaction a commit does not persist changes until the top level transaction commit. While a rollback works regardless of the level of the transactions. That is when a rollback is triggered, all changes of nested logical transactions are discarded. When using PropagationNew
every transaction do commit and rollback independently.
NOT READY FOR PRODUCTION USE. PLEASE USE IT AT YOUR OWN RISK.
go get github.com/oligo/gotx
import (
"context"
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/oligo/gotx"
)
// account model as an example here.
// Gotx use sqlx db tag here to do column mapping
type Account struct {
ID int64 `db:"id"`
Name string `db:"name"`
Articles []*Article
}
type Article struct {
ID int64 `db:"id"`
Content string `db:"content"`
}
func main() {
//using a DNS for mysql here
dsn := "root:12345@tcp(127.0.0.1:3306)/db"
db, err := sqlx.Connect("mysql", dsn)
if err != nil {
panic(err)
}
txManager := gotx.NewTxManager(db)
// declares the tx options here. Please note that if options is omitted,
// the default option is used. By default we use PropagationRequired for propagation
// and RepeatableRead for isolation level.
opts := &gotx.Options{
Propagation: gotx.PropagationRequired,
IsolationLevel: sql.LevelRepeatableRead,
}
// use userID=10 for example
userID := 10
var account Account
_ = txManager.Exec(context.Background(), func(tx *gotx.Transaction) error {
// for more query method, please consult the godoc of this project
err := tx.GetOne(&account, "select id, name from account where id=?", userID)
if err != nil {
return err
}
var articles []*Article
// execute a nested transaction
err = txManager.Exec(context.Background(), func(tx *gotx.Transaction) error {
// for more query method, please consult the godoc of this project
err := tx.Select(&articles, "select * from article where user_id=?", userID)
if err != nil {
return err
}
account.Articles = articles
return nil
}, opts)
// Any error returned in this function will trigger the rollback of the transaction.
return err
}, opts)
}
Gotx uses goroutine ID to track nested transactions. This will be a problem when any of the nested transactions is executed in a
seperate goroutine and we demand a PropagationRequired
propagation setting. This may cause db locked when we are using a database
system that does not support nested transaction such as Sqlite3.
Go deliberately choose not to expose goroutine ID and there's not such ThreadLocal
objects. This makes the management of states
bound to the thread/goroutine hard. It is encouraged to use content.Contenxt
to pass state around。In the case of Gotx, it is quite
tricky to track transactions using content.WithValue
. We have to return contenxt from every tx execution and pass it down to nested transaction.
This leads to complicated API design. Gotx chooses to go with the hacking way that leverages the Goroutine identifier internally and this works.