基本介绍

使用GoFrame ORM组件进行事务操作非常简便、安全,可以通过两种操作方式来实现。

  1. 常规操作:通过Begin开启事务之后会返回一个事务操作接口gdb.TX,随后可以使用该接口进行如之前章节介绍的方法操作和链式操作。常规操作容易漏掉关闭事务,有一定的事务操作安全风险。
  2. 闭包操作:通过Transaction闭包方法的形式来操作事务,所有的事务逻辑在闭包中实现,闭包结束后自动关闭事务保障事务操作安全。并且闭包操作支持非常便捷的嵌套事务,嵌套事务在业务操作中透明无感知。

我们推荐事务操作均统一采用Transaction闭包方式实现。

接口文档: https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#TX

相关文档




Content Menu

  • No labels

16 Comments

  1. 不管事务是 commit 或者 rollback,这个事务都结束了;所以获取到的事务连接 tx  不要想着保存了供后续使用

  2. 现在ORM生成的DAO,似乎无法自动完成事务嵌套。比如,最早我之前实现了一个 aService.GetInfoByID,这里面只有一个FindOne操作。然而,我需要新设计一个UpdateInfo,逻辑为更新后,再查询。而查询是调用了GetInfoByID方法。即,其实大概逻辑应该是:

    begin();

    update....

    GetInfoByID()

    ...

    commit;

    当出现这类嵌套需求时,对于GetInfoByID,它其实并不知道外层会有事务调用。所以在GetInfoByID内部,其实并不会受到事务影响。这样就出现一个问题,我用GetInfoByID获取到的数据,并不是事务内更新过的数据。这样就引起了一个BUG。但是,若我想事务作用于GetInfoByID内部,那就必须传递tx进去。


    还有一种情况,存在一个Update1()的方法,其内部使用了事务。这时又需要编写一个Update2(),在Update1()的基础上,再额外增加一些其它更新操作。保证Update2的操作是事务的。但由于Update1内部已经有一个事务。那么,在Update2中开了事务后,两个嵌套事务能否一起提交与回滚?


    对于这些情况。GF有无考虑?若有考虑,那应该如何处理?

    1. 你好啊,以下是我的个人看法,欢迎交流讨论。

      1. 事务操作往往是和特定的业务场景绑定的,因此特定业务逻辑的封装方法中增加tx对象的输入是合理的。
      2. 建议通过Transaction闭包的方式来管理事务,内部会自动执行Commit或者Rollback方法。
      3. 如果是多个数据库的多个事务,涉及到分布式事务,属于架构设计,不是ORM组件能够解决的。
      1. 若是每次使用事务,都必须如此操作。那么很多业务级的方法就很难去封装及维护。经常存在某个业务逻辑,是由更小的多个业务逻辑组合而成。而为了原子性,则需要在这个业务中启用事务。

        但是,现在问题来了。若更底层的逻辑不是由当前调用者维护的,这些方法的内部处理对外则是透明的。若我想实现所有底层的业务处理完毕,再统一提交事务,这样就很困难了。因为现在没办法在更高层逻辑中开启事务。

        同时,就算团队约定好每个方法必须支持事务传入,但是,这样的代码量也会增加非常多。并不利于快速开发。

        这个问题,我也一直在考虑如何在业务层能解决,但是发现,还是比较困难。可能还是需要调整ORM底层,或更换框架的使用方式(比如放弃sevice, dao的单例使用)。

        现在我是自己封装了一个 Transaction,通过获取堆栈信息来识别当前的协程ID。然后完成的嵌套处理。不过,这样依然有缺陷,主要问题还是,若子方法中的Transaction未覆盖所有数据库操作,那么这些操作将不会被加入上一级的事务操作中。比如我们的一个子逻辑为“先查询,再事务更新”。在父逻辑中,是会执行 事务更新数据后再调用这个子逻辑(事务中调用)。这样子逻辑中由于查询未在事务中,则会导致查出来的数据出现不一致,引起逻辑BUG。思来想去,还是只有ORM底层改造能解决这个问题。


        1. 大概明白你的意思了,你这个痛点确实只有从orm入手才能解决。

          这个时候service的对象不能使用dao的数据对象了,而是应当动态生成service对象,service依赖的dao对象应当支持依赖注入,这样才能满足你的要求。不过如何优雅的设计可能会比较麻烦,你可以提个issue,我后续考虑下这个改进,你也可以直接在goframe的个人空间上写个wiki文章描述你的设计,我们一起讨论。

            1. 已改进,v1.16新版不再需要显示传递tx参数,但是需要层级传递ctx上下文变量。

  3. 事务嵌套确实有必要支持,项目维护的人稍微一多,事务嵌套就避免不了,要是不支持事务嵌套,会带来很大麻烦,可以参考一下 laravel 是怎么处理事务嵌套的

  4. 事务嵌套,是在子事务开启时设置还原点,需保证是同一个连接

     SAVEPOINT  name1

    在子嵌套事务中回滚,就是回滚到还原点
    ROLLBACK TO SAVEPOINT name1

    commit提交时全部提交

    1. 已支持嵌套事务。

  5. func (db DB) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error)

    从 v1.15 升级到 v1.16,发现 Transaction 多了 context.Context 参数。


    问:实践当中这个 ctx 哪里来?

    从 controller 层的 *ghttp.Request  一路传过来吗?这样的话,整个调用链的参数都要改,成本太大了

    但默认的  context.TODO() 和 context.Background()  又是全局共用的,似乎不太适合http服务(看到有人说即使从它俩继承也不太好)

    求解

    1. 推荐是从顶层即r.Context()返回的ctx层级传递到下层。如果实在不想用ctx,就传递默认的context.TODO()或者context.Background() 。此外,其实GoFrame ORMCtx方法也支持nil,不过Go官方不推荐使用nil传递ctx,程序处理不好可能会在某些场景产生意想不到的问题。做Go项目你会发现将第一个参数设置为ctx参数是一个很好的习惯,项目越到后面你会发现越有必要,特别是针对于微服务项目来说。

  6. 我使用sql server 时,使用orm的事务,发现当sql server 死锁后,sql server 会自动杀掉死锁的进程。然后这时不会回滚。

            m.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
                _, err := tx.Ctx(ctx).Exec(`SET XACT_ABORT ON`)

    这样设置也无效。求解。

  7. 使用 pgsql ,不使用事务可以使用LastInsertId,但在事务中却不能使用LastInsertId,有什么解决办法吗?


    //不使用事务
    id, err := dao.User.Ctx(ctx).Data(entity.User{
       Id:     0,
       Name:   in.Name,
       Age:    in.Age,
       Gender: in.Gender,
    }).OmitEmpty().InsertAndGetId()
    fmt.Println("LastInsertId", id) //此时输出正常:LastInsertId xxx

    //使用事务
    g.DB().Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
       id, err := dao.User.Ctx(ctx).TX(tx).Data(entity.User{
          Id:     0,
          Name:   in.Name,
          Age:    in.Age,
          Gender: in.Gender,
       }).OmitEmpty().InsertAndGetId() //此时输出:LastInsertId is not supported by this driver
  8. 数据库配置成sqlite,事务调用就会卡住。

    2023-06-03 18:45:35.227 [DEBU] {bcc5410bb0d977171748805318db79c4} [  0 ms] [default] [resource/data/111.sqlite3] [rows:0  ] [txid:1] BEGIN
    
    2023-06-03 18:45:35.228 [DEBU] {bcc5410bb0d977171748805318db79c4} [  0 ms] [default] [resource/data/111.sqlite3] [rows:1  ] [txid:1] UPDATE `sys_role` SET `status`=1,`list_order`=0,`name`='超级管理员',`remark`='备注',`updated_at`='2023-06-03 18:45:35' WHERE `id`=1

    数据库配置如下

      default:
        link: "sqlite::@file(resource/data/111.sqlite3)"
        debug:  true
        charset: "utf8mb4" #数据库编码
        dryRun: false #空跑
        maxIdle: 1 #连接池最大闲置的连接数
        maxOpen: 1 #连接池最大打开的连接数
        maxLifetime: "30s" #(单位秒)连接对象可重复使用的时间长度


    代码

    // EditRole 修改角色
    func (s *sSysRole) EditRole(ctx context.Context, req *system.RoleEditReq) (err error) {
    	err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
    		err = g.Try(ctx, func(ctx context.Context) {
    			_, e := dao.SysRole.Ctx(ctx).TX(tx).WherePri(req.Id).Data(&do.SysRole{
    				Status:    req.Status,
    				ListOrder: req.ListOrder,
    				Name:      req.Name,
    				Remark:    req.Remark,
    			}).Update()
    			liberr.ErrIsNil(ctx, e, "修改角色失败")
    			//删除角色权限
    			e = s.DelRoleRule(ctx, req.Id)
    			liberr.ErrIsNil(ctx, e)
    			//添加角色权限
    			e = s.AddRoleRule(ctx, req.MenuIds, req.Id)
    			liberr.ErrIsNil(ctx, e)
    			//清除缓存
    			commonService.Cache().Remove(ctx, consts.CacheSysRole)
    		})
    		return err
    	})
    	return
    }