GORM框架实践:从连接MySQL到删库跑路 | 青训营

引言

在开发软件应用时,我们经常需要与数据库进行交互,存储和查询数据。然而,数据库中的数据通常是以表格的形式组织的,而我们在编程语言中使用的数据结构则是以对象的形式表示的。这就导致了一个问题:如何在两种不同的数据表示之间进行转换和映射?这就是 ORM(对象关系映射)要解决的问题。

ORM,全称为"对象关系映射",是一种编程技术,旨在将关系型数据库中的表和数据映射到面向对象的编程语言中。通过ORM,开发者可以使用类和对象来代表数据库中的表和记录,从而在代码中实现与数据库的交互。这种抽象层级的使用方式有助于降低开发复杂度,加速开发速度,并且在一定程度上提升了代码的可维护性。

GORM 是针对 Go 编程语言的一款流行的 ORM 框架,它在开发人员中间得到了广泛的认可和使用。作为一个功能丰富且强大的库,GORM 提供了一系列的工具和函数,使得在 Go 项目中执行数据库操作变得异常简单。支持多种数据库后端,如 MySQL、PostgreSQL、SQLite 等。GORM 具有以下特点:

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus...
  • 每个特性都经过了测试的重重考验
  • 开发者友好

本文将介绍如何使用 GORM 框架进行 MySQL 数据库的操作,从连接数据库到删库跑路(当然这只是个玩笑,请不要真的删库跑路)。我们将通过实例演示如何定义数据模型、执行 CRUD(增删改查)操作、管理事务、优化性能等。

准备工作

安装 GORM 库及其依赖

在项目根目录下,执行以下命令

bash 复制代码
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

配置 MySQL 数据库连接

请参考MySQL 8.0 文档

然后创建数据库 test

sql 复制代码
create database test default character set utf8mb4 collate utf8mb4_unicode_ci;

测试代码

go 复制代码
package main

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

type MySQLConfig struct {
    Host     string
    Port     string
    User     string
    Password string
    DBName   string
    Opts     string
}

type User struct {
    gorm.Model
    Email    string `gorm:"type:varchar(128);uniqueIndex;not null;comment:邮箱"`
    Name     string `gorm:"type:varchar(32);not null;comment:姓名"`
    Password string `gorm:"type:varchar(128);not null;comment:密码"`
}

func main() {
    dbConfig := MySQLConfig{
        Host:     "127.0.0.1",
        Port:     "3306",
        User:     "root",
        Password: "HimeHina6923.",
        DBName:   "test",
        Opts:     "charset=utf8mb4&parseTime=True&loc=Local",
    }
    // 连接到 MySQL 数据库
    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?%s",
        dbConfig.User,
        dbConfig.Password,
        dbConfig.Host,
        dbConfig.Port,
        dbConfig.DBName,
        dbConfig.Opts,
    )
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // 自动迁移
    err = db.AutoMigrate(&User{})
    if err != nil {
        log.Panicln("迁移用户表失败", err)
    }

    // 创建
    user := User{
        Email:    "hh@lara.com",
        Name:     "hh",
        Password: "123456",
    }
    result := db.Create(&user)
    if result.Error != nil {
        log.Panicln("创建用户失败", result.Error)
    }
    log.Println("创建用户成功", user)

    // 查询
    var users []User
    result = db.Find(&users)
    if result.Error != nil {
        log.Panicln("查询用户失败", result.Error)
    }
    log.Println("查询用户成功", users)

    // 更新
    result = db.Model(&users[0]).Update("name", "hh")
    if result.Error != nil {
        log.Panicln("更新用户失败", result.Error)
    }
    log.Println("更新用户成功", users[0])

    // 删除
    result = db.Delete(&users[0])
    if result.Error != nil {
        log.Panicln("删除用户失败", result.Error)
    }
    log.Println("删除用户成功", users[0])
}

以上代码示例已经展示了 GORM 的基本操作。

