GoLong的学习之路(十九)基础工具之GORM(操作数据库)(关联)GORM中最重要的特点!(简化代码)

上回书说到,CRUD的基本操作,这里就必须说一下。在正规的数据操作中,其实我们返还给后端返还给前端的数据,和前端所需要的数据是不一致。

就比如一个注册的操作。前端传给后端就包括但不限于。邮箱,密码,账号,姓名等,后端就会将这些保存起来,但是在登录的操作中只需要账号和密码。也就意味着我们需要经常进行数据的转换。

在GORM中有些方式可以减少我们的操作。

所以这回书就写,如何关联结构体数据库字段

文章目录

  • 预加载
    • Joins预加载
    • 预加载全部
    • 嵌套预加载
    • 条件预加载
    • 自定义预加载SQL
    • 嵌入式预加载(嵌入式结构体)
    • `注意`
  • Belongs To(属于)
    • 重写外键
    • 重写引用
    • 外键约束
  • Has one(一对一)
    • 重写外键
    • 重写引用
    • 多态关联
    • 自引用 Has One
    • 外键约束
  • Has Many(一对多)
    • 重写外键
    • 重写引用
    • 多态关联
    • 自引用 Has Many
    • 外键约束
  • Many To Many(多对多)
    • 反向引用
      • 声明
      • 检索
    • 重写外键
    • 自引用 Many2Many
    • 自定义连接表
    • 外键约束
    • 复合主键
    • 复合外键

预加载

什么是是预加载?
预加载其实就是在真正开始使用数据之前,先异步把数据加载好,等到需要使用时,就可以直接使用之前加载好的数据。

在GORM中其实就是通过预加载将联系进行加载建表。

type User struct {
  gorm.Model
  Username string
  Orders   []Order
}

type Order struct {
  gorm.Model
  UserID uint
  Price  float64
}

// 查找 user 时预加载相关 Order
db.Preload("Orders").Find(&users)
// SELECT * FROM users;
db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one
// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to

Joins预加载

Preload在单独的查询中加载关联数据,Join Preload将使用左连接加载关联数据。

db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users)
// SELECT `users`.`id`,`users`.`name`,`users`.`age`,`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `users` LEFT JOIN `companies` AS `Company` ON `users`.`company_id` = `Company`.`id` AND `Company`.`alive` = true;

联接嵌套模型

db.Joins("Manager").Joins("Manager.Company").Find(&users)
// SELECT "users"."id","users"."created_at","users"."updated_at","users"."deleted_at","users"."name","users"."age","users"."birthday","users"."company_id","users"."manager_id","users"."active","Manager"."id" AS "Manager__id","Manager"."created_at" AS "Manager__created_at","Manager"."updated_at" AS "Manager__updated_at","Manager"."deleted_at" AS "Manager__deleted_at","Manager"."name" AS "Manager__name","Manager"."age" AS "Manager__age","Manager"."birthday" AS "Manager__birthday","Manager"."company_id" AS "Manager__company_id","Manager"."manager_id" AS "Manager__manager_id","Manager"."active" AS "Manager__active","Manager__Company"."id" AS "Manager__Company__id","Manager__Company"."name" AS "Manager__Company__name" FROM "users" LEFT JOIN "users" "Manager" ON "users"."manager_id" = "Manager"."id" AND "Manager"."deleted_at" IS NULL LEFT JOIN "companies" "Manager__Company" ON "Manager"."company_id" = "Manager__Company"."id" WHERE "users"."deleted_at" IS NULL

预加载全部

clause.Associations关联可以像Select一样使用Preload,您可以使用它来预加载所有关联

type User struct {
  gorm.Model
  Name       string
  CompanyID  uint
  Company    Company
  Role       Role
  Orders     []Order
}

db.Preload(clause.Associations).Find(&users)

clause.Associations不会预加载嵌套关联,但可以将它与嵌套预加载一起使用

嵌套预加载

GORM支持嵌套预加载

db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users)

// 自定义“订单”的预加载条件
// 并且GORM不会预加载不匹配的订单的OrderItems
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)

条件预加载

