[英]How to do transaction Management
我找到了很多关于如何处理事务、清理架构的 golang 教程,但没有一个能满足我的需求。 我有一个go web 应用程序,其中包含一系列 rest ZDB974238714CA8DE634A7CE1D0 结构,每一个由 a8 结构提供
这是主要的:
func main() {
DB = db.New()
dao = dao.New(DB)
service := service.New(dao)
server := &http.Server(....)
server.ListenAndServe()
}
现在,我会意识到服务层中的事务管理器。 像这样的东西:
type Service interface {
TransactionalExecute(fn func() error)
}
type ServiceImpl struct {
dao DaoImpl
}
func (service *ServiceImpl) TransactionalExecute(fn func() error) {
service.dao.TxBegin()
err := fn()
service.dao.TxEnd(err)
}
道应该是这样的:
type Dao interface {
TxBegin()
TxEnd(err error)
}
type DaoImpl struct {
DB db
tx *sql.Tx
}
func (dao *DaoImpl) TxBegin() {
dao.tx = dao.db.Begin()
}
func (dao *DaoImpl) TxEnd(err error) {
if p:= recover(); p != nil {
dao.tx.Rollback()
panic(p)
} else if err != nil {
dao.tx.Rollback()
} else {
dao.tx.Commit()
}
}
这种POC有两个问题:
有什么建议吗? 还是解决问题的不同方法?
事务不应该真正成为 DAO 层的一部分,它是一个完全独立的关注点。 事务只是以原子方式执行所有语句的单个连接。 另一方面,DAO 是使用连接/事务的数据库函数的集合。
您可以很好地将事务和 DAO 实现为两个独立的接口。 事务可以是由 sql.DB(标准库中的连接池)和 sql.Tx(事务)实现的接口。
// Transaction represents both database connection pool and a single transaction/connection
type Transaction interface {
// ExecContext from database/sql library
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// PrepareContext from database/sql library
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
// QueryContext from database/sql library
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
// QueryRowContext from database/sql library
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
// QueryxContext from sqlx library
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
// QueryRowxContext from sqlx library
QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row
// PreparexContext from sqlx library
PreparexContext(ctx context.Context, query string) (*sqlx.Stmt, error)
// GetContext from sqlx library
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
// SelectContext from sqlx library
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}
我们同时使用 database/sql 和 sqlx 扩展库来简化数据扫描,如果不需要,可以直接删除 sqlx 函数。 然后是帮助器 function 以便于工作:
// A TxFn is a function that will be called with an initialized `Transaction` object
// that can be used for executing statements and queries against a database.
type TxFn func(Transaction) error
// WithTransaction creates a new transaction and handles rollback/commit based on the
// error object returned by the `TxFn` or when it panics.
func WithTransaction(ctx context.Context, db *sqlx.DB, fn TxFn) error {
logger := ctxval.GetLogger(ctx)
tx, err := db.BeginTxx(ctx, &sql.TxOptions{
Isolation: sql.LevelDefault,
ReadOnly: false,
})
if err != nil {
logger.Error().Err(err).Msg("Cannot begin database transaction")
return fmt.Errorf("cannot begin transaction: %w", err)
}
defer func() {
if p := recover(); p != nil {
logger.Trace().Msg("Rolling database transaction back")
err := tx.Rollback()
if err != nil {
logger.Error().Err(err).Msg("Cannot rollback database transaction")
return
}
panic(p)
} else if err != nil {
logger.Trace().Msg("Rolling database transaction back")
err := tx.Rollback()
if err != nil {
logger.Error().Err(err).Msg("Cannot rollback database transaction")
return
}
} else {
logger.Trace().Msg("Committing database transaction")
err = tx.Commit()
if err != nil {
logger.Error().Err(err).Msg("Cannot rollback database transaction")
return
}
}
}()
logger.Trace().Msg("Starting database transaction")
err = fn(tx)
return err
}
然后你需要一个 DAO 接口,我们有一个作为 function 的初始化器,然后由各个实现初始化,它接受上下文和事务并返回 DAO 实现:
var GetAccountDao func(ctx context.Context, tx Transaction) (AccountDao, error)
type AccountDao interface {
GetById(ctx context.Context, id uint64) (*models.Account, error)
GetByAccountNumber(ctx context.Context, number string) (*models.Account, error)
GetByOrgId(ctx context.Context, orgId string) (*models.Account, error)
List(ctx context.Context, limit, offset uint64) ([]*models.Account, error)
}
实现看起来像这样:
const (
getAccountById = `SELECT * FROM accounts WHERE id = $1 LIMIT 1`
)
type accountDaoSqlx struct {
name string
getById *sqlx.Stmt
}
func getAccountDao(ctx context.Context, tx dao.Transaction) (dao.AccountDao, error) {
var err error
daoImpl := accountDaoSqlx{}
daoImpl.name = "account"
daoImpl.getById, err = tx.PreparexContext(ctx, getAccountById)
if err != nil {
return nil, NewPrepareStatementError(ctx, &daoImpl, getAccountById, err)
}
return &daoImpl, nil
}
func init() {
dao.GetAccountDao = getAccountDao
}
func (dao *accountDaoSqlx) GetById(ctx context.Context, id uint64) (*models.Account, error) {
query := getAccountById
stmt := dao.getById
result := &models.Account{}
err := stmt.GetContext(ctx, result, id)
if err != nil {
return nil, NewGetError(ctx, dao, query, err)
}
return result, nil
}
// etc
并在“服务”package 中使用示例(没有和有事务):
// import sqlx implementation which calls init()
_ "internal/dao/sqlx"
"internal/dao"
// etc
func CreatePubkey(w http.ResponseWriter, r *http.Request) {
payload := &payloads.PubkeyRequest{}
if err := render.Bind(r, payload); err != nil {
renderError(w, r, payloads.NewInvalidRequestError(r.Context(), err))
return
}
// example with transaction
err := dao.WithTransaction(r.Context(), db.DB, func(tx dao.Transaction) error {
pkDao, err := dao.GetPubkeyDao(r.Context(), tx)
if err != nil {
return payloads.NewInitializeDAOError(r.Context(), "pubkey DAO", err)
}
err = pkDao.Create(r.Context(), payload.Pubkey)
if err != nil {
return payloads.NewDAOError(r.Context(), "create pubkey", err)
}
// ... cut ...
return nil
})
// ignore errors etc
}
func ListPubkeys(w http.ResponseWriter, r *http.Request) {
// example without transaction
pubkeyDao, err := dao.GetPubkeyDao(r.Context(), db.DB)
if err != nil {
renderError(w, r, payloads.NewInitializeDAOError(r.Context(), "pubkey DAO", err))
return
}
pubkeys, err := pubkeyDao.List(r.Context(), 100, 0)
if err != nil {
renderError(w, r, payloads.NewDAOError(r.Context(), "list pubkeys", err))
return
}
if err := render.RenderList(w, r, payloads.NewPubkeyListResponse(pubkeys)); err != nil {
renderError(w, r, payloads.NewRenderError(r.Context(), "list pubkeys", err))
return
}
}
这是我删除细节的不完整代码。 注意我在我们的 dao package 中有这两个接口,它们可能是单独的或相同的 package。
一件重要的事情要知道:你不能像那样共享 DAO 实现(就像在你的代码片段中一样)。 标准库 sql.DB 接口实际上是一个连接池,它是线程安全的(如文档所述),您可以根据需要调用任意数量的 Query/Exec/Etc function,驱动程序将根据需要使用一个或多个连接。
因此正确的做法是,每次有新请求进来时,创建新的 DAO 类型,连接池会自动获取现有的或新的连接。 Go 文档中有关此的更多信息。
您可以将 package dao 修改为如下所示,这将使其成为线程安全的,因为func NewDao
将在处理程序中调用。 您完成这项工作需要将db *sql.DB
从 package 传递给处理程序。
type Dao interface {
TxEnd(err error)
}
type DaoImpl struct {
tx *sql.Tx
db *sql.DB
txStared bool
}
func NewDao(db *sql.DB, txStared bool) Dao {
if txStared {
tx, _ := db.Begin()
return &DaoImpl{tx: tx, db: db, txStared: txStared}
}
return &DaoImpl{tx: nil, db: db, txStared: txStared}
}
func (dao *DaoImpl) TxEnd(err error) {
if !dao.txStared {
return
}
if p:= recover(); p != nil {
dao.tx.Rollback()
panic(p)
} else if err != nil {
dao.tx.Rollback()
} else {
dao.tx.Commit()
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.