声明数据模型

在使用 GORM 框架进行数据库操作之前,我们需要声明数据模型,即用 Go 语言中的结构体(struct)来表示数据库中的表和数据。这样,我们就可以利用 GORM 的 ORM 功能,将结构体和数据库表进行自动的映射和转换。

模型是标准的 struct,由 Go 的基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成。

创建 Go 结构体来映射数据库表

要创建 Go 结构体来映射数据库表,我们只需要按照以下几个简单的规则:

  • 结构体的名称应该与数据库表的名称相对应,或者使用 TableName 方法来指定表名
  • 结构体的字段应该与数据库表的列相对应,或者使用标签(tag)来指定列名
  • 结构体的字段类型应该与数据库表的列类型兼容,或者使用标签(tag)来指定列类型
  • 结构体可以包含任意数量的字段,但是必须包含一个主键字段,或者使用标签(tag)来指定主键

之前的代码中,就定义了一个 users 表。

go 复制代码
type User struct {
     // 嵌入 gorm.Model 来自动添加 id, created_at, updated_at, deleted_at 字段
    gorm.Model
    // Email 字段对应 email 列,类型为 varchar(128),并且添加 not null 约束以及 unique 索引
    // uniqueIndex 标签等效于 "index:,unique"
    Email    string `gorm:"type:varchar(128);uniqueIndex;not null;comment:邮箱"`
    Name     string `gorm:"type:varchar(32);not null;comment:姓名"`
    Password string `gorm:"type:varchar(128);not null;comment:密码"`
}

使用标签定义字段名、类型、约束等

