From 32d78269f89e7d73f182d8a9953928fc0499bbcf Mon Sep 17 00:00:00 2001 From: han-joker Date: Thu, 27 Apr 2023 14:53:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B3=E8=81=94=E5=92=8C=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- association.go | 397 ++++++++++++++++++++++++++++++++++++++++++++ association_test.go | 39 +++++ models.go | 104 +++++++++++- session.go | 1 - transaction.go | 263 +++++++++++++++++++++++++++++ transaction_test.go | 19 +++ 6 files changed, 814 insertions(+), 9 deletions(-) create mode 100644 association.go create mode 100644 association_test.go create mode 100644 transaction.go create mode 100644 transaction_test.go diff --git a/association.go b/association.go new file mode 100644 index 0000000..f8f8f6a --- /dev/null +++ b/association.go @@ -0,0 +1,397 @@ +package gormExample + +import ( + "fmt" + "gorm.io/gorm/clause" + "log" +) + +func StdAssocModel() { + // 利用migrate创建表 + // 以及多对多的关联表 + // 以及外键约束 + if err := DB.AutoMigrate(&Author{}, &Essay{}, &Tag{}, &EssayMate{}); err != nil { + log.Fatalln(err) + } + // CREATE TABLE `msb_author` ( + // `id` bigint unsigned NOT NULL AUTO_INCREMENT, + // `created_at` datetime(3) DEFAULT NULL, + // `updated_at` datetime(3) DEFAULT NULL, + // `deleted_at` datetime(3) DEFAULT NULL, + // `status` bigint DEFAULT NULL, + // `name` longtext, + // `email` longtext, + // PRIMARY KEY (`id`), + // KEY `idx_msb_author_deleted_at` (`deleted_at`) + //) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + + // CREATE TABLE `msb_essay` ( + // `id` bigint unsigned NOT NULL AUTO_INCREMENT, + // `created_at` datetime(3) DEFAULT NULL, + // `updated_at` datetime(3) DEFAULT NULL, + // `deleted_at` datetime(3) DEFAULT NULL, + // `subject` longtext, + // `content` longtext, + // `author_id` bigint unsigned DEFAULT NULL, + // PRIMARY KEY (`id`), + // KEY `idx_msb_essay_deleted_at` (`deleted_at`), + // KEY `fk_msb_author_essays` (`author_id`), + // CONSTRAINT `fk_msb_author_essays` FOREIGN KEY (`author_id`) REFERENCES `msb_author` (`id`) + //) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + + // CREATE TABLE `msb_essay_mate` ( + // `id` bigint unsigned NOT NULL AUTO_INCREMENT, + // `created_at` datetime(3) DEFAULT NULL, + // `updated_at` datetime(3) DEFAULT NULL, + // `deleted_at` datetime(3) DEFAULT NULL, + // `keyword` longtext, + // `description` longtext, + // `essay_id` bigint unsigned DEFAULT NULL, + // PRIMARY KEY (`id`), + // KEY `idx_msb_essay_mate_deleted_at` (`deleted_at`), + // KEY `fk_msb_essay_essay_mate` (`essay_id`), + // CONSTRAINT `fk_msb_essay_essay_mate` FOREIGN KEY (`essay_id`) REFERENCES `msb_essay` (`id`) + //) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + + // CREATE TABLE `msb_tag` ( + // `id` bigint unsigned NOT NULL AUTO_INCREMENT, + // `created_at` datetime(3) DEFAULT NULL, + // `updated_at` datetime(3) DEFAULT NULL, + // `deleted_at` datetime(3) DEFAULT NULL, + // `title` longtext, + // PRIMARY KEY (`id`), + // KEY `idx_msb_tag_deleted_at` (`deleted_at`) + //) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + + // CREATE TABLE `msb_essay_tag` ( + // `tag_id` bigint unsigned NOT NULL, + // `essay_id` bigint unsigned NOT NULL, + // PRIMARY KEY (`tag_id`,`essay_id`), + // KEY `fk_msb_essay_tag_essay` (`essay_id`), + // CONSTRAINT `fk_msb_essay_tag_essay` FOREIGN KEY (`essay_id`) REFERENCES `msb_essay` (`id`), + // CONSTRAINT `fk_msb_essay_tag_tag` FOREIGN KEY (`tag_id`) REFERENCES `msb_tag` (`id`) + //) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + log.Println("migrate successful") +} + +// 添加关联 +func AssocAppend() { + // A:一对多的关系, Author 1:n Essay + // 创建测试数据 + var a Author + a.Name = "一位作者" + if err := DB.Create(&a).Error; err != nil { + log.Println(err) + } + log.Println("a:", a.ID) + var e1, e2 Essay + e1.Subject = "一篇内容" + //e1.AuthorID = a.ID + e2.Subject = "另一篇内容" + if err := DB.Create([]*Essay{&e1, &e2}).Error; err != nil { + log.Println(err) + } + log.Println("e1, e2: ", e1.ID, e2.ID) + + // 添加关联 + if err := DB.Model(&a).Association("Essays").Append([]Essay{e1}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + // 基于当前的基础上,添加关联 + if err := DB.Model(&a).Association("Essays").Append([]Essay{e2}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + // 添加后,a模型对象的Essays字段,自动包含了关联的Essay模型 + //fmt.Println(a.Essays) + + // B: Essay M:N TAg + var t1, t2, t3 Tag + t1.Title = "Go" + t2.Title = "GORM" + t3.Title = "Ma" + if err := DB.Create([]*Tag{&t1, &t2, &t3}).Error; err != nil { + log.Println(err) + } + log.Println("t1, t2, t3: ", t1.ID, t2.ID, t3.ID) + + // e1 t1, t3 + // e2 t1, t2, t3 + if err := DB.Model(&e1).Association("Tags").Append([]Tag{t1, t3}); err != nil { + log.Println(err) + } + + if err := DB.Model(&e2).Association("Tags").Append([]Tag{t1, t2, t3}); err != nil { + log.Println(err) + } + + // 关联表查看 + // mysql> select * from msb_essay_tag; + //+--------+----------+ + //| tag_id | essay_id | + //+--------+----------+ + //| 1 | 12 | + //| 3 | 12 | + //| 1 | 13 | + //| 2 | 13 | + //| 3 | 13 | + //+--------+----------+ + + // C, Belongs To. Essay N:1 Author + var e3 Essay + e3.Subject = "第三篇内容" + if err := DB.Create([]*Essay{&e3}).Error; err != nil { + log.Println(err) + } + log.Println("e3: ", e3.ID) + + log.Println(e3.Author) + // 关联 + if err := DB.Model(&e3).Association("Author").Append(&a); err != nil { + log.Println(err) + } + log.Println(e3.Author.ID) + + // 对一的关联,会导致关联被更新 + var a2 Author + a2.Name = "另一位作者" + if err := DB.Create(&a2).Error; err != nil { + log.Println(err) + } + log.Println("a2:", a2.ID) + if err := DB.Model(&e3).Association("Author").Append(&a2); err != nil { + log.Println(err) + } + log.Println(e3.Author.ID) + +} + +func AssocReplace() { + // A. 替换 + // 创建测试数据 + var a Author + a.Name = "一位作者" + if err := DB.Create(&a).Error; err != nil { + log.Println(err) + } + log.Println("a:", a.ID) + + var e1, e2, e3 Essay + e1.Subject = "一篇内容" + e2.Subject = "另一篇内容" + e3.Subject = "第三篇内容" + if err := DB.Create([]*Essay{&e1, &e2, &e3}).Error; err != nil { + log.Println(err) + } + log.Println("e1, e2, e3: ", e1.ID, e2.ID, e3.ID) + + // 添加关联 + if err := DB.Model(&a).Association("Essays").Replace([]Essay{e1, e3}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + // 基于当前的基础上,添加关联 + if err := DB.Model(&a).Association("Essays").Replace([]Essay{e2, e3}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + +} + +func AssocDelete() { + // B. 删除,外键的 + // 创建测试数据 + var a Author + a.Name = "一位作者" + if err := DB.Create(&a).Error; err != nil { + log.Println(err) + } + log.Println("a:", a.ID) + + var e1, e2, e3 Essay + e1.Subject = "一篇内容" + e2.Subject = "另一篇内容" + e3.Subject = "第三篇内容" + if err := DB.Create([]*Essay{&e1, &e2, &e3}).Error; err != nil { + log.Println(err) + } + log.Println("e1, e2, e3: ", e1.ID, e2.ID, e3.ID) + + // 添加关联 + if err := DB.Model(&a).Association("Essays").Replace([]Essay{e1, e2, e3}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + + if err := DB.Model(&a).Association("Essays").Delete([]Essay{e1, e3}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + fmt.Println("------------------------") + + // B. 删除,多对多,关联表 + var t1, t2, t3 Tag + t1.Title = "Go" + t2.Title = "GORM" + t3.Title = "Ma" + if err := DB.Create([]*Tag{&t1, &t2, &t3}).Error; err != nil { + log.Println(err) + } + log.Println("t1, t2, t3: ", t1.ID, t2.ID, t3.ID) + // e1 t1, t3 + // e2 t1, t2, t3 + if err := DB.Model(&e1).Association("Tags").Append([]Tag{t1, t2, t3}); err != nil { + log.Println(err) + } + fmt.Println(len(e1.Tags)) + + if err := DB.Model(&e1).Association("Tags").Delete([]Tag{t1, t3}); err != nil { + log.Println(err) + } + fmt.Println(len(e1.Tags)) + + // C. 清空关联 + if err := DB.Model(&e1).Association("Tags").Clear(); err != nil { + log.Println(err) + } + fmt.Println(len(e1.Tags)) +} + +func AssocFind() { + // + e := Essay{} + DB.First(&e, 18) + + // 查询关联的tags + //var ts []Tag + if err := DB.Model(&e).Association("Tags").Find(&e.Tags); err != nil { + log.Println(err) + } + log.Println(e.Tags) + + // 子句,要写在Association()方法前面 + if err := DB.Model(&e). + Where("tag_id > ?", 7). + Order("tag_id DESC"). + Association("Tags").Find(&e.Tags); err != nil { + log.Println(err) + } + log.Println(e.Tags) + + // 查询关联的模型的数量 + count := DB.Model(&e).Association("Tags").Count() + log.Println("count:", count) + +} + +func AssocSave() { + var t1 Tag + DB.First(&t1, 10) + + e := Essay{ + Subject: "一个组合的Save", + Author: Author{Name: "马士兵"}, + Tags: []Tag{ + t1, + {Title: "Ma"}, + {Title: "GORM"}, + }, + } + + if err := DB.Save(&e).Error; err != nil { + log.Println(err) + } + + log.Printf("%+v\n", e) + +} + +// Preload +func AssocPreload() { + // A.直接一步查询Author对应的Essays + a := Author{} + if err := DB. + Preload("Essays"). + First(&a, 1).Error; err != nil { + log.Fatalln(err) + } + // [3.840ms] [rows:2] SELECT * FROM `msb_essay` WHERE `msb_essay`.`author_id` = 1 AND `msb_essay`.`deleted_at` IS NULL + // [13.014ms] [rows:1] SELECT * FROM `msb_author` WHERE `msb_author`.`id` = 1 AND `msb_author`.`deleted_at` IS NULL ORDER BY `msb_author`.`id` LIMIT 1 + log.Println(a.Essays) + log.Println("--------------------") + + // B.支持条件过滤 + if err := DB. + Preload("Essays", "id IN ?", []uint{2, 3, 4}). + First(&a, 1).Error; err != nil { + log.Fatalln(err) + } + // [3.217ms] [rows:1] SELECT * FROM `msb_essay` WHERE `msb_essay`.`author_id` = 1 AND id IN (2,3,4) AND `msb_essay`.`deleted_at` IS NULL + log.Println(a.Essays) + log.Println("-----------------------") + + // C. 支持多次链式调用,同时预加载多个关联 + e := Essay{} + if err := DB. + //Preload("Author"). + //Preload("EssayMate"). + //Preload("Tags"). + Preload(clause.Associations). + First(&e, 1).Error; err != nil { + log.Fatalln(err) + } + log.Println(e) + // [2.776ms] [rows:1] SELECT * FROM `msb_author` WHERE `msb_author`.`id` = 1 AND `msb_author`.`deleted_at` IS NULL + // [10.398ms] [rows:0] SELECT * FROM `msb_essay_mate` WHERE `msb_essay_mate`.`essay_id` = 1 AND `msb_essay_mate`.`deleted_at` IS NULL + // [3.260ms] [rows:2] SELECT * FROM `msb_essay_tag` WHERE `msb_essay_tag`.`essay_id` = 1 + // [3.264ms] [rows:2] SELECT * FROM `msb_tag` WHERE `msb_tag`.`id` IN (1,3) AND `msb_tag`.`deleted_at` IS NULL + // [28.067ms] [rows:1] SELECT * FROM `msb_essay` WHERE `msb_essay`.`id` = 1 AND `msb_essay`.`deleted_at` IS NULL ORDER BY `msb_essay`.`id` LIMIT 1 +} + +// 多级 +func AssocLevelPreload() { + a := Author{} + if err := DB. + //Preload("Essays"). + // 多级关联 + Preload("Essays.Tags"). + First(&a, 1).Error; err != nil { + log.Fatalln(err) + } + // [3.843ms] [rows:5] SELECT * FROM `msb_essay_tag` WHERE `msb_essay_tag`.`essay_id` IN (1,2) + // [3.284ms] [rows:3] SELECT * FROM `msb_tag` WHERE `msb_tag`.`id` IN (1,3,2) AND `msb_tag`.`deleted_at` IS NULL + // [10.396ms] [rows:2] SELECT * FROM `msb_essay` WHERE `msb_essay`.`author_id` = 1 AND `msb_essay`.`deleted_at` IS NULL + // [17.609ms] [rows:1] SELECT * FROM `msb_author` WHERE `msb_author`.`id` = 1 AND `msb_author`.`deleted_at` IS NULL ORDER BY `msb_author`.`id` LIMIT 1 + + log.Println(a.Essays[0].Tags) + log.Println(a.Essays[1].Tags) +} + +func AssocOperate() { + // 创建测试数据 + var a Author + a.Name = "一位作者" + if err := DB.Create(&a).Error; err != nil { + log.Println(err) + } + log.Println("a:", a.ID) + var e1, e2 Essay + e1.Subject = "一篇内容" + //e1.AuthorID = a.ID + e2.Subject = "另一篇内容" + if err := DB.Create([]*Essay{&e1, &e2}).Error; err != nil { + log.Println(err) + } + log.Println("e1, e2: ", e1.ID, e2.ID) + // 基于当前的基础上,添加关联 + if err := DB.Model(&a).Association("Essays").Append([]Essay{e1, e2}); err != nil { + log.Println(err) + } + fmt.Println(len(a.Essays)) + + // 删除a,注意essays的处理 + if err := DB.Unscoped().Delete(&a).Error; err != nil { + log.Fatalln(err) + } +} diff --git a/association_test.go b/association_test.go new file mode 100644 index 0000000..32af837 --- /dev/null +++ b/association_test.go @@ -0,0 +1,39 @@ +package gormExample + +import "testing" + +func TestStdAssocModel(t *testing.T) { + StdAssocModel() +} + +func TestAssocAppend(t *testing.T) { + AssocAppend() +} + +func TestAssocReplace(t *testing.T) { + AssocReplace() +} + +func TestAssocDelete(t *testing.T) { + AssocDelete() +} + +func TestAssocFind(t *testing.T) { + AssocFind() +} + +func TestAssocSave(t *testing.T) { + AssocSave() +} + +func TestAssocPreload(t *testing.T) { + AssocPreload() +} + +func TestAssocLevelPreload(t *testing.T) { + AssocLevelPreload() +} + +func TestAssocOperate(t *testing.T) { + AssocOperate() +} diff --git a/models.go b/models.go index befa51f..d396d09 100644 --- a/models.go +++ b/models.go @@ -24,14 +24,14 @@ type Content struct { AuthorID uint } -// Author模型 -type Author struct { - gorm.Model - Status int - - Name string - Email string -} +//// Author模型 +//type Author struct { +// gorm.Model +// Status int +// +// Name string +// Email string +//} type ContentStrPK struct { ID string `gorm:"primaryKey"` @@ -81,3 +81,91 @@ func (c *Content) AfterFind(db *gorm.DB) error { return nil } + +// 作者模型 +// Author模型 +type Author struct { + gorm.Model + Status int + Name string + Email string + + // 积分 + Points int + + // 拥有多个论文内容 + // has many + + // 默认关联 + Essays []Essay `gorm:"constraint:OnDelete:SET NULL;"` + + // 第一作者论文列表 + FirstEssays []Essay `gorm:"foreignKey:FirstAuthorID;references:;"` + // 第二作者论文列表 + SecondEssays []Essay `gorm:"foreignKey:SecondAuthorID;references:;"` +} + +// CREATE TABLE `msb_essay` ( +// `id` bigint unsigned NOT NULL AUTO_INCREMENT, +// `first_author_id` bigint unsigned DEFAULT NULL, +// `second_author_id` bigint unsigned DEFAULT NULL, +// `author_id` bigint unsigned DEFAULT NULL, +// PRIMARY KEY (`id`), +// KEY `idx_msb_essay_deleted_at` (`deleted_at`), +// KEY `fk_msb_author_essays` (`author_id`), +// KEY `fk_msb_author_first_essays` (`first_author_id`), +// KEY `fk_msb_author_second_essays` (`second_author_id`), +// CONSTRAINT `fk_msb_author_essays` FOREIGN KEY (`author_id`) REFERENCES `msb_author` (`id`), +// CONSTRAINT `fk_msb_author_first_essays` FOREIGN KEY (`first_author_id`) REFERENCES `msb_author` (`id`), +// CONSTRAINT `fk_msb_author_second_essays` FOREIGN KEY (`second_author_id`) REFERENCES `msb_author` (`id`) +//) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci + +// 论文内容 +type Essay struct { + gorm.Model + Subject string + Content string + + // 自定义关联字段 + FirstAuthorID *uint + SecondAuthorID *uint + + FirstAuthor Author `gorm:"foreignKey:FirstAuthorID;references:;"` + SecondAuthor Author `gorm:"foreignKey:SecondAuthorID;references:;"` + + // 外键字段 + AuthorID *uint + // 属于某个作者 + // belongs to + Author Author + + // 拥有一个论文元信息 + // has one + EssayMate EssayMate + + // 拥有多个Tag + // many to many + Tags []Tag `gorm:"many2many:essay_tag"` +} + +// 论文元信息 +type EssayMate struct { + gorm.Model + Keyword string + Description string + + // 外键字段 + EssayID *uint + + // 属于一个论文内容,比较少用 + //Essay *Essay +} + +type Tag struct { + gorm.Model + Title string + + // 拥有多个Essay + // many to many + Essays []Essay `gorm:"many2many:essay_tag"` +} diff --git a/session.go b/session.go index 15f506d..5599f53 100644 --- a/session.go +++ b/session.go @@ -72,7 +72,6 @@ func SessionNew() { db.Where("likes < ?", 5).Find(&cs2) // [3.846ms] [rows:0] SELECT * FROM `msb_content` WHERE views > 100 AND likes < 5 AND `msb_content`.`deleted_at` IS NULL } - func SessionOptions() { // Skip Hook //db := DB.Session(&gorm.Session{ diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..2668882 --- /dev/null +++ b/transaction.go @@ -0,0 +1,263 @@ +package gormExample + +import ( + "errors" + "gorm.io/gorm" + "log" +) + +func TXDemo() { + // 初始化测试数据 + if err := DB.AutoMigrate(&Author{}); err != nil { + log.Fatalln(err) + } + var a1, a2 Author + a1.Name = "库里" + a2.Name = "莫兰特" + a1.Points = 11600 + a2.Points = 200 + if err := DB.Create([]*Author{&a1, &a2}).Error; err != nil { + log.Fatalln(err) + } + + // 事务操作 + // a1 赠送 a2 2000 积分 + p := 2000 + // 开始事务 + tx := DB.Begin() + // 有时需要考虑数据库是否支持事务的情景 + if tx.Error != nil { + log.Fatalln(tx.Error) + } + + // 执行赠送操作 + a1.Points -= p + a2.Points += p + + // 1执行SQL,可能导致的错误 + if err := tx.Save(&a1).Error; err != nil { + tx.Rollback() + return + } + + if err := tx.Save(&a2).Error; err != nil { + // 回滚事务 + tx.Rollback() + return + } + + // 2业务逻辑可能导致的错误 + // 要求author的积分不能为负数 + if a1.Points < 0 || a2.Points < 0 { + log.Println("a1.Points < 0 || a2.Points < 0") + // 回滚事务 + if err := tx.Rollback().Error; err != nil { + log.Fatalln(err) + } + } + + // 提交事务 + if err := tx.Commit().Error; err != nil { + log.Fatalln(err) + } + + // 决定回滚还是提交,集中的处理错误风格 + //if err1 != nil || err2 != nil { + // tx.Rollback() + //} else { + // tx.Commit() + //} +} + +func TXCallback() { + // 初始化测试数据 + if err := DB.AutoMigrate(&Author{}); err != nil { + log.Fatalln(err) + } + var a1, a2 Author + a1.Name = "库里" + a2.Name = "莫兰特" + a1.Points = 1600 + a2.Points = 200 + if err := DB.Create([]*Author{&a1, &a2}).Error; err != nil { + log.Fatalln(err) + } + log.Println(a1.ID, a2.ID) + + // 实现事务 + if err := DB.Transaction(func(tx *gorm.DB) error { + // a1 赠送 a2 2000 积分 + p := 200 + // 执行赠送操作 + a1.Points -= p + a2.Points += p + + // 1执行SQL,可能导致的错误 + if err := tx.Save(&a1).Error; err != nil { + return err + } + + if err := tx.Save(&a2).Error; err != nil { + return err + } + + // 2业务逻辑可能导致的错误 + // 要求author的积分不能为负数 + if a1.Points < 0 || a2.Points < 0 { + return errors.New("a1.Points < 0 || a2.Points < 0") + } + + // nil 的返回,会导致事务提交 + return nil + }); err != nil { + // 返回错误,为了后续的业务逻辑处理 + // 为了通知我们,事务成功还是失败 + // 返回错误,不影响事务的提交和回滚 + log.Println(err) + } +} + +func TXNested() { + // 初始化测试数据 + if err := DB.AutoMigrate(&Author{}); err != nil { + log.Fatalln(err) + } + var a1, a2, a3 Author + a1.Name = "库里" + a2.Name = "莫兰特" + a3.Name = "欧文" + a1.Points = 1600 + a2.Points = 200 + a3.Points = 4000 + if err := DB.Create([]*Author{&a1, &a2, &a3}).Error; err != nil { + log.Fatalln(err) + } + log.Println(a1.ID, a2.ID, a3.ID) + + // 实现事务 + if err := DB.Transaction(func(tx *gorm.DB) error { + // a1 赠送 a2 2000 积分 + p := 20000 + + // 执行赠送操作 + + // a2 多了积分 + a2.Points += p + if err := tx.Save(&a2).Error; err != nil { + return err + } + + // a1 赠送,使用嵌套事务完成 + errA1 := tx.Transaction(func(tx *gorm.DB) error { + a1.Points -= p + // 1执行SQL,可能导致的错误 + if err := tx.Save(&a1).Error; err != nil { + return err + } + if a1.Points < 0 { + return errors.New("a1.Points < 0") + } + // 没有错误成功 + return nil + }) + + // a1 发送失败,才需要a3 + if errA1 != nil { + // a3 赠送,使用嵌套事务完成 + errA3 := DB.Transaction(func(tx *gorm.DB) error { + a3.Points -= p + if err := tx.Save(&a3).Error; err != nil { + return err + } + if a3.Points < 0 { + return errors.New("a3.Points < 0") + } + return nil + }) + // a3 同样失败 + if errA3 != nil { + return errors.New("a1 and a3 all send points failed") + } + } + + // nil 的返回,会导致事务提交 + return nil + }); err != nil { + // 返回错误,为了后续的业务逻辑处理 + // 为了通知我们,事务成功还是失败 + // 返回错误,不影响事务的提交和回滚 + log.Println(err) + } +} + +func TXSavePoint() { + // 初始化测试数据 + if err := DB.AutoMigrate(&Author{}); err != nil { + log.Fatalln(err) + } + var a1, a2, a3 Author + a1.Name = "库里" + a2.Name = "莫兰特" + a3.Name = "欧文" + a1.Points = 1600 + a2.Points = 200 + a3.Points = 4000 + if err := DB.Create([]*Author{&a1, &a2, &a3}).Error; err != nil { + log.Fatalln(err) + } + log.Println(a1.ID, a2.ID, a3.ID) + + // 事务操作 + // a1 赠送 a2 2000 积分 + p := 20000 + // 开始事务 + tx := DB.Begin() + // 有时需要考虑数据库是否支持事务的情景 + if tx.Error != nil { + log.Fatalln(tx.Error) + } + + // 执行赠送操作 + // a2 得到积分 + a2.Points += p + // 1执行SQL,可能导致的错误 + if err := tx.Save(&a2).Error; err != nil { + tx.Rollback() + return + } + + // 逻辑记录发送points是否成功 + var flagSend bool + + // a1 先给 a2 send + // 设置一个 savepoint + tx.SavePoint("beforeA1") + a1.Points -= p + if err := tx.Save(&a1).Error; err != nil || a1.Points < 0 { + // 回滚到 beforeA1 + tx.RollbackTo("beforeA1") + + // a3 to a2 + tx.SavePoint("beforeA3") + a3.Points -= p + if err := tx.Save(&a3).Error; err != nil || a3.Points < 0 { + // 回滚到 beforeA3 + tx.RollbackTo("beforeA3") + } else { + flagSend = true + } + } else { + flagSend = true + } + + // 判定发送是否成功 + if flagSend { + // 提交事务 + if err := tx.Commit().Error; err != nil { + log.Fatalln(err) + } + } else { + // 回滚事务 + tx.Rollback() + } +} diff --git a/transaction_test.go b/transaction_test.go new file mode 100644 index 0000000..3471f4d --- /dev/null +++ b/transaction_test.go @@ -0,0 +1,19 @@ +package gormExample + +import "testing" + +func TestTXDemo(t *testing.T) { + TXDemo() +} + +func TestTXCallback(t *testing.T) { + TXCallback() +} + +func TestTXNested(t *testing.T) { + TXNested() +} + +func TestTXSavePoint(t *testing.T) { + TXSavePoint() +}