本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《 阿里云开发者社区用户服务协议 》和 《 阿里云开发者社区知识产权保护指引 》。如果您发现本社区中有涉嫌抄袭的内容,填写 侵权投诉表单 进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

前言


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

本人学识尚浅,如有错误,请评论指出,谢谢!

详细可见个人博 客: https://linzyblog.netlify.app/


一、One To One 一对一


数据库连接例子:


var db *gorm.DB
func init() {
    var err error
    //我这里用到数据库是mysql,需要配置DSN属性[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
    dsn := "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8&parseTime=True"
    db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
}


1、Belongs To 属于


belongs to 会与另一个模型建立了一对一的连接。 这种模型的每一个实例都“属于”另一个模型的一个实例。


例如:你有两张表 users 表和 companies 表


  • users --用户表
  • companies --公司表


user 是属于 company 的,就是每个 user 有且只能对应分配给一个 company。


注意:在 User 对象中,有一个和 Company 一样的 CompanyID。 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系, 因此必须包含在 User 结构体中才能填充 Company 内部结构体。



// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
    gorm.Model
    Name      string
    CompanyID int // 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系
    Company   Company
type Company struct {
    ID   int
    Name string
func main() {
    //user 里面有 company表的结构 所以只需要自动迁移user表即可
    db.AutoMigrate(&User{})
}


1ccb4cbfa837407bbfc416ff625559fa.png


1)创建记录


创建在dachang里的用户linzy的记录


c := Company{
    ID:   1,
    Name: "dachang",
u := User{
    Name:    "linzy",
    Company: c,
db.Create(&u)


1c3f78b05c0a4282bcc7f500995fd539.png


不仅 users 表新增记录,companies 表也新增了。这是因为在创建、更新记录时,GORM 会通过 Upsert 自动保存关联及其引用记录。


2)查询记录


var u User
db.Model(&User{}).First(&u)
fmt.Println(u)

3f4f014004bc4d2a97f81d91e162ffad.png


注意:我们发现并没有查出我们关联的 companies 表里面的记录,因为我们在使用CRUD的时候需要所关联的结构时,必须要使用预加载 Preload .


var u User
db.Model(&User{}).Preload("Company").First(&u)
fmt.Println(u)

e3900effe27444c39ceb9d2377017da7.png


3)重写外键


要定义一个 belongs to 关系,数据库的表中必须存在外键。默认gorm使用(关联属性类型 + 主键)组成外键名,如上面的例子User + ID 组成UserID,UserID就作为Profile的外键。


例如我们想自定义外键,就需要用标签foreignKe来指定外键:


type User struct {
  gorm.Model
  Name         string
  CompanyRefer int
  Company      Company `gorm:"foreignKey:CompanyRefer"`
  // 使用 CompanyRefer 作为外键
type Company struct {
  ID   int
  Name string
}


4)重写引用


对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。例如上面的例子,User 中 CompanyRefer 属性作为外键,它和Company 中的ID进行关联,这里 Company 的ID就是关联外键。


我们可以使用标签 references 来更改它,例如:


type User struct {
  gorm.Model
  Name      string
  CompanyID string
  Company   Company `gorm:"references:Code"` // 使用 Code 作为引用
type Company struct {
  ID   int
  Code string
  Name string
}


5)关联模式


a、查找关联


如果我们想查找指定的 user 匹配的关联记录,可以用Association找users表关联的记录:


var user User
db.Where("id = ?", 1).Take(&user)
fmt.Println(user)
var c Company
// `user` 是源模型,它的主键不能为空
// 关系的字段名是 `Company`
// 如果匹配了上面两个要求,会开始关联模式,否则会返回错误
db.Model(&user).Association("Company").Find(&c)
fmt.Println(c)

30225ba0913f4e13bf1f034e0cc27c40.png


b、删除关联


user 可以关联 company,同样也可以不关联,但是去库里删很麻烦,用Delete方法删除源模型与参数之间的关系,只会删除引用,不会从数据库中删除这些对象。


