Bootstrap

Golang gorm 关联关系 一对多

GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

一对多入门


外键是指在一个表中的某个字段引用了另一个表中的主键,通过外键可以建立表之间的关联关系。

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

例如,您的应用包含 user 和 credit card 模型,且每个 user 可以有多张 credit card。

has many介绍

  • has many 关联就是创建和另一个模型的一对多关系(数据库里面是一对多,然后struct里面也是一对多)
  • 例如, 例如每一个用户都拥有多张信用卡,这样就是生活中一个简单的一对多关系

在设计表的时候不可能将所有的信息都放在一个表里面,那么表就会非常非常的宽。这样字段就会非常的多,性能就会受到影响。

在设计的时候这里其实就可以设置为两张表。一个是用户的详情表和信用卡的详情表,用户表里面加上卡的id和他做一个关联,那么这就是一对多的关系。

当一个数据库存储了很多数据的时候就需要分库分表,上面也类似,将一张表分为两张表。

Gorm 关联查询(又叫连表查询)中的 Has Many 关系是 一对多 的关联关系,通常用于描述一个 Model 拥有多个 Model。

比如:一个 用户拥有多张信用卡,下面以 Go Struct 表示表结构

creditcar的外键是userid。

type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// 继承gorm的基础Model,里面默认定义了ID、CreatedAt、UpdatedAt、DeletedAt 4个字段
type User struct {
	gorm.Model 
//默认创建三个时间字段和一个id字段,create_at update_at delete_at这样可以知道创建时间/更新时间/删除时间

	CreditCards []CreditCard66 
//代表在查询user数据的时候可以包含creditcard,这其实是有一个关联关系的,代表查询可以查出来,不代表有这个字段。方便做关联查询使用的
// 一对多关联属性,表示多张信用卡
}


//用户有多张信用卡,UserID是外键
type CreditCard struct {
	gorm.Model
	Number   string
	UserId   int //默认会在CreditCard表中生成UserId字段作为与user表关联的外键id
  //默认会在CreditCard表中生成UserID字段作为与User表关联的外键ID 
}

func main() {
	dsn := "root:7PXjAkY!&nlR@tcp(192.168.11.128:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn66), &gorm.Config{})
	db.AutoMigrate(User{}, CreditCard{})
}

真正表名两张表有关联关系的是UserId这个字段。当你去查user的时候可以通过userid去去将creditcard捞出来。

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name       string
	CreditCard []CreditCard
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID uint
}

func main() {
	dsn := "root:7PXjAkY!&nlR@tcp(192.168.11.128:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	db.AutoMigrate(User{}, CreditCard{})

	u := &User{
		Name: "lucas",
		CreditCard: []CreditCard{
			{Number: "0001"},
			{Number: "0002"},
		},
	}

	db.Create(u)
}



mysql> select * from credit_cards;
+----+-------------------------+-------------------------+------------+--------+---------+
| id | created_at              | updated_at              | deleted_at | number | user_id |
+----+-------------------------+-------------------------+------------+--------+---------+
|  1 | 2023-06-29 16:02:41.504 | 2023-06-29 16:02:41.504 | NULL       | 0001   |       1 |
|  2 | 2023-06-29 16:02:41.504 | 2023-06-29 16:02:41.504 | NULL       | 0002   |       1 |
+----+-------------------------+-------------------------+------------+--------+---------+
2 rows in set (0.00 sec)

mysql> select * from users;
+----+-------------------------+-------------------------+------------+-------+
| id | created_at              | updated_at              | deleted_at | name  |
+----+-------------------------+-------------------------+------------+-------+
|  1 | 2023-06-29 16:02:41.478 | 2023-06-29 16:02:41.478 | NULL       | lucas |
+----+-------------------------+-------------------------+------------+-------+
1 row in set (0.00 sec)

 二  外键


