Model对象也可以通过TX事务接口创建,通过事务对象创建的Model对象与通过DB数据库对象创建的Model对象功能是一样的,只不过前者的所有操作都是基于事务,而当事务提交或者回滚后,对应的Model对象不能被继续使用,否则会返回错误。因为该TX接口不能被继续使用,一个事务对象仅对应于一个事务流程,Commit/Rollback后即结束。

本章节仅对链式操作涉及到的事务处理方法做简单介绍,更详细的介绍请参考 ORM事务处理 章节。

示例1,通过Transaction

为方便事务操作,gdb提供了事务的闭包操作,通过Transaction方法实现,该方法定义如下:

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

当给定的闭包方法返回的errornil时,那么闭包执行结束后当前事务自动执行Commit提交操作;否则自动执行Rollback回滚操作。

如果闭包内部操作产生panic中断,该事务也将进行回滚。

func Register() error {
	return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
		var (
			result sql.Result
			err    error
		)
		// 写入用户基础数据
		result, err = tx.Table("user").Insert(g.Map{
			"name":  "john",
			"score": 100,
			//...
		})
		if err != nil {
			return err
		}
		// 写入用户详情数据,需要用到上一次写入得到的用户uid
		result, err = tx.Table("user_detail").Insert(g.Map{
			"uid":   result.LastInsertId(),
			"phone": "18010576258",
			//...
		})
		return err
	})
}

示例2,通过TX链式操作

我们也可以在链式操作中通过TX方法切换绑定的事务对象。多次链式操作可以绑定同一个事务对象,在该事务对象中执行对应的链式操作。

func Register() error {
	var (
		uid int64
		err error
	)
	tx, err := g.DB().Begin()
	if err != nil {
		return err
	}
	// 方法退出时检验返回值,
	// 如果结果成功则执行tx.Commit()提交,
	// 否则执行tx.Rollback()回滚操作。
	defer func() {
		if err != nil {
			tx.Rollback()
		} else {
			tx.Commit()
		}
	}()
	// 写入用户基础数据
	uid, err = AddUserInfo(tx, g.Map{
		"name":  "john",
		"score": 100,
		//...
	})
	if err != nil {
		return err
	}
	// 写入用户详情数据,需要用到上一次写入得到的用户uid
	err = AddUserDetail(tx, g.Map{
		"uid":   uid,
		"phone": "18010576259",
		//...
	})
	return err
}

func AddUserInfo(tx gdb.TX, data g.Map) (int64, error) {
	result, err := g.Model("user").TX(tx).Data(data).Insert()
	if err != nil {
		return 0, err
	}
	uid, err := result.LastInsertId()
	if err != nil {
		return 0, err
	}
	return uid, nil
}

func AddUserDetail(tx gdb.TX, data g.Map) error {
	_, err := g.Model("user_detail").TX(tx).Data(data).Insert()
	return err
}







Content Menu

  • No labels

