如何在 Go Kratos 框架中使用 gorm 事务 ?
准备工作
1 2 3 4 5 6 7 kratos new helloworld cd helloworld# 拉取项目依赖 go mod download# 项目中的 config 等请自行修改
添加事务 如果您还不了解 Kratos 、 mysql 事务 和 GORM 的话请先了解一下。
其实最简单也最直接的方法就是在 data 层的具体操作数据库的方法中使用事务,比如,用户消费的业务,用户消费成功之后,修改用户积分表的数据(这里假设你没有消息队列之类的中间件),那么用户消费表和用户积分表两个表都要添加数据的时候,用户的消费记录更改与积分增加必须在一个事务里面。 但是如果加入到 data 层的具体每个方法的话,当上层(biz)有个业务是只消费不增加积分呢?这时候就用不到积分表了。这时你又要增加一个独立的只操作用户消费表的方法。随着业务增多,你会发现你写了大量的重复的方法。
想要优雅的使用事务,发现 data 层不太好,我们决定在 Usecase 来解决 data 层写了很多重复的方法这个问题。这时候我们在 biz 层提供一个事务接口,然后 usecase 进行调用, data 层的方法,只需要判断一下 DB 实例是不是事务实例,保证是事务执行的就执行事务,不是事务执行的就正常执行。
废话少说写代码 集成 gorm 事务
在这里定义 repo 事务接口,具体的业务逻辑是否会使用事务,使用依赖倒置的原则,
1 2 3 4 5 6 7 8 9 package biz ... # 新增事务接口方法type Transaction interface { ExecTx(context.Context, func (ctx context.Context) error ) error }
引入 gorm,实现 biz 的 repo 事务接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 package dataimport ( "context" "github.com/go-kratos/kratos/v2/log" "github.com/google/wire" "gorm.io/driver/mysql" "gorm.io/gorm" "helloworld/internal/biz" "helloworld/internal/conf" )var ProviderSet = wire.NewSet(NewData, NewDB, NewTransaction, NewConsumeRepo, NewCredRepo)type Data struct { db *gorm.DB log *log.Helper }type contextTxKey struct {}func NewTransaction (d *Data) biz .Transaction { return d }func (d *Data) ExecTx (ctx context.Context, fn func (ctx context.Context) error ) error { return d.db.WithContext(ctx).Transaction(func (tx *gorm.DB) error { ctx = context.WithValue(ctx, contextTxKey{}, tx) return fn(ctx) }) }func (d *Data) DB (ctx context.Context) *gorm .DB { tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB) if ok { return tx } return d.db }func NewData (db *gorm.DB, logger log.Logger) (*Data, func () , error ) { l := log.NewHelper(log.With(logger, "module" , "transaction/data" )) d := &Data{ db: db, log: l, } return d, func () { }, nil }func NewDB (conf *conf.Data, logger log.Logger) *gorm .DB { log := log.NewHelper(log.With(logger, "module" , "order-service/data/gorm" )) db, err := gorm.Open(mysql.Open(conf.Database.Source), &gorm.Config{}) if err != nil { log.Fatalf("failed opening connection to mysql: %v" , err) } if err := db.AutoMigrate(&Consume{}, &Cred{}); err != nil { log.Fatal(err) } return db }
修改 internal/service/greeter.go
刚才初始化项目之后,默认提供了一个 SayHello 方法,咱们就假设这个 GET 请求的方法是要用到事务的业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...func (s *GreeterService) SayHello (ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) { consumeID, err := s.uc.Consume(ctx, &biz.Consume{ UserID: 1 , OrderID: "202202251234567890" , OrderPrice: 500 , }) if err != nil { return nil , err } return &v1.HelloReply{Message: "消费记录生成" + strconv.FormatInt(consumeID, 10 )}, nil }
修改 internal/biz/greeter.go
编写业务方法 Consume,可以看到 Consume 方法使用了 repo 定义的 ExecTx 开启事务方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package bizimport ( "context" "github.com/go-kratos/kratos/v2/log" )type Consume struct { ID int64 UserID int64 OrderID string OrderPrice int64 }type Cred struct { ID int64 UserID int64 Source int64 Integral int64 }type ConsumeRepo interface { CreateConsume(ctx context.Context, a *Consume) (int64 , error) }type CredRepo interface { CreateCred(ctx context.Context, cred *Cred) (int64 , error) }type GreeterUsecase struct { consumeRepo ConsumeRepo cardRepo CredRepo tx Transaction log *log.Helper }func NewGreeterUsecase (repo ConsumeRepo, cardRepo CredRepo, tx Transaction, logger log.Logger) *GreeterUsecase { return &GreeterUsecase{ consumeRepo: repo, cardRepo: cardRepo, tx: tx, log: log.NewHelper(logger), } }func (uc *GreeterUsecase) Consume (ctx context.Context, c *Consume) (int64 , error) { var ( err error id int64 ) err = uc.tx.ExecTx(ctx, func (ctx context.Context) error { id, err = uc.consumeRepo.CreateConsume(ctx, c) if err != nil { return err } _, err = uc.cardRepo.CreateCred(ctx, &Cred{ UserID: c.UserID, Source: id, Integral: c.OrderPrice, }) if err != nil { return err } return nil }) if err != nil { return 0 , err } return id, nil }
修改 internal/data/greeter.go
实现 repo 接口定义的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package dataimport ( "context" "github.com/go-kratos/kratos/v2/log" "helloworld/internal/biz" )type consumeRepo struct { data *Data log *log.Helper }type credRepo struct { data *Data log *log.Helper }type Consume struct { ID int64 UserID int64 OrderID string OrderPrice int64 }type Cred struct { ID int64 UserID int64 Source int64 Integral int64 }func NewConsumeRepo (data *Data, logger log.Logger) biz .ConsumeRepo { return &consumeRepo{ data: data, log: log.NewHelper(logger), } }func NewCredRepo (data *Data, logger log.Logger) biz .CredRepo { return &credRepo{ data: data, log: log.NewHelper(logger), } }func (c *consumeRepo) CreateConsume (ctx context.Context, a *biz.Consume) (int64 , error) { consume := Consume{ UserID: a.UserID, OrderID: a.OrderID, OrderPrice: a.OrderPrice, } result := c.data.DB(ctx).Create(&consume) return consume.ID, result.Error }func (c *credRepo) CreateCred (ctx context.Context, a *biz.Cred) (int64 , error) { cred := Cred{ UserID: a.UserID, Source: a.Source, Integral: a.Integral, } result := c.data.DB(ctx).Create(&cred) return cred.ID, result.Error }
测试
1 2 3 4 5 6 7 cd cmd/helloworld wirecd ../../ kratos run
1 2 3 4 5 6 curl 'http://127.0.0.1:8000 /helloworld/kratos' 输出: { "message" : "消费记录生成1" }
结束 修改 data/greeter.go
中的 CreateConsume 或者 CreateCred 方法,return 一个错误,重启服务,再次访问地址,回到数据库查看,你会发现库中还是之前生成的一条记录,新的记录并未纪录到库中,证明事务集成成功。
感谢您的耐心阅读,动动手指点个赞吧。