Gorm 的关联查询

背景介绍

gorm 与 mybatis-plus 、hibernate 等 ORM 框架一样,为了应对查询场景居多的现象,支持原生 sql 和 api 两种方式读数据库。

gorm 原生 sql 参见:https://gorm.io/docs/sql_builder.html。

gorm 提供的 api 支持关联插入、关联查询、关联更新、关联删除等功能。这篇文章对各种类型关联查询做以介绍。

关联类型

所谓关联查询就是:在执行查询操作时,如果对象的某个字段也是对象,那就将内部对象也一并查出。以前的做法是先查外围对象,再查内部对象,在内存中组装后返回给前端。

实现关联查询的核心是:依据业务理清楚关联类型以及外键应该放在哪里,在建对象的时候采用标签方式把外键和关联字段格式写正确

gorm 有默认使用类名+ID 作为默认外键,但是不建议这么使用。推荐使用标签指定外键和关联列,看起来清晰明了,出错少。

belong to (多对一)

链接:https://gorm.io/docs/belongs_to.html

使用场景如:一个员工属于一个公司,但一个公司可以有多个员工。在这种场景下,外键在 User 中,关联到 Company 的主键。

// foreignKey 标签显式指定外键。
// references 标签显式指定外键的关联字段。

type User struct {
	gorm.Model
	Name      string
	CompanyID int
	Company   Company `gorm:"foreignKey:CompanyID;references:ID"`
}

type Company struct {
	ID   int
	Name string
}

对应的表信息如下:

CREATE TABLE `users` (
  `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,
  `name` longtext,
  `company_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_users_deleted_at` (`deleted_at`),
  KEY `fk_users_company` (`company_id`),
  CONSTRAINT `fk_users_company` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
-- 外键 company_id 关联到 companies 的 id 字段。

CREATE TABLE `companies` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` longtext,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

关联查询:
Preloading 就是在外围对象时关联上要查的内部对象。至于怎么关联的,全靠外键。


// 在查 user 对象时将其关联的 compancy 信息一并查出来。
db.Model(&User{}).Preload("Company").Find(&result, "name = ?", user.Name)
data, _ := json.Marshal(result)
println(string(data))

// {"ID":1,"CreatedAt":"2023-12-11T16:57:09.452+08:00","UpdatedAt":"2023-12-11T16:57:09.452+08:00","DeletedAt":null,"Name":"amos","CompanyID":12,"Company":{"ID":12,"Name":"google"}}

完整代码:

package main

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

func main() {
	source := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
		"USERNAME", "PASSWORD", "127.0.0.1", 3306, "test")
	sqlDB, _ := sql.Open("mysql", source)
	db, _ := gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})

	// 创建表
	db.AutoMigrate(&User{}, &Company{})

	// 因为已经指定了约束关系,User.companyId 字段会自动被 Company.ID 字段填充。
	user := User{
		Model: gorm.Model{},
		Name:  "amos",
		Company: Company{
			ID:   12,
			Name: "google",
		},
	}
	
	// 关联插入。在插入 user 记录时,也插入 compancy 记录。
	db.Create(&user)

	var result User
	db.Model(&User{}).Preload("Company").Find(&result, "name = ?", user.Name)
	data, _ := json.Marshal(result)
	println(string(data))
}

type User struct {
	gorm.Model
	Name      string
	CompanyID int
	Company   Company `gorm:"foreignKey:CompanyID;references:ID"`
}

type Company struct {
	ID   int
	Name string
}

has one (一对一)

链接:https://gorm.io/docs/has_one.html

使用场景如:一个用户只能办理一张信用卡。此时外键写法比较随意,外键既可以放在 User 中,也可以放在 CreditCard 中。

// 外键放在 CreditCard 中。
type User struct {
	gorm.Model
	CreditCard   CreditCard `gorm:"foreignKey:UserID;references:ID"`
}

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

// --------------------------------------

// 外键放在 User 中。
type User struct {
	gorm.Model
	CreditCardId uint
	CreditCard   CreditCard `gorm:"foreignKey:CreditCardId;references:ID"`
}

type CreditCard struct {
	gorm.Model
	Number string
}

表:

--- 外键放在 CreditCard 中。
CREATE TABLE `credit_cards` (
  `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,
  `number` longtext,
  `user_id` bigint unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_credit_cards_deleted_at` (`deleted_at`),
  KEY `fk_users_credit_card` (`user_id`),
  CONSTRAINT `fk_users_credit_card` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

------------------------------------------------------------------------------------

--- 外键放在 User 中。
CREATE TABLE `users` (
  `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,
  `credit_card_id` bigint unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_users_deleted_at` (`deleted_at`),
  KEY `fk_users_credit_card` (`credit_card_id`),
  CONSTRAINT `fk_users_credit_card` FOREIGN KEY (`credit_card_id`) REFERENCES `credit_cards` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

关联查询:

user := User{
		Model: gorm.Model{ID: 1},
		CreditCard: CreditCard{
			Number: "374738728629",
		},
	}

	db.Create(&user)

	var result User
	db.Model(&User{}).Preload("CreditCard").Find(&result, "ID = ?", user.ID)
	data, _ := json.Marshal(result)
	println(string(data))

/*
{
    "ID":1,
    "CreatedAt":"2023-12-11T17:37:03.935+08:00",
    "UpdatedAt":"2023-12-11T17:37:03.935+08:00",
    "DeletedAt":null,
    "CreditCard":{
        "ID":1,
        "CreatedAt":"2023-12-11T17:37:03.936+08:00",
        "UpdatedAt":"2023-12-11T17:37:03.936+08:00",
        "DeletedAt":null,
        "Number":"374738728629",
        "UserID":1
    }
}
*/

has many (一对多)

参考链接:https://gorm.io/docs/has_many.html

使用场景如:一个用户有持有多张信用卡。外键在 CreditCard 中。

type User struct {
  gorm.Model
  MemberNumber string
  CreditCards  []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}

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

关联查询

owner := User{
		Model:        gorm.Model{},
		MemberNumber: "2",
		CreditCards: []CreditCard{
			{
				Model:  gorm.Model{},
				Number: "1",
			},
			{
				Model:  gorm.Model{},
				Number: "2",
			},
		},
	}

	db.Create(&owner)

	var result User
	db.Model(&User{}).Preload("CreditCards").Find(&result, "member_number = ?", "2")
	data, _ := json.Marshal(result)
	println(string(data))

/*
{
    "ID":1,
    "CreatedAt":"2023-12-11T19:45:48.533+08:00",
    "UpdatedAt":"2023-12-11T19:45:48.533+08:00",
    "DeletedAt":null,
    "MemberNumber":"2",
    "CreditCards":[
        {
            "ID":1,
            "CreatedAt":"2023-12-11T19:45:48.534+08:00",
            "UpdatedAt":"2023-12-11T19:45:48.534+08:00",
            "DeletedAt":null,
            "Number":"1",
            "UserNumber":"2"
        },
        {
            "ID":2,
            "CreatedAt":"2023-12-11T19:45:48.534+08:00",
            "UpdatedAt":"2023-12-11T19:45:48.534+08:00",
            "DeletedAt":null,
            "Number":"2",
            "UserNumber":"2"
        }
    ]
}
*/

many to many (多对多)

链接:https://gorm.io/docs/many_to_many.html

使用场景如:一个学生可以选多门课,一门课也可以被多个学生选择。在该场景下,学生和课程的主键必须存在,因为 gorm 还要再生成一张关联表。

// many2many 标签表示多对多关联关系
// Student2Course 是生成的关联表名字。
// Student 和 Course 各自的主键必须存在,最好显式指定。

type Student struct {
	SId  int `gorm:"primary_key"`
	SNo  int
	Name string
	Sex  string
	Age  int
	Course []Course `gorm:"many2many:Student2Course"`
}

type Course struct {
	CId         int `gorm:"primary_key"`
	CName       string
	TeacherName string
	Room        string
}

CREATE TABLE `students` (
  `s_id` bigint NOT NULL AUTO_INCREMENT,
  `s_no` bigint DEFAULT NULL,
  `name` longtext,
  `sex` longtext,
  `age` bigint DEFAULT NULL,
  PRIMARY KEY (`s_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

CREATE TABLE `Courses` (
  `c_id` bigint NOT NULL AUTO_INCREMENT,
  `c_name` longtext,
  `teacher_name` longtext,
  `room` longtext,
  PRIMARY KEY (`c_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

CREATE TABLE `student2_courses` (
  `student_s_id` bigint NOT NULL,
  `course_c_id` bigint NOT NULL,
  PRIMARY KEY (`student_s_id`,`course_c_id`),
  KEY `fk_student2_courses_course` (`course_c_id`),
  CONSTRAINT `fk_student2_courses_course` FOREIGN KEY (`course_c_id`) REFERENCES `courses` (`c_id`),
  CONSTRAINT `fk_student2_courses_student` FOREIGN KEY (`student_s_id`) REFERENCES `students` (`s_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 约束条件都在生成的关联表里。

关联查询:

stu1 := models.Student{
		SNo:  10010,
		Name: "amos",
		Sex:  "male",
		Age:  18,
		Course: []models.Course{
			{
				CName:       "match",
				TeacherName: "Mr.lube",
				Room:        "No1",
			},
			{
				CName:       "Physics",
				TeacherName: "Morris.li",
				Room:        "No2",
			},
		},
	}

	stu2 := models.Student{
		SNo:  10011,
		Name: "sam",
		Sex:  "male",
		Age:  19,
		Course: []models.Course{
			{
				CName:       "match",
				TeacherName: "Mr.lube",
				Room:        "No1",
			},
			{
				CName:       "Physics",
				TeacherName: "Morris.li",
				Room:        "No2",
			},
		},
	}

	db.Create(&stu1)
	db.Create(&stu2)
	
	var result []*models.Student
	db.Model(&models.Student{}).Preload("Course").Find(&result)
	bytes, _ := json.Marshal(result)
	println(string(bytes))

/*
[{"SId":1,"SNo":10010,"Name":"amos","Sex":"male","Age":18,"Course":[{"CId":1,"CName":"match","TeacherName":"Mr.lube","Room":"No1"},{"CId":2,"CName":"Physics","TeacherName":"Morris.li","Room":"No2"}]},{"SId":2,"SNo":10011,"Name":"sam","Sex":"male","Age":19,"Course":[{"CId":3,"CName":"match","TeacherName":"Mr.lube","Room":"No1"},{"CId":4,"CName":"Physics","TeacherName":"Morris.li","Room":"No2"}]}]

*/

Polymorphism Association 多态关联

它主要解决一张表怎么与其他多张表关联的问题。

在社交媒体中有如下应用:

  • 一个用户可以发表评论。
  • 一个图片也可以被评论。

怎么处理评论和用户、博客、图片之间的关系。比如:查用户时怎么关联到他的评论。在比如:如果后期再来个博客的评论,该怎么处理?

/*
1. 
2. polymorphicValue 表示多态类型的值,如果不显式指定,默认是类名。
*/

// 评论
type Comment struct {
    gorm.Model
    Content     string
    OwnerID     uint
    OwnerType   string 
}
// 用户
type person struct {
    gorm.Model
    Name       string
    Comments   []Comment `gorm:"polymorphic:Owner;polymorphicValue:user"`
}
// 图片
type Image struct {
    gorm.Model
~~~~    URL        string
    Comments   []Comment `gorm:"polymorphic:Owner;polymorphicValue:image"`
}
  1. polymorphic:Owner:polymorphic 是多态关键字,Owner 表示“所有者”,会将“所有者”的 ID 填充到 Comment.OwnerID 字段上。
  2. polymorphicValue:user:polymorphicValue 表示多态类型,user 表示类型的值,会将 “user” 值填充到 Comment.OwnerType 字段上。如果 polymorphicValue 缺失,则使用类型名填充。

Comments 表
在这里插入图片描述

people 表

在这里插入图片描述

images 表

在这里插入图片描述

关联查询

db.AutoMigrate(&person{}, &Image{}, &Comment{})

	user := person{
		Name: "John",
		Comments: []Comment{
			{Content: "Great post!"},
			{Content: "terrible post!"},
		},
	}

	image := Image{
		Model: gorm.Model{},
		URL:   "https://example.com/image.jpg",
		Comments: []Comment{
			{Content: "Nice image!"},
		},
	}

	db.Create(&user)
	db.Create(&image)

	var fetchedUser person
	db.Model(&person{}).Preload("Comments").Find(&fetchedUser, "ID = ?", user.ID)
	userBytes, _ := json.Marshal(fetchedUser)
	println(string(userBytes))
	// {"ID":1,"CreatedAt":"2023-12-11T21:45:08.457+08:00","UpdatedAt":"2023-12-11T21:45:08.457+08:00","DeletedAt":null,"Name":"John","Comments":[{"ID":1,"CreatedAt":"2023-12-11T21:45:08.458+08:00","UpdatedAt":"2023-12-11T21:45:08.458+08:00","DeletedAt":null,"Content":"Great post!","OwnerID":1,"OwnerType":"user"},{"ID":2,"CreatedAt":"2023-12-11T21:45:08.458+08:00","UpdatedAt":"2023-12-11T21:45:08.458+08:00","DeletedAt":null,"Content":"terrible post!","OwnerID":1,"OwnerType":"user"}]}

	var fetchedImage Image
	db.Model(&Image{}).Preload("Comments").Find(&fetchedImage, "ID = ?", image.ID)
	imageBytes, _ := json.Marshal(fetchedImage)
	println(string(imageBytes))
	// {"ID":1,"CreatedAt":"2023-12-11T21:45:08.459+08:00","UpdatedAt":"2023-12-11T21:45:08.459+08:00","DeletedAt":null,"URL":"https://example.com/image.jpg","Comments":[{"ID":3,"CreatedAt":"2023-12-11T21:45:08.459+08:00","UpdatedAt":"2023-12-11T21:45:08.459+08:00","DeletedAt":null,"Content":"Nice image!","OwnerID":1,"OwnerType":"image"}]}

如果后期来个视频的评论怎么处理?

创建新对象就可以,不用像以前一样新增列。

type Post struct {
    gorm.Model
    Title    string
    Content  string
    Comments []Comment `gorm:"polymorphic:Owner;polymorphicValue:post"`
}

异常现象

官网给的代码有 bug,有时候不能直接使用,可能是版本不一致。

异常 1:

BLOB/TEXT column 'user_number' used in key specification without a key len

解决办法:

type CreditCard struct {
	gorm.Model
	Number     string
	UserNumber string `gorm:"size:191"`
}

异常 2:

Failed to add the foreign key constraint. Missing index for constraint 'fk_card_owners_credit_cards' in the referenced table 'card_owners'

解决办法:

关联字段必须有索引。

type CardOwner struct {
	gorm.Model
	MemberNumber string       `gorm:"index"` # 添加索引
	CreditCards  []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}

你可能感兴趣的:(golang,gorm,preloading,关联查询,Polymorphism)