删除前:


eb0357f32dfa468493ac3e5fb6f0c633.png


var user User
db.Where("id = ?", 1).First(&user)
//一定要指定关联的主键
db.Model(&user).Association("Company").Delete(&Company{ID: 1})

548bf690b7234d3db720ff1b3823ac92.png


c、添加关联


var user User
db.Where("id = ?", 1).First(&user)
//一定要指定关联的主键
db.Model(&user).Association("Company").Append(&Company{ID: 1})

ad346ad6d64348d18c8fd9e186da394e.png


d、修改(替换)关联


修改关联就是删除关联后,再添加新的关联。


382e1227e34946ed9602d00c243ce2fe.png


var user User
db.Where("id = ?", 1).First(&user)
db.Model(&user).Association("Company").Replace(&Company{ID: 2})


c81211ea39334868a114f7dcca7eb6f9.png


063f933703c94a309170d516912ea2aa.png


2、Has One 拥有


has one 与另一个模型建立一对一的关联,但它和一对一关系有些许不同。 这种关联表明一个模型的每个实例都包含或拥有另一个模型的一个实例。


提示:Has one很像属于(belongs to)关系,都是一对一关系,区别是Has One关系和Belongs To关系,持有关联Model属性的关系是相反的,例如:A 关联 B,Has One关系通常是A 结构体持有B属性, belongs to关系则是B结构体持有A。


例如:你有两张表 users 表和 credit_cards 表


  • users --用户表
  • credit_cards --信用卡表


user 是拥有 creditcard 的,creditcard 有且只能被一个 user 拥有。