为了定义一对多关系,外键是必须存在的,默认外键的名字是所有者类型的名字加上它的主键
(UserId) 。 就像上面的例子,为了定义一个属于User 的模型,外键就应该为 UserID
使用其他的字段名作为外键,你可以通过 foreignkey 来定制它,例如 :
type User struct{
 gorm.Model
 CreditCards []CreditCard `gorm:"foreignKey:UserRefer"`
}

type CreditCard struct{
 gorm.Model
 Number  string
 UserRefer uint
}

`gorm:"foreignKey:UserRefer"` 可以使用这个字段来去改它的外键,但是外键参考的还是user表的id。

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	CreditCards []*CreditCard `gorm:"foreignKey:UserRefer"`
}

type CreditCard struct {
	gorm.Model
	Number    int
	UserRefer int  
}

func main() {
	dsn6 := "root:7PXjAkY!&nlR@tcp(192.168.11.128:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn6), &gorm.Config{})
	db.AutoMigrate(&User{}, &CreditCard{})
}

mysql> desc users;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)     | YES  |     | NULL    |                |
| updated_at | datetime(3)     | YES  |     | NULL    |                |
| deleted_at | datetime(3)     | YES  | MUL | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+

mysql> desc credit_cards;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)     | YES  |     | NULL    |                |
| updated_at | datetime(3)     | YES  |     | NULL    |                |
| deleted_at | datetime(3)     | YES  | MUL | NULL    |                |
| number     | bigint          | YES  |     | NULL    |                |
| user_refer | bigint unsigned | YES  | MUL | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+

mysql> drop tables users;
ERROR 3730 (HY000): Cannot drop table 'users' referenced by a foreign key constraint 'fk_users_credit_cards' on table 'credit_cards'.

mysql> show create table credit_cards;
 CONSTRAINT `fk_users_credit_cards` FOREIGN KEY (`user_refer`) REFERENCES `users` (`id`)

一般不推荐使用这种方式,还是推荐使用Userid的方式。

三 外键关联


GORM 通常使用所有者的主键作为外键的值,在上面的例子中,它就是 User ID 。当你分配信用卡给一个用户,GORM 将保存用户 ID 到信用卡表的 UserID 字段中。 你能通过association_foreignkey来改变它。
type User struct {
    gorm.Model
    MemberNumber string
// 默认CreditCard会使用User表的Id作为外键,association_foreignkey:MemberNumber
// 指定使用MemberNumber 作为外键关联
CreditCards []CreditCard
`gorm:"foreignkey:UserMemberNumber;association_foreignkey:MemberNumber"`
}


type CreditCard struct {
    gorm.Model
    Number string
    UserMemberNumber string
}

其实就是creditcard外键UserMemberNumber直接去找user的 MemberNumber。

上面其实也就是改变了外键的默认值,默认外键是Userid,那么现在变为了MemberNumber。

一般使用默认id的外键就能够覆盖很多场景,上面这种只是举例。

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	MemberNumber string
	CreditCards  []*CreditCard `gorm:"foreignKey:UserMemberNumber;association_foreignKey:MemberNumber"`
}

type CreditCard struct {
	gorm.Model
	Number           int
	UserMemberNumber string
}

func main() {
	dsn6 := "root:7PXjAkY!&nlR@tcp(192.168.11.128:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn6), &gorm.Config{})
	db.AutoMigrate(&User{}, &CreditCard{})
}


mysql> desc users;
+---------------+-----------------+------+-----+---------+----------------+
| Field         | Type            | Null | Key | Default | Extra          |
+---------------+-----------------+------+-----+---------+----------------+
| id            | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| created_at    | datetime(3)     | YES  |     | NULL    |                |
| updated_at    | datetime(3)     | YES  |     | NULL    |                |
| deleted_at    | datetime(3)     | YES  | MUL | NULL    |                |
| member_number | longtext        | YES  |     | NULL    |                |
+---------------+-----------------+------+-----+---------+----------------+

