How To Wrap Sql Transaction In Go With Existing Repository That Doesn't Use Sql.tx?
Solution 1:
Much depends on how your entire architecture is set up. There are many variations and considerations to take into account (I won't go into detail, as there's plenty of documentation out there). But in general a best-practice is if the domain model - and in many cases even the Services - is entirely independent of any implementation choices. It shouldn't be aware of SQL transactions.
For instance, if User is an Aggregate Root in DDD terminology and Picture, Contact and Address are Value Objects, then User is responsible for consistency.
In my own DDD code I use a Ports and Adapters architecture and CQRS. I might have a UpdateUserDetails Command in the Application layer that is passed a Repository implementation (e.g. for a SQL database) and does the transaction handling. The Domain layer would just define the Repository interface.
The command handler retrieves the Aggregate Root object i.e. User from the repository and does the updating on that (again various ways to implement e.g. in a single User.UpdateDetails() or in multiple steps in the handler itself). Only after this is done without any validation or business rules errors, does the handler persist to the repository and here I wrap calls into a Transaction.
My code - being very specific - would not be useful for you, though.
Instead check the great Wild Workouts example for a complete intro of DDD for Go, and including transaction handling. The blog series gradually refactors an application, introducing ever more DDD-related concepts. It starts with a "DDD lite" version (best-practice: start as simple as possible), then adds CQRS and even goes to Event Sourcing and Microservices (where due to 'eventual conistency' transactions would be handled entirely differently).
Specifically the series' article The Repository pattern: a painless way to simplify your Go service logic, offers some insightful ideas on how to handle your transactions.
Solution 2:
instead of declaring a repository such as
type UserRepo struct{
db *sql.DB
}
func(s UserRepo)Create(ctx context.Context, u modelUser) error{ ... }
Prefer a signature such as
type Execer interface {
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}
type Querier interface {
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
}
type UserRepo struct{}
func(s UserRepo)Create(ctx context.Context, db Execer, u modelUser) error{ ... }
It adds two interfaces that abstract the concrete underlying type of the DB.
In consequence, within the caller you can write something similar to
db := GetDB()
tx := db.Begin()
var err errordeferfunc() {
if err == nil {
err = tx.Commit()
} else {
err = tx.Rollback() // or use a []error, or else, to not shadow the underlying error.
}
}()
var dataUser something
dataUser, err = user.repository.Create(ctx, tx, modelUser)
if err != nil {
return err
}
var dataPic something
dataPic, err = pic.repository.Create(ctx, tx, modelUserPic)
if err != nil {
return err
}
// etcWhen you need not to start a transaction, just pass in the db instance,
db:=GetDB()
dataUser, err := user.repository.Create(ctx, db, modelUser)
if err != nil {return err}
dataPic, err := pic.repository.Create(ctx, db, modelUserPic)
if err != nil {return err}
// etcConsider that if you need retryable ops, you should wrap the whole code responsible to begin/end the transaction.
https://golang.org/pkg/database/sql/#Tx
After a call to Commit or Rollback, all operations on the transaction fail with ErrTxDone.
Post a Comment for "How To Wrap Sql Transaction In Go With Existing Repository That Doesn't Use Sql.tx?"