Gorm学习(五)进阶:多态关联、关联标签以及事务

本文最后更新于:1 年前

前言

感谢开源项目gin-vue-admin,以及1010工作室的视频教程

一、多态关联

1、多态关联概述

  • 什么是多态?

    多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

  • 什么是多态表?
    假设我们有一张地址表,其中的地址可能是来自User中的,也可能是来自Orders中的。而区分不同的对象则用type字段。如:type=User时对象是文章表。

  • 什么是多态关联?
    多态关联就是多态表和对象表之间的关联性。一个多态关联由使用同一外键发生的两个(或多个)关联组成.

2、为什么用多态关联?

出现需要外键引用多个表的情况,不可能删除原来表结构,重新添加一个外键ID再建表,所以我们可以建立一个交叉表。让Addres不再依赖于User表或者Order表。

has one的情况解决方案,如果我们希望一个给定的地址,只能够在一张交叉表中出现一次,上面的复合主键已经做到了。

has many的情况解决方案,如果希望一个地址可以在一张交叉表中出现多次,可以取消Address的复合主键。

3、Has One

GORM 为 has onehas many 提供了多态关联支持,它会将拥有者实体的表名、主键值都保存到多态类型的字段中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type User struct {
ID int
Name string
//polymorphic指定多态类型,比如模型名
Address Address `gorm:"polymorphic:Owner;"`
}

type Order struct {
ID int
Name string
Address Address `gorm:"polymorphic:Owner;"`
}

type Address struct {
ID int
Name string
OwnerID int
OwnerType string
}

func main() {
db.AutoMigrate(&User{}, &Order{}, &Address{})
}

  • 创建记录
1
2
3
4
5
6
7
8
9
10
11
12
db.Create(&User{
Name: "linzy",
Address: Address{
Name: "翻斗花园",
},
})
db.Create(&Order{
Name: "忘崽牛奶",
Address: Address{
Name: "火星幼儿园",
},
})



owner_type就是关联的那张表。
owner_id就是关联的表的主键。

4、Has Many

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
type User struct {
ID int
Name string
Address []Address `gorm:"polymorphic:Owner;"`
}

type Order struct {
ID int
Name string
Address Address `gorm:"polymorphic:Owner;"`
}

type Address struct {
ID int
Name string
OwnerID int
OwnerType string
}

func main() {
db.AutoMigrate(&User{}, &Order{}, &Address{})
db.Create(&User{
Name: "linzy",
Address: []Address{
{Name: "翻斗花园"},
{Name: "杭州西湖"},
},
})
}

二、关联标签

1、polymorphic & polymorphicValue

  • polymorphic:通俗讲用来指定id与type的前缀。
  • polymorphicValue用来告诉关联表我是谁,默认都是表名。
    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
    type User struct {
    ID int
    Name string
    //polymorphic:通俗讲用来指定id与type的前缀
    Address []Address `gorm:"polymorphic:Address;"`
    }

    type Order struct {
    ID int
    Name string
    //polymorphicValue用来告诉关联表我是谁,默认都是表名
    Address Address `gorm:"polymorphic:Address;polymorphicValue:master"`
    }

    type Address struct {
    ID int
    Name string
    AddressID int
    AddressType string
    }

    func main() {
    db.AutoMigrate(&User{}, &Order{}, &Address{})
    db.Create(&User{
    Name: "linzy",
    Address: []Address{
    {Name: "翻斗花园"},
    {Name: "杭州西湖"},
    },
    })

    db.Create(&Order{
    Name: "忘崽牛奶",
    Address: Address{
    Name: "火星幼儿园",
    },
    })
    }


2、foreignKey & references

  • foreignKey:用来指定连接表的外键。
  • references:用来指定引用表的列名与连接表的外键映射。

GORM里默认是连接表和引用表的主键来作为做外键以及外键映射的。

Has One的例子:

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
type CreditCard struct {
gorm.Model
Number string
//外键指向CreditCardNumber
Info Info `gorm:"foreignKey:CreditCardNumber"`
}

type Info struct {
ID uint
Name string
Age int
CreditCardNumber string
}

func main() {
db.AutoMigrate(&CreditCard{}, &Info{})
db.Create(&CreditCard{
Number: "123456",
Info: Info{
Name: "linzy",
Age: 18,
},
})

db.Create(&CreditCard{
Number: "456789",
Info: Info{
Name: "slyyy",
Age: 66,
},
})
}

注意:credit_card_number并没有自动指向creditcard表里的number字段,他还是会默认指向引用表里的主键,所以在用foreignKey的时候最好类型相同或者使用references搭配使用。

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
type CreditCard struct {
ID uint
//设置唯一和固定长度
Number string `gorm:"index:unique;size:255"`
Info Info `gorm:"foreignKey:CreditCardNumber;references:Number"`
}