mysql> desc credit_cards;
+--------------------+-----------------+------+-----+---------+----------------+
| Field              | Type            | Null | Key | Default | Extra          |
+--------------------+-----------------+------+-----+---------+----------------+
| id                 | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| created_at         | datetime(3)     | YES  |     | NULL    |                |
| updated_at         | datetime(3)     | YES  |     | NULL    |                |
| deleted_at         | datetime(3)     | YES  | MUL | NULL    |                |
| number             | bigint          | YES  |     | NULL    |                |
| user_member_number | bigint unsigned | YES  | MUL | NULL    |                |
+--------------------+-----------------+------+-----+---------+----------------+

创建一对多表


表结构定义
/*
constraint:OnUpdate:CASCADE 【当User表更新,也会同步给CreditCards】 // 外键约束
OnDelete:SET NULL 【当User中数据被删除时,CreditCard关联设置为 NULL,不删除记录】
*/

type User struct {
    gorm.Model
    Username string `json:"username" gorm:"column:username"`
    CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
    gorm.Model
    Number string
    UserID uint
}

func main() {
    // 0、连接数据库
    dsn := "root:1@tcp(127.0.0.1:3306)/test_db?
    charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

// 创建表结构
    db.AutoMigrate(User{}, CreditCard{})

// 1、创建一对多
    user := User{
        Username: "zhangsan",
        CreditCards: []CreditCard{
        {Number: "0001"},
        {Number: "0002"},
    },
}

db.Create(&user)

// 2、为已存在用户添加信用卡
    u := User{Username: "zhangsan"}
    db.First(&u)
    //fmt.Println(u.Username)

}
上面如果要查询的话是没有去做关联查询的,只能查到user表相关的信息,并不能查到creditcard信息。
创建结果说明
我们没有指定 foreignkey ,所以会与 UserID 字段自动建立外键关联关系

一对多Association 查找关联


使用 Association 方法, 需要把把 User 查询好, 然后根据 User 定义中指定AssociationForeignKey 去查找CreditCard。

查询某条数据使用first 查询唯一值

	u := &User{Id: 1}
	db.Debug().First(u)
	fmt.Println(u)
 
	db.Where("id=?",u.Id).First(u)
import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	UserName    string       `json:"username" gorm:"column:username"`
	CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID int
}

func main() {
	dsn6 := "root:7PXjAkY!&nlR@tcp(192.168.11.128:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn6), &gorm.Config{})
	db.AutoMigrate(&User{}, &CreditCard{})

	/*
		u := &User{
			Model:    gorm.Model{},
			UserName: "lucas",
			CreditCards: []*CreditCard{
				{Number: "0001", UserID: 1},
				{Number: "0002", UserID: 2},
			},
		}

		db.Create(u)

	*/

    //查找 用户名为 lucas 的所有信用卡信息
	u1 := &User{UserName: "lucas"}

    //Association必须要先查出User才能关联查询对应的CreditCard
	db.First(u1)

	db.Model(u1).Association("CreditCards").Find(&u1.CreditCards)
	fmt.Println(u1)
}

这里只查找了creditcard

[1.513ms] [rows:2] SELECT * FROM `credit_cards` WHERE `credit_cards`.`user_id` = 1 AND `credit_cards`.`deleted_at` IS NULL
associate是先去获取user,再去获取creditcard !!!!!!!!!

var u User
db.Debug().Where("name = ?", "lucas").Find(&u)
fmt.Println(u)
[2.756ms] [rows:1] SELECT * FROM `user` WHERE name = 'lucas'
{1 lucas []}
// 关联查询的结果,保存到user.CreditCard属性

db.Debug().Model(&u).Association("Articles").Find(&u.Articles)
fmt.Println(u)
[0.712ms] [rows:2] SELECT * FROM `article` WHERE `article`.`user_id` = 1
{1 lucas [{1 k8s 1 {0  []}} {2 golang 1 {0  []}}]} 

追加   db.Model().Association("CreditCards").Append


package main

