事务
引言
事务是作为一个工作单元执行的一个或多个操作的序列。这允许在事务期间发生失败时进行回滚,从而有效取消所有修改。如果所有操作都成功完成,事务将被提交并应用更改。
通常,事务在数据库级别进行控制,并且强烈依赖于数据库引擎的工作方式。在将业务逻辑与数据层分离的同时管理事务是一个真正的挑战,并且通常会导致复杂的存储库。
为了解决这个问题,服务可以利用会话机制。该系统创建了一个事务系统的抽象(无论是数据库还是其他),因此服务可以定义和控制业务事务,而无需直接与数据库交互。
为了使该系统流畅工作,重要的是所有存储库方法只执行一个单一操作。
会话
session.Session
,位于 goyave.dev/goyave/v5/util/session
包中,是一个可以轻松作为依赖项传递给服务的接口。
- 根会话在服务器初始化时、注册服务之前定义。
- 服务依赖于根会话。
- 当服务需要执行事务时,它使用根事务和
context.Context
来创建一个事务。与事务关联的底层数据库实例作为值注入到context.Context
中。这个子上下文被传递给一个回调函数,事务操作将在其中进行。多个不同的存储库,甚至不同的服务,可以在事务内无缝调用。 - 存储库使用
session.DB()
从上下文中检索实际的数据库实例。如果上下文中没有数据库实例,存储库将使用其根数据库依赖项作为回退。因此,存储库不知道,也不需要知道它们是否在事务中操作。 - 如果回调返回错误,会话机制将自动回滚。如果没有,它将自动提交。
示例
让我们举一个例子,我们有一个跟踪用户操作(用户历史)的系统,并且我们希望当用户创建其帐户时创建一个“注册”历史条目。
// main.go
import (
"database/sql"
"my-project/database/repository"
"my-project/service/user"
"goyave.dev/goyave/v5"
"goyave.dev/goyave/v5/util/session"
)
func registerServices(server *goyave.Server) {
server.Logger.Info("Registering services")
session := session.GORM(server.DB(), &sql.TxOptions{})
userRepository := repository.NewUser(server.DB())
userService := user.NewService(session, userRepository)
server.RegisterService(userService)
}
INFO
这里,我们使用一个抽象了 Gorm 的 Session
实现。存储库将使用 session.DB()
检索 Gorm 实例。
// service/user/user.go
type UserRepository interface {
Create(ctx context.Context, user *model.User) (*model.User, error)
}
type HistoryRepository interface {
Create(ctx context.Context, history *model.History) (*model.History, error)
}
func NewService(session session.Session, userRepository UserRepository, historyRepository HistoryRepository) *Service {
return &Service{
session: session,
userRepository: userRepository,
historyRepository: userRepository,
}
}
func (s *Service) Register(ctx context.Context, user *dto.RegisterUser) (*dto.User, error) {
u := typeutil.Copy(&model.User{}, user)
err := s.session.Transaction(ctx, func(ctx context.Context) error {
var err error
u, err = s.userRepository.Create(ctx, u)
if err != nil {
return errors.New(err)
}
history := &model.History{
UserID: u.ID,
Action: "register",
}
_, err = s.historyRepository.Create(ctx, history)
return errors.New(err)
})
return typeutil.MustConvert[*dto.User](u), err
}
// database/repository/user.go
func (r *User) Create(ctx context.Context, user *model.User) (*model.User, error) {
db := session.DB(ctx, r.DB).Omit(clause.Associations).Create(&user)
return user, errors.New(db.Error)
}
// database/repository/history.go
func (r *History) Create(ctx context.Context, history *model.History) (*model.History, error) {
db := session.DB(ctx, r.DB).Omit(clause.Associations).Create(&history)
return history, errors.New(db.Error)
}
TIP
不需要事务的服务不需要依赖于会话,因为它们只会调用一个单一的存储库操作。
手动事务
Session
接口允许您手动控制事务,而不是使用回调,这得益于 Begin()
、Rollback()
和 Commit()
方法。您将负责自己调用 Commit()
或 Rollback()
!
当您调用 Begin()
时,将创建一个子会话。它将包含子 context.Context
。您必须将此上下文用于事务内执行的所有操作。
以下示例是使用手动事务控制实现与先前示例相同逻辑的代码:
// service/user/user.go
func (s *Service) Register(ctx context.Context, user *dto.RegisterUser) (*dto.User, error) {
u := typeutil.Copy(&model.User{}, user)
tx, err := s.session.Begin(ctx)
if err != nil {
return nil, errors.New(err)
}
u, err = s.repository.Create(tx.Context(), u)
if err != nil {
_ = tx.Rollback()
return nil, errors.New(err)
}
history := &model.History{
UserID: u.ID,
Action: "register",
}
_, err = s.repository.Create(tx.Context(), history)
if err != nil {
_ = tx.Rollback()
return nil, errors.New(err)
}
err = tx.Commit()
return typeutil.MustConvert[*dto.User](u), errors.New(err)
}
嵌套事务
该系统优雅地处理嵌套事务。如果服务使用已经包含事务的上下文启动事务(使用 Transaction()
或手动使用 Begin()
),会话将自动检索此数据库实例,并将其用作创建嵌套事务的起点。
嵌套事务使用保存点(savepoints)工作。如果嵌套事务返回错误,它将回滚到其起点,保持根事务完好无损。仍然建议通过简单地将返回的错误向上传递来回滚根事务。
可以通过将 Gorm 的 DisableNestedTransaction
设置设置为 true
来禁用嵌套事务中的保存点使用。
这样,服务就不必担心在另一个依赖服务内部操作潜在的事务。一切都可以作为可以在任何地方使用的单元正常工作。