网站运营与管理期末考试,看动漫是怎么做视频网站,网盟推广平台,建设银行六安分行网站在写代码过程中遇到了需要使用gorm执行sql事务的情况#xff0c;研究了一下各位大佬的实现方案#xff0c;结合了自身遇到的问题#xff0c;特此记录。 代码架构介绍
.
├── apis
├── config
├── internal
│ ├── constant
│ ├── controller
│ ├──…在写代码过程中遇到了需要使用gorm执行sql事务的情况研究了一下各位大佬的实现方案结合了自身遇到的问题特此记录。 代码架构介绍
.
├── apis
├── config
├── internal
│ ├── constant
│ ├── controller
│ ├── logic
│ ├── model
│ ├── repo
│ ├── router
│ └── service
├── library
├── pkg
├── scripts目录结构其实很简单主要参考了goframe的架构。internal/logic放逻辑代码internal/repo是dao层负责与数据库进行交互。 演变过程
最开始的时候是直接把对事务操作放在repo层中了。比如说有两张表一张用户消费记录表一张用户积分表在用户消费完成后就要增加对应的积分。那么在repo层就会有一个函数负责开启事务写消费记录表写积分表提交事务。在logic层中只需要调用这个函数就可以同时完成这两个操作。 看起来很美好逻辑分的很清晰但是这么做其实有个问题。如果logic层中有逻辑只操作消费记录表呢或者用户消费积分只操作积分表呢或者又有其他的表需要进行联合操作。这种写法会在repo层不断增加一个又一个函数导致函数越来越多因为承载了过多的业务逻辑。 发现这种情况之后我们想可不可以把业务逻辑从repo层抽离放在logic层进行处理repo层只做简单的增删改查。那么repo层就可以只实现几个基础的函数Create(),Update(),Selete()等然后在logic层通过调用这几个函数实现复杂的逻辑操作。还是之前那个例子比如说先消费再增加积分就可以变成对两个函数调用。 // logic 层func buy() {tx : db.begin()cosume.Create(tx)point.Update(tx)tx.commit()}分层是明确了但是又引出了新的问题就是如果我想使用事务那么就需要在logic层对事务进行开启和提交并且logic层还需要保存db的连接用于开启事务并且将tx传给repo层进行操作这不合理。logic应该专注于代码逻辑的处理而不是如何开启一个数据库事务。 目标很明确如何让logic层只关注与逻辑的操作而不是开启事务。上文也说到repo层只做一些简单的增删改查的操作在repo里实现是不可能了那么只能采取终极方案在logic和repo之间增加一层中间层专门用于开启事务。 这个中间层更像一个管理层用于管理具体操作数据库的repo类和事务的操作当需要开启事务时logic只需要编写好业务逻辑然后调用管理层的接口。管理层负责开启事务调用logic层编写好的逻辑提交/回滚事务即可。 这样logic层既不用处理事务相关的任务repo层也不需要处理复杂的业务逻辑。 实现方案
总结一下上面所说我们需要实现一个管理类在管理类里面提供一个事务接口这个接口的入参有一个func这个func就是logic层编写好的各种逻辑为了将tx传递给repo我们是将其放在了ctx中。
管理层示例 Internal/repo/transaction.go 这个代码参考的时 gorm.Transaction()做了简单修改。注释写的很详细了。
// Mysql Mysql数据库
type Mysql struct {db *gorm.DB
}func (m *Mysql) Transaction(ctx context.Context, fc func(txCtx context.Context) error) error {panicked : truevar err error// 拿到 ctx 里面的 db 连接如果没有则使用默认的 db 连接db : m.getDB(ctx)// ...// 使用这个连接开启事务tx : db.WithContext(ctx).Begin()if tx.Error ! nil {return tx.Error}defer func() {// Make sure to rollback when panic, Block error or Commit errorif panicked || err ! nil {tx.Rollback()}}()// 将事务的 tx 写入 ctx 中为了真正操作数据库的函数拿到 txtxCtx : generateContextWithDB(ctx, tx)// 调用 logic 层传入的 funcif err fc(txCtx); err nil {panicked false// 如果没有报错就提交事务return tx.Commit().Error}panicked false// 把事务内的报错转发出去return err
}
调用示例 /internal/logic/manager.go func (m *manager) Buy(ctx context.Context) error {// 调用 Transaction 函数txCtx 则为带有 tx 的 ctxif err : m.mysql.Transaction(ctx, func(txCtx context.Context) {// 这里只需要实现对应的业务即可无需关注事务的开启和提交/回滚逻辑m.mysql.Consume(txCtx).Create()m.mysql.Point(txCtx).Update()}); err ! nil {// do something}return nil
}从 manager 这个函数的角度来看整个事务执行的逻辑为通过 Transaction 第一个参数 ctx 获取要执行事务的 db 连接开启事务后将 tx 封到 context 中然后传入 Transaction 第二个参数 func() 的入参中。通过将 txCtx 传入到不同的 repo 类中来实现各个 repo 使用同一个 tx 进行事务的操作。Transaction 函数保证了中间有任何错误都会进行回滚和无错误的提交操作。
Consume()和Point()做的是解析 txCtx 中的 db 连接然后 new 一个repo 类返回。getDB() 负责返回 ctx 里的 db 接。
func (m *Mysql) Consume(ctx context.Context) ConsumeRepo {return NewConsumeRepo(m.getDB(ctx))
}整体的代码逻辑就是这样如果需要支持多个引擎其实可以考虑抽出一个interface各个引擎实现这个 interface 的接口即可。 gorm 一些写法
多表联查
我们的表设计的不是很规范导致表和表之间基本上没有什么关联外键也没有所以没有办法使用gorm提供的preload采取的是手动join的方式。
func (c *ConsumePoint) SelectForUpdate(ctx context.Context) (*model.ConsumeAndPoint, error) {// 接收数据的结构var record model.ConsumeAndPointif err : c.db.WithContext(ctx).// 主表Table(t_consume).// 返回什么Select(t_consume.*, t_point.f_point).// 连接方式Joins(join t_point on t_consume.f_user_id t_point.f_user_id).// 只查询一条数据First(record).Error; err ! nil {// do something}return record, nil
}select for update
先读后更新的数据竞争且应该将加锁操作放到事务中防止锁被自动释放其实主要就在于 Set(“gorm:query_option”, “FOR UPDATE”) 这一行。如果采用上面多表联查的方式设置了 FOR UPDATE 后两张表的内容都会有X锁直到事务提交。可以用这个特性来搞分布式的竞争更新。
func UpdateUser(db *gorm.DB, id int64) error {tx : db.Begin()defer func() {if r : recover(); r ! nil {tx.Rollback()}}()if err : tx.Error; err ! nil {return err}user : User{}// 锁住指定 id 的 User 记录if err : tx.Set(gorm:query_option, FOR UPDATE).First(user, id).Error; err ! nil {tx.Rollback()return err}// 更新操作...// commit事务释放锁if err : tx.Commit().Error; err ! nil {return err}return nil
}