GORM允许预加载与条件关联,它的工作原理类似于内联条件。
带条件的预加载订单

db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled');

db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users WHERE state = 'active';
// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');

自定义预加载SQL

可以通过传入func(db *gorm.DB) *gorm.DB来定制预加载SQL

db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
  return db.Order("orders.amount DESC")
}).Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC;

嵌入式预加载(嵌入式结构体)

嵌入式预加载用于嵌入式结构体,特别是同一结构体。嵌入式预加载的语法类似于嵌套预加载,它们由点划分。

type Address struct {
    CountryID int
    Country   Country
}

type Org struct {
    PostalAddress   Address `gorm:"embedded;embeddedPrefix:postal_address_"`
    VisitingAddress Address `gorm:"embedded;embeddedPrefix:visiting_address_"`
    Address         struct {
        ID int
        Address
    }
}

//  Org.Address and Org.Address.Country
db.Preload("Address.Country")  // "Address" is has_one, "Country" is belongs_to (nested association)

// Only preload Org.VisitingAddress
db.Preload("PostalAddress.Country") // "PostalAddress.Country" is belongs_to (embedded association)

// Only preload Org.NestedAddress
db.Preload("NestedAddress.Address.Country") // "NestedAddress.Address.Country" is belongs_to (embedded association)

// All preloaded include "Address" but exclude "Address.Country", because it won't preload nested associations.
db.Preload(clause.Associations)

在没有歧义的情况下,可以省略嵌入部分(基本够用)

type Address struct {
    CountryID int
    Country   Country
}

type Org struct {
    Address Address `gorm:"embedded"`
}

db.Preload("Address.Country")
db.Preload("Country") // omit "Address" because there is no ambiguity

注意

嵌入式预加载只适用于归属关系。其他关系的值在数据库中是相同的,我们无法区分它们。

只有预加载才能将下面的关系加载出来。


Belongs To(属于)

应用包含 user 和 company,并且每个 user 能且只能被分配给一个 company。

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

// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
  gorm.Model
  Name      string
  CompanyID int
  Company   Company
}

type Company struct {
  ID   int
  Name string
}

重写外键

要定义一个 belongs to 关系,数据库的表中必须存在外键。默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字

定义一个User实体属于Company实体,那么外键的名字一般使用CompanyID。

例子特殊化,方便理解

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

type Company struct {
  ID   int
  Name string
}

重写引用

对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。

如果设置了User实体属于Company实体,那么GORM会自动把Company中的ID属性保存到User的CompanyID属性中。

但是如果不想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
}

如果外键名恰好在拥有者类型中存在,GORM 通常会错误的认为它是 has one 关系。我们需要在 belongs to 关系中指定 references

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

type Company struct {
  CompanyID   int
  Code        string
  Name        string
}

外键约束

通过 constraint 标签配置 OnUpdate、OnDelete 实现外键约束,在使用 GORM 进行迁移时它会被创建。

type User struct {
  gorm.Model
  Name      string
  CompanyID int
  Company   Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type Company struct {
  ID   int
  Name string
}

Has one(一对一)

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

场景:应用包含 user 和 credit card 模型,且每个 user 只能有一张 credit card

// User 有一张 CreditCard,UserID 是外键
type User struct {
  gorm.Model
  CreditCard CreditCard
}

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

如何做?

// 检索用户列表并预加载信用卡
func GetAll(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.Model(&User{}).Preload("CreditCard").Find(&users).Error
    return users, err
}

重写外键

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

这个字段的名称通常由 has one 模型的类型加上其 主键 生成,对于上面的例子,它是 UserID

为 user 添加 credit card 时,它会将 user 的 ID 保存到自己的 UserID 字段。

如果不想通过每个结构体中的实体主键来建立关系。可以通过foreignKey 来更改他。

type User struct {
  gorm.Model
  CreditCard CreditCard `gorm:"foreignKey:UserName"` // 使用 UserName 作为外键
}

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

重写引用

默认情况下,拥有者实体会将 has one 对应模型的主键保存为外键,但是可以修改它,用另一个字段来保存,例如下面这个使用 Name 来保存的例子。

可以使用标签 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
}

多态关联

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