在创建 Go 结构体时,我们可以使用标签(tag)来自定义字段和列之间的映射关系,以及一些其他的设置。标签(tag)是一种在 Go 语言中给结构体字段添加元数据的方式,它们通常写在反引号(`)中,并且以键值对的形式表示。GORM 支持多种标签(tag),用于定义字段名、类型、约束、索引、默认值、外键等。下面是一些常用的标签(tag)及其含义:

  • column:指定字段对应的列名
  • type:指定字段对应的列类型
  • serializer:指定将数据序列化或反序列化到数据库中的序列化器
  • size:定义列数据类型的大小或长度
  • scale:指定列大小
  • precision:指定列的精度
  • primary_key:指定字段为主键
  • unique:指定字段为唯一键
  • index:指定字段为普通索引
  • unique_index:指定字段为唯一索引
  • auto_increment:指定字段为自增长
  • default:指定字段的默认值
  • not null:指定字段不允许为空
  • foreign_key:指定字段为外键

想指定更多 tag,请参考字段标签

go 复制代码
// User 结构体对应 users 表
type User struct {
    ID    uint `gorm:"primary_key"` // ID 字段对应 id 列,类型为 uint,并且是主键
    Name  string `gorm:"column:username;size:50;not null"` // Name 字段对应 username 列,类型为 varchar(50),并且不允许为空
    Age   int `gorm:"type:tinyint"` // Age 字段对应 age 列,类型为 tinyint
    Email string `gorm:"unique_index"` // Email 字段对应 email 列,并且是唯一索引
}

添加关联关系(如一对多、多对多)

在数据库中,我们经常需要使用关联关系(association)来表示不同表之间的联系。例如,一个用户可以拥有多个订单,一个订单可以包含多个商品,这就是一对多(one-to-many)和多对多(many-to-many)的关联关系。在 GORM 中,我们可以使用结构体的嵌套来表示这些关联关系,以及使用标签(tag)来指定外键和引用键等信息。

下面是一个示例代码,展示了如何使用 GORM 来定义一对多和多对多的关联关系:

go 复制代码
// User 结构体对应 users 表
type User struct {
    gorm.Model
    Name   string  `gorm:"type:varchar(32);not null;comment:用户名"`
    Email  string  `gorm:"type:varchar(256);uniqueIndex;not null;comment:邮箱"`
    Orders []Order // 一个用户可以拥有多个订单,这是一对多的关联关系
}

// Product 结构体对应 products 表
type Product struct {
    gorm.Model
    Name  string
    Price float64
}

// Order 结构体对应 orders 表
type Order struct {
    gorm.Model
    UserID   uint      // UserID 字段是外键,指向 users 表的 id 列
    User     User      `gorm:"foreignKey:UserID;references:ID;"` // User 字段是引用,表示订单属于哪个用户
    Products []Product `gorm:"many2many:order_products"`         // 一个订单可以包含多个商品,一个商品可以属于多个订单,这是多对多的关联关系,中间表为 order_products
}

CRUD操作

CRUD 是指对数据库中的数据进行增删改查(Create, Read, Update, Delete)的操作。在 GORM 框架中,我们可以使用一些简单而强大的方法来执行这些操作,无需编写复杂的 SQL 语句。在本节中,我们将分别介绍如何使用 GORM 进行增、查、改、删的操作,以及如何处理各种情况和错误。

要使用 GORM 插入新记录到数据库中,我们可以使用Create方法,该方法接受一个结构体或者一个结构体的切片作为参数,然后将其映射到对应的数据库表中。例如,要插入一个新用户到users表中,我们可以这样写:

go 复制代码
// 创建一个新用户
user := User{Name: "Alice", Age: 18, Email: "alice@example.com"}

// 使用Create方法插入新记录
db.Create(&user)

如果插入成功,Create方法会返回一个*gorm.DB类型的对象,该对象包含了一些有用的信息,如影响的行数、主键值、错误等。我们可以通过这个对象来检查插入操作是否成功,以及获取一些其他的信息。例如:

go 复制代码
// 使用Create方法插入新记录,并获取返回值
result := db.Create(&user)

// 检查是否有错误发生
if result.Error != nil {
    // 处理错误
}

// 获取影响的行数
rows := result.RowsAffected

// 获取主键值
id := user.ID

如果插入失败,Create方法会返回一个包含错误信息的对象,我们可以通过检查该对象的Error字段来获取错误原因,并进行相应的处理。例如:

go 复制代码
// 创建一个新用户,但是Email已经存在于数据库中,违反了unique约束
user := User{Name: "Bob", Age: 20, Email: "alice@example.com"}

// 使用Create方法插入新记录,并获取返回值
result := db.Create(&user)

// 检查是否有错误发生
if result.Error != nil {
    // 打印错误信息
    fmt.Println(result.Error)
}

输出:

txt 复制代码
Error 1062: Duplicate entry 'alice@example.com' for key 'email'

除了插入单个记录外,我们还可以使用Create方法批量插入多个记录,只需要将一个结构体的切片作为参数传递即可。例如:

go 复制代码
// 创建多个新用户
users := []User{
    {Name: "Charlie", Age: 22, Email: "charlie@example.com"},
    {Name: "David", Age: 24, Email: "david@example.com"},
}

// 使用Create方法批量插入新记录
db.Create(&users)

如果我们想在插入新记录时同时创建关联数据,我们可以使用嵌套结构体或者标签(tag)来指定关联关系。例如:

go 复制代码
// 创建一个新订单,并同时创建关联的用户和商品数据
order := Order{
    User: User{Name: "Eve", Age: 26, Email: "eve@example.com"}, // 嵌套结构体表示一对一或者多对一的关联关系
    Products: []Product{ // 切片表示一对多或者多对多的关联关系
        {Name: "Book", Price: 10.0},
        {Name: "Pen", Price: 1.0},
    },
}

// 使用Create方法插入新记录,并同时创建关联数据
db.Create(&order)

要使用 GORM 查询数据库中的数据,我们可以使用一系列的方法来执行不同类型和条件的查询,如FindFirstLastTakeWhere等。这些方法都会返回一个*gorm.DB类型的对象,该对象包含了查询结果和错误等信息。我们可以通过该对象来获取和处理查询结果和错误。例如:

go 复制代码
// 查询users表中的所有记录,并将结果存储到一个User类型的切片中
var users []User
result := db.Find(&users)

// 检查是否有错误发生
if result.Error != nil {
    // 处理错误
}

// 获取影响的行数
rows := result.RowsAffected

// 遍历查询结果
for _, user := range users {
    // 处理每个用户的数据
}

如果我们只想查询一条记录,我们可以使用FirstLastTake等方法,这些方法会按照主键或者其他条件来返回第一条或者最后一条记录。例如:

go 复制代码
// 查询users表中主键为1的记录,并将结果存储到一个User类型的变量中
var user User
result := db.First(&user, 1)

// 检查是否有错误发生
if result.Error != nil {
    // 处理错误
}

// 处理查询结果
fmt.Println(user)

如果我们想根据一些条件来查询记录,我们可以使用Where方法,该方法接受一个或多个条件作为参数,然后返回一个新的查询对象,我们可以在该对象上继续调用其他方法来执行查询。例如:

go 复制代码
// 查询users表中年龄大于18且邮箱以.com结尾的记录,并将结果存储到一个User类型的切片中
var users []User
result := db.Where("age > ? AND email LIKE ?", 18, "%.com").Find(&users)

// 检查是否有错误发生
if result.Error != nil {
    // 处理错误
}

// 遍历查询结果
for _, user := range users {
    // 处理每个用户的数据
}

如果我们想查询关联数据,我们可以使用Preload方法,该方法会在执行查询时自动加载关联数据,并将其存储到对应的结构体字段中。例如:

go 复制代码
// 查询orders表中的所有记录,并同时加载关联的用户和商品数据,并将结果存储到一个Order类型的切片中
var orders []Order
result := db.Preload("User").Preload("Products").Find(&orders)

// 检查是否有错误发生
if result.Error != nil {
    // 处理错误
}

// 遍历查询结果
for _, order := range orders {
    // 处理每个订单的数据,以及关联的用户和商品数据
}

想使用更高级的查询,请参考高级查询

要使用 GORM 更新数据库中的数据,我们可以使用ModelUpdates方法,这两个方法可以配合使用来执行基本的更新和部分更新操作。Model方法接受一个结构体或者主键作为参数,用于指定要更新的表和记录。Updates方法接受一个结构体或者一个映射(map)作为参数,用于指定要更新的字段和值。例如:

go 复制代码
// 更新users表中主键为1的记录的Name字段为"Frank"
db.Model(&User{}).Where("id = ?", 1).Updates(User{Name: "Frank"})

// 或者使用主键作为参数
db.Model(&User{ID: 1}).Updates(User{Name: "Frank"})

// 或者使用映射(map)作为参数
db.Model(&User{ID: 1}).Updates(map[string]interface{}{"name": "Frank"})

如果我们想更新多条记录,我们可以省略Model方法,直接使用WhereUpdates方法,这样就会更新符合条件的所有记录。例如:

go 复制代码
// 更新users表中年龄小于18的记录的Age字段为18
db.Where("age < ?", 18).Updates(User{Age: 18})

如果我们想更新关联数据,我们可以使用嵌套结构体或者标签(tag)来指定关联关系。例如:

go 复制代码
// 更新orders表中主键为1的记录,并同时更新关联的用户和商品数据
order := Order{
    Model: gorm.Model{ID: 1},
    User: User{Name: "Grace", Age: 28, Email: "grace@example.com"}, // 嵌套结构体表示一对一或者多对一的关联关系
    Products: []Product{ // 切片表示一对多或者多对多的关联关系
        {Name: "Shoes", Price: 50.0},
        {Name: "Bag", Price: 30.0},
    },
}

// 使用Updates方法更新记录,并同时更新关联数据
db.Updates(&order)

要使用 GORM 删除数据库中的数据,我们可以使用Delete方法,该方法接受一个结构体或者主键作为参数,然后将其从对应的数据库表中删除。例如:

go 复制代码
// 删除users表中主键为1的记录
db.Delete(&User{}, 1)

// 或者使用结构体作为参数
db.Delete(&User{ID: 1})

如果我们想删除多条记录,我们可以省略Delete方法的第一个参数,直接使用WhereDelete方法,这样就会删除符合条件的所有记录。例如:

go 复制代码
// 删除users表中年龄大于30的记录
db.Where("age > ?", 30).Delete(&User{})

如果我们想删除关联数据,我们可以使用嵌套结构体或者标签(tag)来指定关联关系。例如:

go 复制代码
// 删除orders表中主键为1的记录,并同时删除关联的用户和商品数据
order := Order{
    Model: gorm.Model{ID: 1},
    User: User{}, // 嵌套结构体表示一对一或者多对一的关联关系
    Products: []Product{}, // 切片表示一对多或者多对多的关联关系
}

// 使用Delete方法删除记录,并同时删除关联数据
db.Delete(&order)

在 GORM 中,删除操作有两种模式:软删除(soft delete)和硬删除(hard delete)。软删除是指通过标记一个字段(如deleted_at)来表示记录已经被删除,而不是真正地从数据库中移除。这样做的好处是可以保留被删除的数据,以便于恢复或者统计等用途。硬删除是指真正地从数据库中移除记录,这样做的好处是可以节省空间,但是无法恢复被删除的数据。

要使用软删除模式,我们只需要在结构体中嵌入gorm.Model类型,或者自定义一个名为DeletedAt的字段,类型为*time.Time(并不一定需要时间类型,可通过 tag 指定)。这样,当我们调用Delete方法时,GORM 会自动将该字段设置为当前时间,而不是真正地删除记录。例如:

go 复制代码
// User 结构体对应 users 表
type User struct {
    gorm.Model // 嵌入 gorm.Model 来自动添加 id, created_at, updated_at, deleted_at 字段
    Name string
    Age int
    Email string `gorm:"unique"`
}

// 软删除users表中主键为1的记录
db.Delete(&User{}, 1)

// 查询users表中所有记录(不包括被软删除的记录)
var users []User
db.Find(&users)

// 查询users表中所有记录(包括被软删除的记录)
var usersWithDeleted []User
db.Unscoped().Find(&usersWithDeleted)

// 恢复被软删除的记录
db.Unscoped().Model(&User{}).Where("id = ?", 1).Update("deleted_at", nil)

要使用硬删除模式,我们只需要在调用Delete方法时加上Unscoped()方法,这样 GORM 会真正地从数据库中移除记录,而不是标记字段。例如:

go 复制代码
// 硬删除users表中主键为1的记录
db.Unscoped().Delete(&User{}, 1)

事务

在数据库操作中,事务(transaction)是指一组逻辑上相关的操作,它们要么全部成功,要么全部失败,不能出现中间状态。 事务的作用是保证数据的完整性和一致性,避免因为操作失败或者中断而导致数据的损坏或者不一致。

事务通常具有以下四个特性,简称为 ACID:

  • 原子性(Atomicity):事务中的所有操作要么全部执行,要么全部不执行,不允许部分执行。
  • 一致性(Consistency):事务执行前后,数据必须保持一致,即满足所有的约束和规则。
  • 隔离性(Isolation):事务之间互不干扰,一个事务的中间状态对其他事务不可见。
  • 持久性(Durability):事务一旦提交,其对数据的修改就会永久保存,即使发生系统故障或者崩溃。

在 GORM 框架中,我们可以使用以下几个方法来管理事务:BeginCommitRollback。这些方法都会返回一个新的数据库连接对象,该对象与原始的数据库连接对象共享同一个底层连接,但是具有独立的上下文(context)。我们可以在该对象上执行我们想要包含在事务中的操作,并且在最后调用Commit或者Rollback方法来结束事务。例如:

go 复制代码
// 开始一个新的事务
tx := db.Begin()

// 在事务中执行一些操作
tx.Create(&User{Name: "Harry", Age: 30, Email: "harry@example.com"})
tx.Create(&Order{UserID: 1, Products: []Product{{Name: "Watch", Price: 100.0}}})

// 根据操作结果提交或者回滚事务
if err := tx.Error; err != nil {
    // 如果有错误发生,则回滚事务
    tx.Rollback()
} else {
    // 如果没有错误发生,则提交事务
    tx.Commit()
}

在使用事务时,有几点需要注意:

  • 尽量避免在一个事务中执行过多或者过长的操作,以免占用过多的资源或者导致锁等待。
  • 尽量保证事务的原子性和一致性,即不要在一个事务中执行无关或者矛盾的操作。
  • 尽量处理好事务中可能发生的错误和异常情况,以免造成数据的不一致或者丢失。
  • 尽量使用相同的数据库连接对象来执行事务中的所有操作,以免出现意外的结果。

除了使用BeginCommitRollback方法来管理事务外,我们还可以使用 GORM 提供的一个便捷函数:Transaction。 该函数接受一个函数作为参数,在该函数中我们可以执行我们想要包含在事务中的操作,并且返回一个错误值。 GORM 会自动开启一个新的事务,并且根据函数的返回值来自动提交或者回滚事务。例如:

go 复制代码
// 使用Transaction函数执行一个事务
err := db.Transaction(func(tx *gorm.DB) error {
    // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
    if err := tx.Create(&user1).Error; err != nil {
        // 返回任何错误都会回滚事务
        return err
    }
    
    // 嵌套事务
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&user2)
        return errors.New("rollback user2") // Rollback user2
    })

    if err := tx.Create(&user3).Error; err != nil {
        return err
    }

    // 返回 nil 提交事务
    return nil
})

使用Transaction函数的好处是可以简化事务的管理流程,避免忘记提交或者回滚事务,同时使嵌套事务结构更清晰。

GORM 提供了 SavePoint、Rollbackto 方法,来提供保存点以及回滚至保存点功能,例如:

go 复制代码
tx := db.Begin()
tx.Create(&user1)

tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2

tx.Commit() // Commit user1

性能优化和注意事项

禁用默认事务

对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们封装在一个事务里。但这会降低性能,你可以在初始化时禁用这种方式

go 复制代码
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  SkipDefaultTransaction: true,
})

缓存预编译语句

执行任何 SQL 时都创建并缓存预编译语句,可以提高后续的调用速度。

go 复制代码
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  PrepareStmt: true,
})

// 会话模式
tx := db.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Name", "Fucker")

选取字段

默认情况下,GORM 在查询时会选择所有的字段,您可以使用 Select 来指定您想要的字段

go 复制代码
db.Select("Name", "Email").Find(&Users{})

也可以定义字段更少的结构体来自动选择字段。

go 复制代码
type User struct {
    gorm.Model
    Email    string `gorm:"type:varchar(128);unique,uniqueIndex,not null,comment:邮箱"`
    Name     string `gorm:"type:varchar(32);not null,comment:姓名"`
    Password string `gorm:"type:varchar(128);not null,comment:密码"`
}

type UserInfo struct {
    Email string
    Name  string
}

// 查询时会自动选择 `id`、`name` 字段
db.Model(&User{}).Limit(10).Find(&UserInfo{})

索引提示

索引用于提高数据检索和 SQL 查询性能。 Index Hints 向优化器提供了在查询处理过程中如何选择索引的信息。与 optimizer 相比,它可以更灵活地选择更有效的执行计划。

go 复制代码
import "gorm.io/hints"

db.Clauses(hints.UseIndex("idx_users_email")).Find(&User{})
// SELECT * FROM `users` USE INDEX (`idx_users_email`)

db.Clauses(hints.ForceIndex("idx_users_email", "PRIMARY").ForJoin()).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_users_email`,`PRIMARY`)"