16 Comments

  1. kim

    orm中如果对两个表进行了操作,每次操作都是单独声明一个model出来的,那么就是两个独立的事务,如何让后一个事务加入前一个事务中进行统一管理事务呢?

    1. 你好,更新了一下文档,麻烦再看看呢。

      1. kim

        通过显式的tx对象来调用确实可以自己手动控制两张表的事务一致性。然而实际开发中代码需要松耦合,可能很多表的操作都是各自独立一个接口内部完成,而且这样写声明式事务的方式对开发人员来说要求比较高,事务一致性、回滚、提交都要开发人员一手一脚来顾及,容易踩坑,建议最好是能够通过一些配置或者方法参数的传递能够在底层智能地处理完事务问题。

        1. 那你需要的就是Transaction方法,具体看示例1。

          1. kim

            实例1不需要手动提交和回滚,会不会导致事务挂起?

            1. 当给定的闭包方法返回的errornil时,那么闭包执行结束后当前事务自动执行Commit提交操作;否则自动执行Rollback回滚操作。

              1. kim

                明白,谢谢

  2. 您好,我遇到了一个问题。

    当我的`ctxStrict`值设置为true的时候,不管是使用g.DB().Ctx(ctx).Transaction(ctx, func)还是dao.User.Transaction(ctx, func)都会报错

    context is required for database operation, did you missing call function Ctx

    1. v2新版本已经去掉ctxStrict功能,后续依靠工程化规范和开发工具去保证,不在组件中打桩判断。

  3. 郭强 transaction 闭包方法看起来返回值只能是error,我这边的逻辑是期望类似于 user save之后返回user entity数据,代码如下:

    func (s *sUser) CreateUser(ctx context.Context, input *model.UserCreateInput) (err error) {
       return dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
          var user *entity.User
          if err := gconv.Struct(input, &user); err != nil {
             return err
          }
          if err := s.CheckLoginNameUnique(ctx, user.LoginName); err != nil {
             return err
          }
          user.Password = s.EncryptPassword(user.LoginName, user.Password)
          user.Uuid = guid.S()
          _, err := dao.User.Ctx(ctx).Data(user).OmitEmpty().Save()
          return err
       })
    }

    transaction 闭包方法看起来返回值只能是error,而不能返回user的数据。我理解这里的事务除了具有多个操作原子化目的之外,应该还有并发安全的考虑吧。我改成了TX 链式操作,如下:

    func (s *sUser) CreateUser(ctx context.Context, input *model.UserCreateInput) (userUuid string, err error) {
    	tx, err := g.DB().Begin(ctx)
    	if err != nil {
    		return "", err
    	}
    	// 方法退出时检验返回值,
    	// 如果结果成功则执行tx.Commit()提交,
    	// 否则执行tx.Rollback()回滚操作。
    	defer func() {
    		if err != nil {
    			_ = tx.Rollback()
    		} else {
    			_ = tx.Commit()
    		}
    	}()
    
    	var user *entity.User
    	if err := gconv.Struct(input, &user); err != nil {
    		return "", err
    	}
    	if err := s.CheckLoginNameUnique(ctx, user.LoginName); err != nil {
    		return "", err
    	}
    	user.Password = s.EncryptPassword(user.LoginName, user.Password)
    	user.Uuid = guid.S()
    	_, err = dao.User.Ctx(ctx).Data(user).OmitEmpty().Save()
    	return user.Uuid, err
    }

    想问下:TX 链式操作的话,原子化应该依然有,但是并发安全还能保证么?

    1. 在闭包里面你可以设置方法的返回值哈。

      如果你实在想用Begin/Commit/Rollback操作,链式操作有个链式安全的介绍,如果你使用的是DAO,那么无论如何都是并发安全的。

      1. 郭强 

        我的目的是想使用 transaction 闭包,并且可以返回除了error之外的数据,比如返回user.Uuid。

        func (dao *UserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx *gdb.TX) error) (err error)

        但是看 方法定义,是只能返回err ?


        我用了dao: dao.User.Ctx(ctx).Data(user).OmitEmpty().Save(). db 操作是并发安全的,这个没有问题,我担心的是我的多个操作是否可以一起 并发安全?

        s.CheckLoginNameUnique()
        s.EncryptPassword()
        dao.User.Ctx(ctx).Data(user).OmitEmpty().Save()

        还是说需要我增加 锁 来控制?

    2. user 定义到闭包外面是否可行呢

      func (s *sUser) CreateUser(ctx context.Context, input *model.UserCreateInput) (user *entity.User, err error) {
         return dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
            if err := gconv.Struct(input, &user); err != nil {
               return err
            }
            if err := s.CheckLoginNameUnique(ctx, user.LoginName); err != nil {
               return err
            }
            user.Password = s.EncryptPassword(user.LoginName, user.Password)
            user.Uuid = guid.S()
            _, err := dao.User.Ctx(ctx).Data(user).OmitEmpty().Save()
            return err
         })
      }
      1. 赞! 可行。 陷入误区了~ 

        func (s *sUser) CreateUser(ctx context.Context, input *model.UserCreateInput) (userUuid string, err error) {
        	var user *entity.User
        	err = dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
        		if err := gconv.Struct(input, &user); err != nil {
        			return err
        		}
        		if err := s.CheckLoginNameUnique(ctx, user.LoginName); err != nil {
        			return err
        		}
        		user.Password = s.EncryptPassword(user.LoginName, user.Password)
        		user.Uuid = guid.S()
        		_, err := dao.User.Ctx(ctx).Data(user).OmitEmpty().Save()
        		return err
        	})
        	return user.Uuid, err
        }
  4. 如果在后面遇到报错了,他不会回滚吧?

    我问了gtp,他是这样回答的. 你这写法是不是有点问题  

  5. tx.Ctx(ctx).Model(dao.UserWallets.Table())与
    tx.Model(dao.UserWallets)有啥区别,后面这种写法感觉更简便,但是这样写的话会不会事务不起作用?