type Cat struct {
  ID    int
  Name  string
  Toy   Toy `gorm:"polymorphic:Owner;"`
}

type Dog struct {
  ID   int
  Name string
  Toy  Toy `gorm:"polymorphic:Owner;"`
}

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

db.Create(&Dog{Name: "dog1", Toy: Toy{Name: "toy1"}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","dogs")

使用标签 polymorphicValue 来更改多态类型的值

type Dog struct {
  ID   int
  Name string
  Toy  Toy `gorm:"polymorphic:Owner;polymorphicValue:master"`
}

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

db.Create(&Dog{Name: "dog1", Toy: Toy{Name: "toy1"}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","master")

自引用 Has One

type User struct {
  gorm.Model
  Name      string
  ManagerID *uint
  Manager   *User
}

外键约束

你可以通过为标签 constraint 配置 OnUpdateOnDelete 实现外键约束,在使用 GORM 进行迁移时它会被创建。

type User struct {
  gorm.Model
  CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

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

你也可以在删除记录时通过 Select 来删除关联的记录。后面会讲


Has Many(一对多)

has many 与另一个模型建立了一对多的连接

不同于 has one,拥有者可以有零或多个关联模型。

例子:应用包含 user 和 credit card 模型,且每个 user 可以有多张 credit card

// User 有多张 CreditCard,UserID 是外键
type User struct {
  gorm.Model
  CreditCards []CreditCard
}

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

如何解决

// 检索用户列表并预加载信用卡
func GetAll(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.Model(&User{}).Preload("CreditCards").Find(&users).Error
    return users, err
}

重写外键

要定义 has many 关系,同样必须存在外键。

默认的外键名是拥有者的类型名加上其主键字段名。

要定义一个属于 User 的模型,则其外键应该是 UserID。此外,想要使用另一个字段作为外键,您可以使用 foreignKey 标签自定义。

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

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

重写引用

GORM 通常使用拥有者的主键作为外键的值。对于上面的例子,它是 User 的 ID 字段。

为 user 添加credit card时,GORM 会将 user 的 ID 字段保存到 credit cardUserID 字段,使用标签 references 来更改它。

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

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

多态关联

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
}

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")

polymorphicValue 来更改多态类型的值

type Dog struct {
  ID   int
  Name string
  Toys []Toy `gorm:"polymorphic:Owner;polymorphicValue:master"`
}

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

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","master"), ("toy2","1","master")

自引用 Has Many

有时候需要我们进行结构体的自己引用自己

type User struct {
  gorm.Model
  Name      string
  ManagerID *uint
  Team      []User `gorm:"foreignkey:ManagerID"`
}

外键约束

你可以通过为标签 constraint 配置 OnUpdateOnDelete 实现外键约束,在使用 GORM 进行迁移时它会被创建.

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

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

在删除记录时通过 Select 来删除 has many 关联的记录


Many To Many(多对多)

Many to Many 会在两个 model 中添加一张连接表。其实在sql中这种操作非常常见,两张没有实际联系的表,虽然有逻辑的联系,但是没有实际的联系,此时就需要一个记录了两个主键的值的表,做中间表来处理。

在GORM中就不需要去我们自己去创建链接表。可以自己建立链接表。

应用包含了 user 和 language,且一个 user 可以说多种 language,多个 user 也可以说一种 language。

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

type Language struct {
  gorm.Model
  Name string
}

反向引用

声明

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;"`
}

检索

检索 User 列表并预加载 Language

func GetAllUsers(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.Model(&User{}).Preload("Languages").Find(&users).Error
    return users, err
}

检索 Language 列表并预加载 User

func GetAllLanguages(db *gorm.DB) ([]Language, error) {
    var languages []Language
    err := db.Model(&Language{}).Preload("Users").Find(&languages).Error
    return languages, err
}

重写外键

对于 many to many 关系,连接表会同时拥有两个模型的外键

type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
}

连接表:user_languages
foreign key: user_id, reference: users.id
foreign key: language_id, reference: languages.id

若要重写它们,可以使用标签 foreignKeyreferencesjoinforeignKeyjoinReferences不需要使用全部的标签,可以仅使用其中的一个重写部分的外键、引用。

type User struct {
    gorm.Model
    Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer;joinForeignKey:UserReferID;References:UserRefer;joinReferences:ProfileRefer"`
    Refer    uint      `gorm:"index:,unique"`
}

type Profile struct {
    gorm.Model
    Name      string
    UserRefer uint `gorm:"index:,unique"`
}


会创建连接表:user_profiles
foreign key: user_refer_id, reference: users.refer
foreign key: profile_refer, reference: profiles.user_refer

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

什么是迁移?
在设计数据表时,不是一下子就设计好的,而是先设计一个能用的数据表,然后再慢慢的往里面增加东西。 当我们往表里面增加东西时,比如说增加一个字段,那么之前的数据怎么办? 这时候就要用到数据库迁移了,在数据表进行更新时,可以利用数据库迁移脚本在当前数据表进行更新,而不是重新创建一个数据表。

自引用 Many2Many

自引用 many2many 关系

type User struct {
  gorm.Model
    Friends []*User `gorm:"many2many:user_friends"`
}

会创建连接表:user_friends
foreign key: user_id, reference: users.id
foreign key: friend_id, reference: users.id

自定义连接表

JoinTable可以是一个全功能的模型,如有软删除,钩子支持和更多的字段,你可以设置它与SetupJoinTable

自定义连接表要求外键是复合主键或复合唯一索引

type Person struct {
  ID        int
  Name      string
  Addresses []Address `gorm:"many2many:person_addressses;"`
}

type Address struct {
  ID   uint
  Name string
}

type PersonAddress struct {
  PersonID  int `gorm:"primaryKey"`
  AddressID int `gorm:"primaryKey"`
  CreatedAt time.Time
  DeletedAt gorm.DeletedAt
}

func (PersonAddress) BeforeCreate(db *gorm.DB) error {
  // ...
}

err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})