db.Clauses(
    hints.ForceIndex("idx_users_email", "PRIMARY").ForOrderBy(),
    hints.IgnoreIndex("idx_users_email").ForGroupBy(),
).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR ORDER BY (`idx_users_email`,`PRIMARY`) IGNORE INDEX FOR GROUP BY (`idx_users_email`)

读写分离

避免 N+1 查询

N+1 查询问题是指在执行数据库操作时,由于没有合理地使用预加载(Preloading)或者连接(Joining)等功能,而导致执行了过多或者不必要的数据库查询,从而影响了性能和效率。 N+1 查询问题通常发生在以下两种情况:

  • 在循环中执行数据库查询:这种情况下,我们会对每个循环中的元素执行一次数据库查询,从而导致总共执行了 N+1 次数据库查询,其中 N 是循环中元素的个数。例如:
go 复制代码
// 在循环中执行数据库查询
var products []Product
db.Find(&products) // 查询products表
for _, product := range products {
  var orders []Order
  db.Model(&product).Association("Orders").Find(&orders) // 查询每个商品的订单
  // 处理每个商品和订单的数据
}

上面的代码会执行 N+1 次数据库查询,其中 N 是 products 表中的记录数。这样做的缺点是:

  • 每次查询都会消耗数据库和网络资源,导致性能下降。
  • 每次查询都可能发生错误或者异常情况,导致结果不一致或者丢失。
  • 每次查询都需要处理返回值和错误等信息,导致代码复杂度增加。