type Info struct {
ID uint
Name string
Age int
//设置唯一和固定长度
CreditCardNumber string `gorm:"index:unique;size:255"`
}

func main() {
db.AutoMigrate(&CreditCard{}, &Info{})
db.Create(&CreditCard{
ID: 1,
Number: "123456",
Info: Info{

Name: "linzy",
Age: 18,
},
})

db.Create(&CreditCard{
ID: 2,
Number: "456789",
Info: Info{
Name: "slyyy",
Age: 66,
},
})
}

注意: 某些数据库只允许在唯一索引字段上创建外键,如果在迁移时会创建外键,则需要指定 index:unique 标签。


错误:

  1. Error 1170: BLOB/TEXT column 'credit_card_number' used in key specification without a key length
    出现这个问题是因为你的外键或者外键映射的字段是text类型也就是不固定长度string类型,不能作为外键或外键映射,必须通过标签size设置固定长度。
  2. Error 1215: Cannot add foreign key constraint
    这个错误是不能创建外键,主要原因可能是你外键映射的字段不是引用表的主键,建议标签设置为唯一 index:unique

3、Many to Many

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
type CreditCard struct {
ID uint
Number string `gorm:"index:unique;size:255"`
Infos []Info `gorm:"many2many:card_infos;foreignKey:Number;references:Name;"`
}

type Info struct {
ID uint
Name string `gorm:"index:unique;size:255"`
Age int
}

func main() {
db.AutoMigrate(&CreditCard{}, &Info{})
db.Create(&CreditCard{
Number: "123456",
Infos: []Info{
{
ID: 1,
Name: "linzy",
Age: 18,
},
},
})

db.Create(&CreditCard{
Number: "456789",
Infos: []Info{
{
ID: 2,
Name: "slyyy",
Age: 66,
},
{
ID: 3,
Name: "qhgwueiq",
Age: 1,
},
},
})
}



注意:在Many to Many的情况下,foreignKey指向的是引用表的外键映射字段,references指向的是关联表的外键字段,一定不要搞混了。

4、joinForeignKey & joinReferences

  • joinForeignKey:指定Many to Many产生的连接表中关联外键映射字段的名称。
  • joinReferences:指定Many to Many产生的连接表中关联外键字段的名称。
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
type CreditCard struct {
ID uint
Number string `gorm:"index:unique;size:255"`
Infos []Info `gorm:"many2many:card_infos;foreignKey:Number;joinForeignKey:card_number;references:Name;joinReferences:name"`
}

type Info struct {
ID uint
Name string `gorm:"index:unique;size:255"`
Age int
}

func main() {
db.AutoMigrate(&CreditCard{}, &Info{})
db.Create(&CreditCard{
Number: "123456",
Infos: []Info{
{
ID: 1,
Name: "linzy",
Age: 18,
},
},
})

db.Create(&CreditCard{
Number: "456789",
Infos: []Info{
{
ID: 2,
Name: "slyyy",
Age: 66,
},
{
ID: 3,
Name: "qhgwueiq",
Age: 1,
},
},
})
}

三、事务

1、事务概述

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。 事务由事务开始与事务结束之间执行的全部数据库操作组成。

数据库事务必须具备的四个特性:

  • 原子性(Atomicity)
    事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。
  • 一致性(Consistency)
    事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
  • 隔离性(Isolation)
    一个事务的执行不能被其他事务干扰。
  • 持续性/永久性(Durability)
    一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。

2、事务操作

要在事务中执行一系列操作,一般流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type User struct {
gorm.Model
Name string
}

func main() {
db.AutoMigrate(&User{})
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,应该使用 'tx' 而不是 'db')
if err := tx.Create(&User{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}

if err := tx.Create(&User{Name: "Lion"}).Error; err != nil {
return err
}

// 返回 nil 提交事务
return nil
})
}

1)回滚

注意:返回任何错误都会回滚事务。回滚则事务内的操作一律不执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
db.AutoMigrate(&User{})
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,应该使用 'tx' 而不是 'db')
if err := tx.Create(&User{Name: "linzy"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}

if err := tx.Create(&User{Name: "slyyy"}).Error; err != nil {
return err
}

if true {
return errors.New("回滚")
}

// 返回 nil 提交事务
return nil
})
}

2)嵌套事务

嵌套事务的作用在于较大的事务中,你只想回滚一部分操作,例如你去银行转账,已经通过银行卡号和密码登录了,转账的过程是你的银行账户扣去多少钱,同时别人的银行账户加上多少钱,如果中途发生错误,需要回滚,应该回滚到你账号登录后的状态,而不是直接回滚到你账号登录前。

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