import (
	"encoding/json"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	UserName    string       `json:"username" gorm:"column:username"`
	CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type CreditCard struct {
	gorm.Model
	Number string
	UserID int
}

func main() {
	dsn6 := "root:7PXjAkY!&nlR@tcp(192.168.11.128:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn6), &gorm.Config{})
	db.AutoMigrate(&User{}, &CreditCard{})

	u := &User{
		Model:    gorm.Model{},
		UserName: "lucas",
		CreditCards: []CreditCard{
			{Number: "0001", UserID: 0},
			{Number: "0002", UserID: 1},
		},
	}

	db.Create(u)

	u1 := &User{UserName: "lucas"}
	db.First(u1)
	
	db.Model(u1).Association("CreditCards").Find(&u1.CreditCards)
	fmt.Println(u1)

	db.Model(u1).Association("CreditCards").Append([]CreditCard{
		{Number: "0008", UserID: 1},
	})
	fmt.Println(u1)

	strUser, _ := json.Marshal(u1)
	fmt.Println(string(strUser))
}


&{{1 2023-04-16 10:48:54.423 +0800 CST 2023-04-16 14:12:45.825 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} lucas []}

&{{1 2023-04-16 10:48:54.423 +0800 CST 2023-04-16 14:15:51.44 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} lucas [{{17 2023-04-16 14:15:51.445
 +0800 CST 2023-04-16 14:15:51.445 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 0008 1}]}

{"ID":1,"CreatedAt":"2023-04-16T10:48:54.423+08:00","UpdatedAt":"2023-04-16T14:15:51.44+08:00","DeletedAt":null,"username":"lucas","CreditCards":[
{"ID":17,"CreatedAt":"2023-04-16T14:15:51.445+08:00","UpdatedAt":"2023-04-16T14:15:51.445+08:00","DeletedAt":null,"Number":"0008","UserID":1}]} 

一对多Preload 预加载


使用 Preload 方法, 在查询 User 时先去获取 CreditCard 的记录

和上面associate不一样,他们两个最大的区别是执行的顺序是不一样的,associate是先去获取user,再去获取creditcard。

preload是先去获取creditcard,再去获取user。

package main
import (
"encoding/json"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
/*
constraint:OnUpdate:CASCADE 【当User表更新,也会同步给CreditCards】
OnDelete:SET NULL 【当User中数据被删除时,CreditCard关联设置为 NULL,不删除记录】
*/
type User struct {
gorm.Model
Username string `json:"username" gorm:"column:username"`
CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET
NULL;"`
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
func main() {
// 0、连接数据库
dsn := "root:1@tcp(127.0.0.1:3306)/test_db?
charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// 1、预加载: 查找 user 时预加载相关 CreditCards
//users := User{Username: "zhangsan"} // 只查找张三用户的信用卡信息
users := []User{}
db.Preload("CreditCards").Find(&users)
查询结果


[
{
"ID":1,
"username":"zhangsan",
"CreditCards":[
{
"ID":1,
"Number":"0001",
"UserID":1
},
...
]
}
]
	u2 := &User{UserName: "lucas"}
	db.Preload("CreditCards").Find(u2)
	fmt.Println(u2)

可以看到即使username不是外键,它现在的数据是不完整的。那么我依然可以通过username去找到user这条数据。并且可以将creditcard数据也一次性的找出来。

2023/06/12 16:47:34 C:/Users/W10/GolandProjects/day1/gorm/hasmany1.go:42                                                  
[1.726ms] [rows:2] SELECT * FROM `credit_cards` WHERE `credit_cards`.`user_id` = 1 AND `credit_cards`.`deleted_at` IS NULL
                                                                                                                          
2023/06/12 16:47:34 C:/Users/W10/GolandProjects/day1/gorm/hasmany1.go:42                                                  
[2.751ms] [rows:1] SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL

预加载先查了credit card,再去查找user,它会去查两遍。

//preload和association的区别别

//preload会多查询一次,返回的是整个结构体内容,包括属性的结构体//association只查属性的结构体对应的表,返回的是属性的结构体//association支持增删改查,preload仅支持查询//一般情况下,preload用于关联查询,Gssociation用于关联的增删改查

;