Go语言之GORM框架(四)——预加载,关联标签与多态关联,自定义数据类型与事务(完结篇)

简介: Go语言之GORM框架(四)——预加载,关联标签与多态关联,自定义数据类型与事务(完结篇)

前言

本来是想着写多表关系的,不过写了一半发现重复的部分太多了,想了想与其做一些重复性工作,不如把一些当时觉得抽象的东西记录一下,就当用一篇杂记完成专栏的最后一篇文章吧。

预加载

简单示例

预加载主要用于在多表关系中加载关联表的信息,在讲解预加载的类型之前我们先来看一个预加载的示例:

  • 相关表结构
type User struct {
  gorm.Model
  Name      string
  Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
  gorm.Model
  Name  string
  Users []User `gorm:"many2many:user_languages;"`
}

我们尝试往里面插入数据:

// 创建语言对象
  languages := []Language{
    {Name: "Golang"},
    {Name: "Python"},
    {Name: "Java"},
  }
  // 创建用户对象
  users := []User{
    {Name: "Alice", Languages: []Language{languages[0], languages[1]}},   // Alice 会说 Golang 和 Python
    {Name: "Bob", Languages: []Language{languages[1], languages[2]}},     // Bob 会说 Python 和 Java
    {Name: "Charlie", Languages: []Language{languages[0], languages[2]}}, // Charlie 会说 Golang 和 Java
  }
  // 将语言和用户数据插入到数据库中
  for _, lang := range languages {
    db.Create(&lang)
  }
  for _, user := range users {
    db.Create(&user)
  }

然后我们尝试利用预加载来查询:

users := []User{}
  db.Preload("Languages").Find(&users)
  fmt.Println(users

这样我们不仅能搜寻到user,还能把user相关联的Languages打印出来,像这样:

Joins预加载

Joins预加载会使用left join加载关联数据,与其说是预加载其实更像一个关联查询,常用与ONE TO ONEBelongs To的多表关系中:

  • 表结构:
type Student struct {
  ID   uint        `gorm:"size:8"`
  Name string      `gorm:"size:20"`
  Info StudentInfo `gorm:"foreignKey:StudentID"` // 明确指定关联关系
}
type StudentInfo struct {
  ID        uint `gorm:"size:8"`
  Age       int  `gorm:"size:4"`
  Sex       bool `gorm:"size:4"`
  Email     *string
  StudentID uint `gorm:"size:8"`
}
  • 示例:
var student Student
  db.Joins("Info").Take(&student)
  db.Joins("Info", db.Where("Info.Age = ?", 18)) //带条件的Joins
  fmt.Println(student)

条件预加载

var student Student
  db.Where("age > ?", 18).Preload("Info").First(&student)  //方式一
  db.Preload("Info", "age > ?", 18).Find(&student)   //方式二
  fmt.Println(student)

自定义预加载

var student Student
  db.Preload("Info", func(db *gorm.DB) *gorm.DB{
    return db.Where("age > ?", 18)
  }).Find(&student)
  fmt.Println(student)

嵌套预加载

这里我们来看一下官方给的示例:

// Customize Preload conditions for `Orders`
// And GORM won't preload unmatched order's OrderItems then
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)

这段代码的意思是,在加载用户信息时,只预加载订单状态为 “paid” 的订单数据,并且同时预加载这些订单的订单项信息。这样做可以确保在查询用户数据时,只加载特定状态的订单及其订单项数据,而不会加载其他状态的订单信息。

#关联标签与多态关联

多态关联

关于多态关联我们先来看一个实例:

package main
import (
  "gorm.io/gorm"
  "gorm/ConnectDB"
)
type Boy struct {
  gorm.Model
  Name string
  Toys []Toy `gorm:"polymorphic:Owner"`
}
type Girl struct {
  gorm.Model
  Name string
  Toys []Toy `gorm:"polymorphic:Owner"`
}
type Toy struct {
  gorm.Model
  Name      string
  OwnerID   uint
  OwnerType string
}
func main() {
  db, err := ConnectDB.Connect()
  if err != nil {
    panic(err)
  }
  err = db.AutoMigrate(&Boy{}, &Girl{}, &Toy{})
  if err != nil {
    panic(err)
  }
  db.Create(&Boy{
    Name: "张三",
    Toys: []Toy{
      {Name: "玩具1"},
      {Name: "玩具2"},
    },
  })
  db.Create(&Girl{
    Name: "三玖",
    Toys: []Toy{
      {Name: "玩具3"},
      {Name: "玩具4"},
    },
  })
}

它创建出来的表:

我们可以看到在toys表中我们仅仅用owner_typeowner_id就完成了对boysgils表的区分,避免了不必要的麻烦

补充

可以使用标签 polymorphicValue 来更改多态类型的值,像下面这样:

type Boy struct {
  gorm.Model
  Name string
  Toys []Toy `gorm:"polymorphic:Owner;polymorphicValue:bbbb"`
}
type Girl struct {
  gorm.Model
  Name string
  Toys []Toy `gorm:"polymorphic:Owner;polymorphicValue:gggg"`
}
type Toy struct {
  gorm.Model
  Name      string
  OwnerID   uint
  OwnerType string
}

创建出来的表:

关联标签

前言

关联标签这里我们主要介绍四个:

  • foreignKey:
  • references :
  • joinForeignKey
  • joinReferences

foreignKey与references

这里我们用一对多的多表关系来解释

type Boy struct {
  gorm.Model
  Name string
  Toys []Toy `gorm:"polymorphic:Owner;foreign:Name;references:BoyName"`
}
type Toy struct {
  gorm.Model
  ToyName   string
  BoyName   string
  OwnerID   uint
  OwnerType string
}

如上面所示:

foreignKey:用来指定连接表的外键。

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

joinForeignKey与joinReferences

joinForeignKey:指定Many to Many产生的连接表中关联外键映射字段的名称。

joinReferences:指定Many to Many产生的连接表中关联外键字段的名称。

这里的演示我们用多对多的多表关系来演示:

package main
import (
  "gorm.io/gorm"
  "gorm/ConnectDB"
)
type Girl struct {
  gorm.Model
  ToyName string
  Name    string
  Toys    []Toy `gorm:"many2many:girls_toys;foreign:ToyName;joinForeignKey:a;joinReferences:b"`
}
type Toy struct {
  gorm.Model
  ToyName string
}
func main() {
  db, err := ConnectDB.Connect()
  if err != nil {
    panic(err)
  }
  err = db.AutoMigrate(&Girl{}, &Toy{})
  if err != nil {
    panic(err)
  }
  db.Create(&Girl{
    Name: "三玖",
    Toys: []Toy{
      {ToyName: "玩具3"},
      {ToyName: "玩具4"},
    },
  })
}

它创建出来的连接表是这样的:

用通俗的方式来说,其实它们的作用就是决定了连接表的列名。

自定义数据类型

前言

GORM中允许我们去使用自定义的数据类型,但是我们必须要实现ScannerValue接口,以便让GORM知道如何接收并保存该类型到数据库中。

自定义结构体

package main
import (
  "database/sql/driver"
  "encoding/json"
  "errors"
  "fmt"
  "gorm/ConnectDB"
)
type User struct {
  ID   uint
  Info UserInfo
}
type UserInfo struct {
  Name string
  Age  int
}
func (u *UserInfo) Scan(value interface{}) error {
  bytes, ok := value.([]byte)
  if !ok {
    return errors.New(fmt.Sprintf("Scan failed: %v", value))
  }
  info := UserInfo{}
  err := json.Unmarshal(bytes, &info)
  *u = info
  return err
}
func (u UserInfo) Value() (driver.Value, error) {
  return json.Marshal(u)
}
func main() {
  db, err := ConnectDB.Connect()
  if err != nil {
    fmt.Println("数据库连接失败,err:", err)
    return
  }
  err = db.AutoMigrate(&User{})
  if err != nil {
    fmt.Println("表创建失败,err:", err)
    return
  }
  user := User{
    Info: UserInfo{
      Name: "张三",
      Age:  18,
    },
  }
  db.Create(&user)
  db.First(&user)
  fmt.Println(user)
}

自定义数组

func (a *Args) Scan(value interface{}) error {
  str, ok := value.([]byte)
  if !ok {
    return errors.New(fmt.Sprintf("Scan failed: %v", value))
  }
  *a = strings.Split(string(str), ",")
  return nil
}
func (a Args) Value() (driver.Value, error) {
  if len(a) > 0 {
    var str string
    str = a[0]
    for i := 1; i < len(a); i++ {
      str += "," + a[i]
    }
    return str, nil
  }
  return "", nil
}

事务

前言

事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。很形象的一个例子,张三给李四转账100元,在程序里面,张三的余额就要-100,李四的余额就要+100 整个事件是一个整体,哪一步错了,整个事件都是失败的

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

相关表结构

我们这里相关表结构

type User struct {
  ID    uint   `json:"id"`
  Name  string `json:"name"`
  Money int    `json:"money"`
}

事务的使用

现在有一个场景:,张三给李四转账100元,在程序里面,张三的余额就要-100,李四的余额就要+100

如果不使用事务,是这样的:

var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 先给张三-100
zhangsan.Money -= 100
DB.Model(&zhangsan).Update("money", zhangsan.Money)
// 再给李四+100
lisi.Money += 100
DB.Model(&lisi).Update("money", lisi.Money)

在失败的情况下,要么张三白白损失了100,要么李四凭空拿到100元这显然是不合逻辑的,并且不合法的,而这就需要我们来使用事务了,事务一共分为两种:

  • 自动事务
  • 手动事务

我们分别来看一下它们的写法:

  • 自动事务:
var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
DB.Transaction(func(tx *gorm.DB) error {
  // 先给张三-100
  zhangsan.Money -= 100
  err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
  if err != nil {
    fmt.Println(err)
    return err
  }
  // 再给李四+100
  lisi.Money += 100
  err = tx.Model(&lisi).Update("money", lisi.Money).Error
  if err != nil {
    fmt.Println(err)
    return err
  }
  // 提交事务
  return nil
})
  • 手动事务:
  • 执行流程:
//开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// ...
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()
var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
tx := DB.Begin()
// 先给张三-100
zhangsan.Money -= 100
err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
if err != nil {
  tx.Rollback()
}
// 再给李四+100
lisi.Money += 100
err = tx.Model(&lisi).Update("money", lisi.Money).Error
if err != nil {
  tx.Rollback()
}
// 提交事务
tx.Commit()

结语

至此,GORM的学习就告一段落了,大家下篇文章见,拜拜!

相关文章
|
16小时前
|
JSON 中间件 Go
Go语言Web框架Gin介绍
【7月更文挑战第19天】Gin是一个功能强大、高性能且易于使用的Go语言Web框架。它提供了路由、中间件、参数绑定等丰富的功能,帮助开发者快速构建高质量的Web应用。通过本文的介绍,你应该对Gin框架有了初步的了解,并能够使用它来开发简单的Web服务。随着你对Gin的深入学习和实践,你将能够利用它构建更复杂、更强大的Web应用。
|
1天前
|
Oracle 关系型数据库 MySQL
|
5天前
|
Cloud Native Java Go
为什么要学习Go语言?
GO logo的核心理念,即简单胜于复杂。使用现代斜体无衬线字体与三条简单的运动线相结合,形成一个类似于快速运动的两个轮子的标记,传达速度和效率。字母的圆形暗示了GO地鼠的眼睛,创造了一个熟悉的形状,让标记和吉祥物很好地搭配在一起。
22 4
|
7天前
|
安全 Go
Go语言map并发安全,互斥锁和读写锁谁更优?
Go并发编程中,`sync.Mutex`提供独占访问,适合读写操作均衡或写操作频繁的场景;`sync.RWMutex`允许多个读取者并行,适用于读多写少的情况。明智选择锁可提升程序性能和稳定性。示例展示了如何在操作map时使用这两种锁。
12 0
|
7天前
|
安全 Go 开发者
Go语言map并发安全使用的正确姿势
在Go并发编程中,由于普通map不是线程安全的,多goroutine访问可能导致数据竞态。为保证安全,可使用`sync.Mutex`封装map或使用从Go 1.9开始提供的`sync.Map`。前者通过加锁手动同步,后者内置并发控制,适用于多goroutine共享。选择哪种取决于具体场景和性能需求。
9 0
|
7天前
|
存储 安全 Java
Go语言中的map为什么默认不是并发安全的?
Go语言的map默认不保证并发安全,以优化性能和简洁性。官方建议在需要时使用`sync.Mutex`保证安全。从Go 1.6起,并发读写map会导致程序崩溃,鼓励开发者显式处理并发问题。这样做的哲学是让代码更清晰,并避免不必要的性能开销。
11 0
|
9天前
|
存储 Go
go语言中fmt格式化包和内置函数汇总
【7月更文挑战第10天】本文介绍fmt包和`Errorf`用于创建格式化的错误消息。`fmt`包还涉及一些接口,如`Formatter`、`GoStringer`、`ScanState`、`Scanner`和`Stringer`,支持自定义格式化和输入/输出处理。
19 1
|
Java Go
go语言多态接口样例
感觉比java玄幻啊~~~ package main import ( "fmt" ) type notifier interface{ notify() } type user struct { name string email string } func (u *user) notify() { fmt.
971 0
|
9天前
|
Go
go语言中格式化输出的占位符
【7月更文挑战第10天】`fmt` 包在 Go 语言中用于格式化输出,包括不同类型的占位符:%v(默认格式)、%+v(带字段名的结构体)、%#v(Go语法表示)、%T(类型表示)、%%(百分号)。布尔值用%t,整数有%b、%c、%d、%o、%q、%x、%X和%U。浮点数和复数用%b、%e、%E、%f、%g、%G。字符串和字节切片用%s、%q、%x、%X。指针用%p。占位符可配合+、-、#、空格和0进行调整。宽度和精度控制输出格式,例如 %.4g 控制小数精度。Go 没有 `%u`,但无符号整数默认打印为正数。运算符包括逻辑、比较、加减、乘除、移位、按位和按位异或等。
19 1
|
9天前
|
JSON 测试技术 Go
零值在go语言和初始化数据
【7月更文挑战第10天】本文介绍在Go语言中如何初始化数据,未初始化的变量会有对应的零值:bool为`false`,int为`0`,byte和string为空,pointer、function、interface及channel为`nil`,slice和map也为`nil`。。本文档作为指南,帮助理解Go的数据结构和正确使用它们。
54 22
零值在go语言和初始化数据