为了避免这种情况,我们可以使用Preload方法来一次性获取所有需要的数据,从而减少数据库查询的次数。例如:

go 复制代码
// 使用Preload方法,一次性查询products表和orders表的数据
var products []Product
db.Preload("Orders").Find(&products) // 查询products表和orders表
for _, product := range products {
  // 处理每个商品和订单的数据
}

上面的代码只会执行一次数据库查询,从而提高了查询的性能。

  • 在条件查询中执行数据库查询:这种情况下,我们会先根据一些条件执行一次数据库查询,然后再根据查询结果执行另一次数据库查询,从而导致总共执行了两次数据库查询。例如:
go 复制代码
// 在条件查询中执行数据库查询
var user User
db.Where("name = ?", "Jack").First(&user) // 根据姓名查询用户
var orders []Order
db.Where("user_id = ?", user.ID).Find(&orders) // 根据用户ID查询订单
// 处理用户和订单的数据

上面的代码会执行两次数据库查询,其中第二次查询依赖于第一次查询的结果。这样做的缺点是:

  • 每次查询都会消耗数据库和网络资源,导致性能下降。
  • 每次查询都可能发生错误或者异常情况,导致结果不一致或者丢失。
  • 每次查询都需要处理返回值和错误等信息,导致代码复杂度增加。