type User struct {
gorm.Model
Name string
Number int
}

func main() {
db.AutoMigrate(&User{})
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{
Name: "linzy",
Number: 100,
})
tx.Create(&User{
Name: "slyyy",
Number: 100,
})
fmt.Println("登陆后")
tx.Transaction(func(tx2 *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,应该使用 'tx' 而不是 'db')
if err := tx2.Model(&User{}).Where("name = ?", "linzy").Update("number", 80).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}

if err := tx2.Model(&User{}).Where("name = ?", "slyyy").Update("number", 120).Error; err != nil {
return err
}
return nil
})
fmt.Println("转账结束")
// 返回 nil 提交事务
return nil
})
}


正常的过程应该是这样,如果说嵌套事务发生回滚操作之后的情况呢?

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
func main() {
db.AutoMigrate(&User{})
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{
Name: "linzy",
Number: 100,
})
tx.Create(&User{
Name: "slyyy",
Number: 100,
})
fmt.Println("登陆后")
tx.Transaction(func(tx2 *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,应该使用 'tx' 而不是 'db')
if err := tx2.Model(&User{}).Where("name = ?", "linzy").Update("number", 80).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}

if true {
fmt.Println("转账失败,对面是骗子不能转!!")
return errors.New("转账失败")
}

if err := tx2.Model(&User{}).Where("name = ?", "slyyy").Update("number", 120).Error; err != nil {
return err
}
return nil
})
fmt.Println("转账结束")
// 返回 nil 提交事务
return nil
})
}


3、手动事务

Gorm 支持直接调用事务控制方法(commit、rollback),例如:

事务方法 说明
tx := db.Begin() 开始事务
tx.(db操作) 在事务中执行一些 db 操作
tx.Rollback() 遇到错误时回滚事务
tx.SavePoint() 设置保存点标记
tx.RollbackTo() 回滚到保存点标记
tx.Commit() 提交事务
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
type User struct {
gorm.Model
Name string
Number int
}

func main() {
db.AutoMigrate(&User{})
// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(&User{
Name: "linzy",
Number: 100,
})
tx.Create(&User{
Name: "slyyy",
Number: 100,
})
fmt.Println("登陆后")
//设置回滚标记
tx.SavePoint("登录了")
flag := true
{
if err := tx.Model(&User{}).Where("name = ?", "linzy").Update("number", 80).Error; err != nil {
flag = true
}

if err := tx.Model(&User{}).Where("name = ?", "slyyy").Update("number", 120).Error; err != nil {
flag = true
}

//出现问题了得写在一系列事务之后进行回滚
if flag {
fmt.Println("转账失败,对面是骗子不能转!!")
//回滚到指定标记
tx.RollbackTo("登录了")
}
}
// 遇到错误时回滚事务
fmt.Println("转账结束")
// 否则,提交事务
tx.Commit()
}

官方示例:

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
func CreateAnimals(db *gorm.DB) error {
// 再唠叨一下,事务一旦开始,你就应该使用 tx 处理数据
tx := db.Begin()
//延迟函数一定要写上,因为出现panic错误时事务可能没办法回滚,需要手动再回滚
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()

if err := tx.Error; err != nil {
return err
}

if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
tx.Rollback()
return err
}

if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
tx.Rollback()
return err
}

return tx.Commit().Error
}

4、禁用默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

1
2
3
4
5
6
7
8
9
10
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})

// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

四、小结

本章拓展了GORM对数据库的更多支持,关联标签与事务是很重要的内容。

  • 关联标签:
标签 描述
foreignKey 指定当前模型的列作为连接表的外键
references 指定引用表的列名,其将被映射为连接表外键
polymorphic 指定多态类型,比如模型名
polymorphicValue 指定多态值、默认表名
many2many 指定连接表表名
joinForeignKey 指定连接表的外键列名,其将被映射到当前表
joinReferences 指定连接表的外键列名,其将被映射到引用表
  • 事务:

手动事务适用于小事务操作,出错了直接全部回滚会更好,虽然提供了 SavePointRollbackto方法,来提供保存点以及回滚至保存点功能,但是有一些同步操作操作很不方便。
GORM自带事务适用大事务操作,可以使用嵌套事务。

若有写的错误的或者需要改进的地方,希望能直接指出,再次感谢GVA淼哥的教程!


Gorm学习(五)进阶:多态关联、关联标签以及事务
https://gopherlinzy.github.io/2022/10/11/gorm-transaction/
作者
孙禄毅
发布于
2022年10月11日
许可协议