// User 有一张 CreditCard,UserID 是外键
type User struct {
    gorm.Model
    CreditCard CreditCard
type CreditCard struct {
    gorm.Model
    Number string
    UserID uint
func main() {
    //这里需要先创建users表,再创建credit_cards表
    db.AutoMigrate(&User{}, &CreditCard{})
}


1)创建记录


创建用户拥有信用卡号为123456的记录


c := CreditCard{
    Number: "123456",
u := User{
    CreditCard: c,
db.Create(&u)


e413c74fdc2443cc8f1c3962ae7cc0c9.png

89db4020798b42509dc9072245286c90.png


2)查询记录


var u User
db.Model(&User{}).Preload("CreditCard").First(&u)
fmt.Println(u)

799e29348f2b4eba80680e0446ed3677.png


3)重写外键


对于 has one 关系,同样必须存在外键字段。拥有者将把属于它的模型的主键保存到这个字段。


默认情况下Has One关系的外键由持有关联属性的类型名 + 主键 组成外键名,如上例,User关联CreditCard的外键就是User + ID = UserID。


如果你想要使用另一个字段来保存该关系,你同样可以使用标签 foreignKey 来更改它,例如:


type User struct {
  gorm.Model
  CreditCard CreditCard `gorm:"foreignKey:UserName"`
  // use UserName as foreign key
type CreditCard struct {
  gorm.Model
  Number   string
  UserName string
}


4)重写引用


默认情况下,保存User的时候,会自动将User的主键保存到外键UserID中,关联查询的时候,也会使用外键和关联外键进行关联进行查询,这里User的ID就是关联外键。


可以使用标签 references 来更改它,例如:


type User struct {
  gorm.Model
  Name       string     `gorm:"index"`
  CreditCard CreditCard `gorm:"foreignkey:UserName;references:name"`
type CreditCard struct {
  gorm.Model
  Number   string
  UserName string
}


5)关联模式


a、查找关联


如果我们想查找指定的 user 匹配的关联记录,可以用Association找users表关联的记录:


var user User
db.Where("id = ?", 1).Take(&user)
fmt.Println(user)
var c CreditCard
// `user` 是源模型,它的主键不能为空
// 关系的字段名是 `CreditCard`
// 如果匹配了上面两个要求,会开始关联模式,否则会返回错误
db.Model(&user).Association("CreditCard").Find(&c)
fmt.Println(c)

c740d6ae88a84df1b95f539edc3f9fbe.png


b、删除关联


user 可以关联 CreditCard,同样也可以不关联,但是去库里删很麻烦,用Delete方法删除源模型与参数之间的关系,只会删除引用,不会从数据库中删除这些对象。


删除前:


3f5a598302e94fc28a14f8735bf0dc72.png


user := User{}
db.Where("id = ?", "1").First(&user)
//一定要指定关联的主键
db.Model(&user).Association("CreditCard").Delete(&CreditCard{
    Model: gorm.Model{
        ID: 1,
})

c83f31d168d540d2b6acac87bdb3e214.png


c、添加关联


var user User
db.Where("id = ?", 1).First(&user)
//一定要指定关联的主键
db.Model(&user).Association("CreditCard").Append(&CreditCard{
    Model: gorm.Model{
        ID: 1,
})

ea73ab4e45854e81b05238710e3626a7.png


d、修改(替换)关联


修改关联就是删除关联后,再添加新的关联。


80959b12b28341f494f6db9a9710313f.png


user := User{}
db.Where("id = ?", "2").First(&user)
db.Model(&user).Association("CreditCard").Replace(&CreditCard{
    Model: gorm.Model{
        ID: 1,
})

626d8908d6ab48a98657daf25ecbdc41.png


二、Has Many 一对多


has many 与另一个模型建立了一对多的连接。 不同于 has one,拥有者可以有零或多个关联模型。


例如:你有两张表 users 表和 credit_cards 表


  • users --用户表
  • credit_cards --信用卡表


user 是拥有多张 creditcard的。


// User 有多张 CreditCard,UserID 是外键
type User struct {
  gorm.Model
  CreditCards []CreditCard
type CreditCard struct {
  gorm.Model
  Number string
  UserID uint
func main() {
    db.AutoMigrate(&User{}, &CreditCard{})
}


1、创建记录


创建用户拥有两种信用卡的记录


c1 := CreditCard{
    Number: "123456",
c2 := CreditCard{
    Number: "8791265",
u := User{
    CreditCards: []CreditCard{c1, c2},
db.Create(&u)


3e6819909dc3427393a446f28de4e710.png

155396c5f5f143b3ad0b6f1de7db0736.png


2、查询记录


var user User
db.Model(&User{}).Preload("CreditCards").Find(&user)
fmt.Println(user)

18e5110b116347a3bb316e24c5cf4170.png


3、预加载


GORM 允许在 Preload 的其它 SQL 中直接加载关系


1)预加载全部


与创建、更新时使用 Select 类似,clause.Associations 也可以和 Preload一起使用,它可以用来 预加载全部关联,例如:


type User struct {
  gorm.Model
  Name       string
  CompanyID  uint
  Company    Company
  Role       Role
  Orders     []Order
db.Preload(clause.Associations).Find(&users)


2)嵌套预加载


clause.Associations 不会预加载嵌套的关联,如果你在需要用嵌套的关联,你可以使用嵌套预加载 例如:


// User 有多张 CreditCard,UserID 是外键
type User struct {
    gorm.Model
    CreditCards []CreditCard
type CreditCard struct {
    gorm.Model
    Number string
    UserID uint
    Info   Info
type Info struct {
    ID           uint
    Name         string
    Age          int
    CreditCardID int
}

480847c29a4542168f62d86469e58bed.png

34b7e74961bf4719a70e0db24ed1b99f.png


2295011d182a40f9967b31350411af4e.png


使用之前预加载查询记录查询:


var user User
db.Model(&User{}).Preload("CreditCards").Find(&user)
fmt.Println(user)

4c65e634a5d649ada24f2384e3a30bee.png


我们只能查到主表对应关联的表结构记录,所以我们这里需要用到嵌套预加载的方式拿到我们需要的数据。


var user User
//CreditCards.Info关联的下层结构
db.Model(&User{}).Preload("CreditCards.Info").Preload("CreditCards").Find(&user)
fmt.Println(user)


或者 使用 自定义预加载 SQL:


var user User
db.Model(&User{}).Preload("CreditCards", func(db *gorm.DB) *gorm.DB {
    return db.Preload("Info")
}).Find(&user)
fmt.Println(user)

11e132fd0d874c7286e13382fafe0942.png


3)带条件的预加载


有时候我们需要查询特定的关联结构记录时,可以使用带条件的 Preload 关联,类似于内联条件。


var user User
//找到信用卡号不等于`123456`的记录
db.Model(&User{}).Preload("CreditCards.Info").Preload("CreditCards", "Number <> ?", "123456").Find(&user)
fmt.Println(user)

49402fbea98a4a7b849a92e3195e7116.png


a、Joins 预加载


可以直接查询关联的下层关联结构,但是这时候不能直接使用带条件的 Preload 关联了,例如我要找信用卡用户不是”linzy“的时候:


var user User
db.Model(&User{}).Preload("CreditCards.Info", "name <> ?", "linzy").Preload("CreditCards").Find(&user)
fmt.Println(user)

cedf622ff4084fb68d2029c9e3cee97f.png


他虽然满足了条件,但不是我们想要的结果,这个时候需要用到自定义加载SQL以及 Joins 预加载:


var user User
db.Model(&User{}).Preload("CreditCards", func(db *gorm.DB) *gorm.DB {
    return db.Joins("Info").Where("name <> ?", "linzy")
}).Find(&user)
fmt.Println(user)

aa85e912499d4fd896e914beedfd0c88.png


注意:Preload 在一个单独查询中加载关联数据。而 Join Preload 会使用 left join 加载关联数据。


Join Preload 适用于一对一的关系,例如: has one, belongs to。


4、多态关联


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


type Dog struct {
    ID   int
    Name string
    Toys []Toy `gorm:"polymorphic:Owner;"`
type Toy struct {
    ID        int
    Name      string
    OwnerID   int
    OwnerType string
func main() {
    db.AutoMigrate(&Dog{}, &Toy{})
    db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
    // INSERT INTO `dogs` (`name`) VALUES ("dog1")
    // INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","dogs"), ("toy2","1","dogs")
}

537710cad09840be95c17e76dadf7b22.png

bfdd974c44d147d4a52ad88119b5dfae.png


5、关联模式


关联模式下不同关系的CRUD关联都是类似的写法,就不多阐述了。


1)清空关联


删除源模型与关联之间的所有引用,但不会删除这些关联。


var user User
db.First(&user)
db.Model(&user).Association("CreditCards").Clear()

4f21dda433da4dbf829abc3544203516.png


2)关联计数


返回当前关联的总数。


var user User
db.First(&user)
//添加关联
db.Model(&user).Association("CreditCards").Append(&CreditCard{
    Model: gorm.Model{
        ID: 1,
}, &CreditCard{
    Model: gorm.Model{
        ID: 2,
//关联计数
count := db.Model(&user).Association("CreditCards").Count()
fmt.Println(count)

07fa4b41382743d8836cc83769e1fcfb.png


三、Many To Many 多对多


Many to Many 会在两个 model 中添加一张连接表。


例如,你有两张表 users 表和 languages 表


  • users --用户表
  • languages --语言表


一个 用户可以说多种 语言,多个 用户也可以说一种 语言。


// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
type Language struct {
  gorm.Model
  Name string
}


当使用 GORM 的 AutoMigrate 为 User 创建表时,GORM 会自动创建连接表。


某种意义上这种其实还是一对多的关系,反向引用的形式才是真正多对多的关系。


1、反向引用


我既可以用 User 创建多条关联,也可以通过 Language 多条关联。


// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
type Language struct {
  gorm.Model
  Name string
  Users []User `gorm:"many2many:user_languages;"`
func main() {
    db.AutoMigrate(&User{}, &Language{})
}

ade07e7572b7429c98e0b2a55f87d68f.png


2、创建记录


  • 创建用户拥有两种语言的记录


l1 := Language{
    Name: "中文",
l2 := Language{
    Name: "英文",
u1 := User{
    Languages: []Language{l1, l2},
db.Create(&u1)

75403afd336a43379d260778cb0dde31.png

e3f0462013134d478fb60e27db5ed05c.png

9753204cc0fc40d18ae2d2d42ba7a1c4.png


  • 创建外星语被两个用户所使用的记录:


u := User{}
l := Language{
    Name:  "外星语",
    //也可以直接指定创建好的记录的主键
    Users: []User{u, User{Model: gorm.Model{ID: 1}}},
db.Create(&l)


8cc077fd93a84dbc8a223ba2b0fcebf8.png

554698a4478648d9a6fde165658f1ca6.png


be7e20ef335d4696a1a6851dd7da64b4.png


3、查找记录


u := User{}
db.Where("id = ?", 1).Find(&u)
db.Model(&User{}).Preload("Languages").Find(&u)
fmt.Println(u)

549c71be58444df09e8a63ec04cc8a8c.png


4、关联模式


1)查询关联


查询user 对应的关联记录


u := User{}
db.Where("id = ?", 2).Find(&u)
var l []Language
db.Model(&u).Association("Languages").Find(&l)
fmt.Println(l)


18bf57ef7cd444ceb79995c4413ca943.png


2)添加关联


u := User{}
db.Where("id = ?", 2).Find(&u)
l1 := Language{
    Name: "俄语",
l2 := Language{
    Name: "法语",
db.Model(&u).Association("Languages").Append(&l1, &l2)

d42a9af75f184a20a23a4aeca8549f8b.png


047764b263414a2c99d8250711cddd72.png


注意:添加关联的同时,不仅连接表会添加新关联记录,关联的结构表也会添加新的记录,除非指定的是表里已存在数据则不会添加。


3)删除关联


删除关联并不会删除关联的结构表里面的数据。


u := User{}
db.Where("id = ?", 2).Find(&u)
db.Model(&u).Association("Languages").Delete(&Language{
    Model: gorm.Model{
        ID: 3,
})

a1bd950c3b4543a5990e2babb8a2056b.png


4)修改(替换)关联


u := User{}
db.Where("id = ?", 2).Find(&u)
db.Model(&u).Association("Languages").Replace(&Language{
    Model: gorm.Model{
        ID: 3,
})


修改前:


f01aa18e386541d5b016fe37bbb3b17a.png


修改后:


d12bb2ee055442dfbd6916dd88fc8754.png


注意:在多对多使用修改关联会把连接表里面所有关于主表主键与外键关联的记录全部替换掉,慎用此操作。


5)清空关联


u := User{}
db.Where("id = ?", 2).Find(&u)
db.Model(&u).Association("Languages").Clear()

791a4ccdda5c4799bd238938a4ea019b.png


四、小结


一对一、一对多和多对多关系下的CRUD关联操作,在应对复杂的数据表关系时也能更好的应对,本着以实战操作为学习方法,最好结合官方文档上手是最好最正确的。


关联的类型 描述

一对一

两个表在关联的每一侧只能具有一个记录。


每个主键值或者与相关表中的所有记录都无关,或者仅与一个记录相关。


大多数一对一关联是由业务规则强制的,而不是从数据自然流动。 若没有这样的规则,通常可以将两个表相结合,而不会违反任何规范化规则。

一对多 主键表只包含一个记录,其与相关表中零个、一个或多个记录相关。
多对多 两个表中的每个记录都可以与另一个表中的零个或任意数目个记录相关。 由于关系系统不能直接适应关联,因此这些关联需要第三个表,其称为关联或链接表。


a11fcff749654e529b60700ad63a52a9.png


关联标签以及自定义SQL预加载内容将留到事务中。


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

1.模型使用 约定:GORM 倾向于约定,而不是配置。默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间 GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt
dahezhiquan