引言
在开发软件应用时,我们经常需要与数据库进行交互,存储和查询数据。然而,数据库中的数据通常是以表格的形式组织的,而我们在编程语言中使用的数据结构则是以对象的形式表示的。这就导致了一个问题:如何在两种不同的数据表示之间进行转换和映射?这就是 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 中钩子方法
- 支持
Preload
、Joins
的预加载 - 事务,嵌套事务,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 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成。
创建 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 查询数据库中的数据,我们可以使用一系列的方法来执行不同类型和条件的查询,如Find
、First
、Last
、Take
、Where
等。这些方法都会返回一个*gorm.DB
类型的对象,该对象包含了查询结果和错误等信息。我们可以通过该对象来获取和处理查询结果和错误。例如:
go
// 查询users表中的所有记录,并将结果存储到一个User类型的切片中
var users []User
result := db.Find(&users)
// 检查是否有错误发生
if result.Error != nil {
// 处理错误
}
// 获取影响的行数
rows := result.RowsAffected
// 遍历查询结果
for _, user := range users {
// 处理每个用户的数据
}
如果我们只想查询一条记录,我们可以使用First
、Last
、Take
等方法,这些方法会按照主键或者其他条件来返回第一条或者最后一条记录。例如:
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 更新数据库中的数据,我们可以使用Model
和Updates
方法,这两个方法可以配合使用来执行基本的更新和部分更新操作。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
方法,直接使用Where
和Updates
方法,这样就会更新符合条件的所有记录。例如:
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
方法的第一个参数,直接使用Where
和Delete
方法,这样就会删除符合条件的所有记录。例如:
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 框架中,我们可以使用以下几个方法来管理事务:Begin
、Commit
、Rollback
。这些方法都会返回一个新的数据库连接对象,该对象与原始的数据库连接对象共享同一个底层连接,但是具有独立的上下文(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()
}
在使用事务时,有几点需要注意:
- 尽量避免在一个事务中执行过多或者过长的操作,以免占用过多的资源或者导致锁等待。
- 尽量保证事务的原子性和一致性,即不要在一个事务中执行无关或者矛盾的操作。
- 尽量处理好事务中可能发生的错误和异常情况,以免造成数据的不一致或者丢失。
- 尽量使用相同的数据库连接对象来执行事务中的所有操作,以免出现意外的结果。
除了使用Begin
、Commit
、Rollback
方法来管理事务外,我们还可以使用 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{})
上面的代码会执行三次数据库查询,分别删除users
、orders
和products
三个表。
带来的问题有:
- 删除操作是不可逆的,一旦执行就无法恢复被删除的数据。
- 删除操作可能会影响其他依赖于这些表的功能或者服务。
- 删除操作可能会违反一些约束或者规则,导致数据的不一致或者错误。
为了避免这些问题,我们在执行删除操作之前,应该做好以下几点:
- 确保我们有足够的权限和授权来执行删除操作。
- 确保我们有充分的理由和目的来执行删除操作。
- 确保我们已经备份了所有要删除的数据,并且可以在需要时恢复。
- 确保我们已经通知了所有相关的人员和部门,并且得到了他们的同意和支持。
- 确保我们已经考虑了所有可能发生的风险和后果,并且做好了应对措施。
如果我们做好了以上准备,并且仍然想要执行删除操作,那么我们可以使用 GORM 框架提供的另一个方法:Exec
。 该方法可以让我们直接执行任意的SQL语句,而不受 GORM 框架的限制。 例如:
go
// 直接执行SQL语句,删除数据库中的所有表
db.Exec("DROP DATABASE test")
上面的代码会执行一次数据库查询,直接删除数据库。
我们在使用Exec
方法时,应该非常谨慎和小心,避免执行一些危险或者不合理的SQL语句,以免造成删库跑路的情况。
如何避免删库跑路
如果你是老板,要避免删库跑路的情况发生,我们可以从以下几个方面来考虑:
- 增强数据库安全性和权限管理
- 定期备份数据库和数据
- 使用事务和日志记录数据库操作
- 谨慎执行删除或者修改操作
- 提高编程水平和职业素养