修改 PersonAddresses 字段的连接表为 PersonAddress
PersonAddress 必须定义好所需的外键,否则会报错。

外键约束

type User struct {
  gorm.Model
  Languages []Language `gorm:"many2many:user_speaks;"`
}

type Language struct {
  Code string `gorm:"primarykey"`
  Name string
}

// CREATE TABLE `user_speaks` (`user_id` integer,`language_code` text,PRIMARY KEY (`user_id`,`language_code`),CONSTRAINT `fk_user_speaks_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,CONSTRAINT `fk_user_speaks_language` FOREIGN KEY (`language_code`) REFERENCES `languages`(`code`) ON DELETE SET NULL ON UPDATE CASCADE);

可以在删除记录时通过 Select 来删除 many2many 关系的记录。

复合主键

通过将多个字段设为主键,以创建复合主键。

type Product struct {
  ID           string `gorm:"primaryKey"`
  LanguageCode string `gorm:"primaryKey"`
  Code         string
  Name         string
}

复合外键

模型使用了 复合主键,GORM 会默认启用复合外键。

type Tag struct {
  ID     uint   `gorm:"primaryKey"`
  Locale string `gorm:"primaryKey"`
  Value  string
}

type Blog struct {
  ID         uint   `gorm:"primaryKey"`
  Locale     string `gorm:"primaryKey"`
  Subject    string
  Body       string
  Tags       []Tag `gorm:"many2many:blog_tags;"`
  LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"`
  SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"`
}

// 连接表:blog_tags
// foreign key: blog_id, reference: blogs.id
// foreign key: blog_locale, reference: blogs.locale
// foreign key: tag_id, reference: tags.id
// foreign key: tag_locale, reference: tags.locale

// 连接表:locale_blog_tags
// foreign key: blog_id, reference: blogs.id
// foreign key: blog_locale, reference: blogs.locale
// foreign key: tag_id, reference: tags.id

// 连接表:shared_blog_tags
// foreign key: blog_id, reference: blogs.id
// foreign key: tag_id, reference: tags.id

如果你不用这种方式去键连接表。我认为你会炸裂的。至少我会崩溃。

你可能感兴趣的:(GoLong,学习,数据库,状态模式,golang,gin)