为了避免这种情况,我们可以使用Joins方法来连接多个表,并且在一次数据库查询中获取所有需要的数据,从而减少数据库查询的次数。例如:

go 复制代码
// 使用Joins方法,连接users表和orders表,并且在一次数据库查询中获取所有需要的数据
var orders []Order
db.Joins("User").Where("users.name = ?", "Jack").Find(&orders) // 连接users表和orders表,并根据姓名查询订单
// 处理用户和订单的数据

上面的代码只会执行一次数据库查询,从而提高了查询的性能。 对于需要子查询的情况,直接使用*gorm.DB作为子查询,详情参考子查询。 总之,我们尽量在一次查询中就获取所有所需数据。

删库跑路

在本文的标题中,我们开玩笑地说要使用 GORM 框架从连接MySQL到删库跑路。 删库跑路是指故意或者无意地删除数据库中的所有数据,然后逃之夭夭,不负责任地放弃项目或者工作。 这是一种非常危险的行为,可能会造成严重的后果和损失,甚至触犯法律。 因此,我们在本节中强烈建议大家不要尝试或者模仿删库跑路,而是要尊重和保护数据,以及遵守职业道德和法律规范,除非你有很硬的后台。

当然,我们也不是完全不能删除数据库中的数据,只要我们有合理的理由和目的,以及做好了备份和恢复的准备,我们可以在必要的时候执行删除操作。 例如,我们可能需要删除一些过期或者无用的数据,以节省空间和提高性能; 或者我们可能需要删除一些敏感或者错误的数据,以保护隐私和安全; 或者我们可能需要删除一些测试或者演示的数据,以避免干扰正式的数据。 这些情况下,我们可以使用 GORM 框架提供的方法来执行删除操作,如前面介绍的Delete方法。

