Skip to content

事务

引言

事务是作为一个工作单元执行的一个或多个操作的序列。这允许在事务期间发生失败时进行回滚,从而有效取消所有修改。如果所有操作都成功完成,事务将被提交并应用更改。

通常,事务在数据库级别进行控制,并且强烈依赖于数据库引擎的工作方式。在将业务逻辑与数据层分离的同时管理事务是一个真正的挑战,并且通常会导致复杂的存储库。

为了解决这个问题,服务可以利用会话机制。该系统创建了一个事务系统的抽象(无论是数据库还是其他),因此服务可以定义和控制业务事务,而无需直接与数据库交互。

为了使该系统流畅工作,重要的是所有存储库方法只执行一个单一操作。

会话

session.Session,位于 goyave.dev/goyave/v5/util/session 包中,是一个可以轻松作为依赖项传递给服务的接口。

  • 根会话在服务器初始化时、注册服务之前定义。
  • 服务依赖于根会话。
  • 当服务需要执行事务时,它使用根事务和 context.Context 来创建一个事务。与事务关联的底层数据库实例作为值注入到 context.Context 中。这个子上下文被传递给一个回调函数,事务操作将在其中进行。多个不同的存储库,甚至不同的服务,可以在事务内无缝调用。
  • 存储库使用 session.DB() 从上下文中检索实际的数据库实例。如果上下文中没有数据库实例,存储库将使用其根数据库依赖项作为回退。因此,存储库不知道,也不需要知道它们是否在事务中操作。
  • 如果回调返回错误,会话机制将自动回滚。如果没有,它将自动提交

会话示意图

示例

让我们举一个例子,我们有一个跟踪用户操作(用户历史)的系统,并且我们希望当用户创建其帐户时创建一个“注册”历史条目。

go
// 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 实例。

go
// 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
}
go
// 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)
}
go
// 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。您必须将此上下文用于事务内执行的所有操作。

以下示例是使用手动事务控制实现与先前示例相同逻辑的代码:

go
// 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 来禁用嵌套事务中的保存点使用。

这样,服务就不必担心在另一个依赖服务内部操作潜在的事务。一切都可以作为可以在任何地方使用的单元正常工作。