如果我们想要删除数据库中的所有数据,我们可以使用GORM框架提供的另一个方法:Migrator。 该方法可以让我们对数据库中的表和列进行创建、删除、修改等操作。例如:

go 复制代码
// 删除users表
db.Migrator().DropTable(&User{})

// 删除orders表
db.Migrator().DropTable(&Order{})

// 删除products表
db.Migrator().DropTable(&Product{})

上面的代码会执行三次数据库查询,分别删除usersordersproducts三个表。

带来的问题有:

  • 删除操作是不可逆的,一旦执行就无法恢复被删除的数据。
  • 删除操作可能会影响其他依赖于这些表的功能或者服务。
  • 删除操作可能会违反一些约束或者规则,导致数据的不一致或者错误。

为了避免这些问题,我们在执行删除操作之前,应该做好以下几点:

  • 确保我们有足够的权限和授权来执行删除操作。
  • 确保我们有充分的理由和目的来执行删除操作。
  • 确保我们已经备份了所有要删除的数据,并且可以在需要时恢复。
  • 确保我们已经通知了所有相关的人员和部门,并且得到了他们的同意和支持。
  • 确保我们已经考虑了所有可能发生的风险和后果,并且做好了应对措施。

如果我们做好了以上准备,并且仍然想要执行删除操作,那么我们可以使用 GORM 框架提供的另一个方法:Exec。 该方法可以让我们直接执行任意的SQL语句,而不受 GORM 框架的限制。 例如:

go 复制代码
// 直接执行SQL语句,删除数据库中的所有表
db.Exec("DROP DATABASE test")

上面的代码会执行一次数据库查询,直接删除数据库。

我们在使用Exec方法时,应该非常谨慎和小心,避免执行一些危险或者不合理的SQL语句,以免造成删库跑路的情况。

如何避免删库跑路

如果你是老板,要避免删库跑路的情况发生,我们可以从以下几个方面来考虑:

  • 增强数据库安全性和权限管理
  • 定期备份数据库和数据
  • 使用事务和日志记录数据库操作
  • 谨慎执行删除或者修改操作
  • 提高编程水平和职业素养
相关推荐
CallBack8 个月前
Typora+PicGo+阿里云OSS搭建个人图床,纵享丝滑!
前端·青训营笔记
Taonce1 年前
站在Android开发者的角度认识MQTT - 源码篇
android·青训营笔记
AB_IN1 年前
打开抖音会发生什么 | 青训营
青训营笔记
monster1231 年前
结营感受(go) | 青训营
青训营笔记
翼同学1 年前
实践记录:使用Bcrypt进行密码安全性保护和验证 | 青训营
青训营笔记
hu1hu_1 年前
Git 的正确使用姿势与最佳实践(1) | 青训营
青训营笔记
星曈1 年前
详解前端框架中的设计模式 | 青训营
青训营笔记
tuxiaobei1 年前
文件上传漏洞 Upload-lab 实践(中)| 青训营
青训营笔记
yibao1 年前
高质量编程与性能调优实战 | 青训营
青训营笔记
小金先生SG1 年前
阿里云对象存储OSS使用| 青训营
青训营笔记