【GO】GORM 使用教程
GORM 是一个用于 Go 语言的 ORM(Object-Relational Mapping) 库。它将关系型数据库的表与 Go 语言中的结构体相映射,允许开发者以面向对象的方式操作数据库,而不需要直接编写 SQL 语句。通过 GORM,开发者可以利用 Go 语言的结构体和方法来执行常见的数据库操作(如查询、插入、更新、删除等),大大简化了与数据库的交互过程。
1. 安装 GORM
首先,你需要安装 GORM 库。打开终端并运行以下命令:
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite # 示例数据库,可以根据需求更换为其他数据库驱动
2. 创建基础结构体
ORM的一个核心概念是结构体,它代表数据库表的一个映射。例如,假设你有一个“用户”表,我们可以创建一个 User
结构体来表示它。
package main
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"` // 主键
Name string `gorm:"size:100"` // 用户名字段,限制长度为100
Age uint // 用户年龄
CreatedAt time.Time // 创建时间(GORM 会自动管理)
}
func main() {
// 创建数据库连接
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
if err != nil {
panic("failed to connect to the database")
}
// 自动迁移:通过GORM根据结构体自动创建表
db.AutoMigrate(&User{})
}
在 GORM 中,结构体字段的标签(tags)用于定义和控制如何将 Go 结构体映射到数据库表的列。GORM 支持很多标签,可以配置数据库表的列属性、索引、关系等。以下是常见的 GORM 字段标签和它们的作用:
标签 | 描述 | 示例 |
---|---|---|
primaryKey | 指定字段为主键。 | gorm:"primaryKey" |
size | 指定字段的大小,通常用于字符串字段。 | gorm:"size:100" |
unique | 创建唯一索引,确保字段值唯一。 | gorm:"unique" |
not null | 指定字段不能为空。 | gorm:"not null" |
default | 指定字段的默认值。 | gorm:"default:0" |
autoIncrement | 设置字段为自增字段。通常用于整数类型的主键。 | gorm:"autoIncrement" |
index | 为字段创建索引。 | gorm:"index" |
uniqueIndex | 为字段创建唯一索引。 | gorm:"uniqueIndex" |
index:<name> | 创建带有自定义名称的索引。 | gorm:"index:idx_name" |
foreignKey | 指定外键字段,在一对多或多对多关系中使用。 | gorm:"foreignKey:UserID" |
references | 指定外键关系中引用的字段。 | gorm:"references:ID" |
embedded | 嵌入一个结构体,扁平化嵌入的结构体字段到父结构体中。 | gorm:"embedded" |
preload | 在查询时预加载关联数据。 | gorm:"preload" |
createdAt | 自动生成的创建时间戳字段。 | gorm:"autoCreateTime" |
updatedAt | 自动生成的更新时间戳字段。 | gorm:"autoUpdateTime" |
deletedAt | 自动生成的删除时间戳字段,支持软删除。 | gorm:"softDelete" |
softDelete | 用于支持软删除功能。通常与 DeletedAt 配合使用。 | gorm:"index" |
column | 指定数据库中的列名,当字段名与列名不一致时使用。 | gorm:"column:db_column_name" |
type | 指定字段在数据库中的类型,通常用于特殊类型(如 JSON)。 | gorm:"type:text" |
many2many | 用于多对多关系,指定连接表的名称。 | gorm:"many2many:user_posts" |
这个表格展示了 GORM 中常用的字段标签以及它们如何影响数据库表结构,帮助开发者更好地理解和使用 GORM 进行数据库操作。
3. 数据库连接与配置
在上面的示例中,我们使用了 gorm.Open()
来连接 SQLite 数据库。如果你使用 MySQL 或 Postgres 等其他数据库,可以更换相应的驱动。
例如,连接到 MySQL 数据库的代码如下:
db, err := gorm.Open(mysql.Open("user:password@tcp(localhost:3306)/dbname"), &gorm.Config{})
4. 数据库操作
4.1 创建记录
你可以使用 Create
方法来插入数据。下面是如何插入一个新用户:
func createUser(db *gorm.DB) {
user := User{Name: "John Doe", Age: 30}
result := db.Create(&user)
if result.Error != nil {
panic("Failed to create user")
}
fmt.Println("User created:", user.ID)
}
代码中db.Create(&user)
使用 &
符号是因为我们要传递结构体 user
的 指针 给 Create
方法。具体来说,这样做有以下几个原因:
-
指针传递可以修改原结构体
GORM 的
Create
方法接受结构体的 指针,这样它可以直接修改原始结构体的值,而不仅仅是副本。通过传递指针,GORM 能够在插入数据库的过程中修改结构体(例如,给结构体的字段赋值,例如数据库自动生成的ID
或CreatedAt
字段),确保结构体反映数据库中的最新数据。例如,
user
的ID
字段会在插入数据库时由 GORM 自动赋值(通常是自增的主键),如果你传递的是结构体的指针,Create
方法可以直接更新user
结构体中的ID
字段。 -
避免复制大结构体
如果你传递的是结构体的副本,GORM 会先创建一个结构体的拷贝并将其插入数据库。这对于较大的结构体来说可能会浪费内存并降低性能。而传递指针避免了复制整个结构体,只是传递了结构体的内存地址,性能更高。
-
GORM 的工作方式
GORM 内部使用了指针来标识结构体字段的变化。通过传递指针,GORM 可以确定结构体的变化并进行相应的处理。例如,在执行
Create
时,GORM 会检查结构体的指针,判断该字段是否已经赋值、是否需要自动填充等。
4.2 查询记录
GORM 提供了多种查询方式,可以通过结构体查询、条件查询等方式来获取数据。
获取单条记录
func getUser(db *gorm.DB) {
var user User
result := db.First(&user, 1) // 查找 user 表中主键为 1 的记录,并将其填充到 user 结构体中
if result.Error != nil {
panic("User not found")
}
fmt.Println("User:", user)
}
db.First
是 GORM 提供的一个查询方法,用于从数据库中获取 第一条 满足条件的记录。它通常用于根据主键或其他条件查询数据。
db.First
的基本语法:
db.First(&model, conditions...)
&model
是一个指针参数,表示查询的结果将会填充到这个结构体中。conditions...
是查询的条件,可以是主键或其他字段。
如果查询成功,db.First
会把查询到的记录填充到 model
指针所指向的结构体里。如果没有找到记录,它会返回一个错误。
在 db.First(&user, 1)
中,&user
是指向 user
结构体的指针。这里传递指针是因为 GORM 要修改 user
结构体的值(即填充查询结果)。
- 通过传递结构体的指针,GORM 可以将查询结果直接赋值到
user
结构体中。 - 如果你传递的是结构体本身,而不是指针,查询结果将不会填充到结构体中,因为结构体会作为副本传递到
db.First
方法,而 GORM 需要能够修改原始结构体的字段值。
获取多个记录
func getUsers(db *gorm.DB) {
var users []User
result := db.Find(&users)
if result.Error != nil {
panic("No users found")
}
fmt.Println("Users:", users)
}
db.Find
是 GORM 提供的查询方法之一,用于查找多个记录并将其存储到传入的切片结构体中。
Find
方法会根据传入的条件来查找记录,可以是简单的查询(如所有记录),也可以是有条件的查询(如按字段值过滤)。- 传递给
Find
的参数是一个指针,它会将查询到的记录填充到指向的切片中。
db.Find(&users)
会从数据库中查找所有记录(或者根据传入的查询条件查找记录)并将它们填充到 users
切片中。查询的结果会是一个结构体的集合。Find 方法默认返回所有满足条件的记录。
- 如果查询没有条件,
Find
将返回数据库表中的所有记录。 - 如果你传递了查询条件,
Find
将根据条件过滤结果。
Find
方法的其他功能
- 查询条件:你可以通过传递查询条件来限制查询的结果。例如,如果你想查找年龄大于 30 的所有用户,可以这么写:
db.Find(&users, "age > ?", 30)
这个查询会返回所有年龄大于 30 的用户。
- 分页:
Find
还支持分页查询。你可以通过Limit
和Offset
方法来实现分页查询。例如,查询前 10 条记录:
db.Limit(10).Find(&users)
- 排序:你也可以通过
Order
方法来指定查询结果的排序方式。例如,按年龄排序:
db.Order("age desc").Find(&users)
- 返回记录数:
Find
方法还会返回查询的结果,包括查询到的记录数。如果没有记录,它会返回一个空的切片。
4.3 更新记录
在 GORM 中,更新记录是一个常见的操作。你可以通过 GORM 提供的几种方法来更新记录。以下将详细介绍 GORM 中更新记录的方式,包含基本更新、部分更新、批量更新等操作,并解释每种方法的具体用法和注意事项。
基本更新:db.Save
方法
db.Save
方法用于保存(或更新)结构体中的数据。如果结构体的主键已经存在,GORM 会执行 更新操作;如果主键不存在,GORM 会执行 插入操作(也称为 “upsert”)。因此,db.Save
不仅适用于更新已有记录,也适用于插入新记录。
示例
func main() {
var user User
db.First(&user, 1) // 查找主键为 1 的用户
user.Name = "Alice Updated" // 修改字段
user.Age = 30
db.Save(&user) // 更新记录
}
db.Save(&user)
会检查user
是否已有主键值(假设主键存在)。如果存在,它将执行更新操作,将user
结构体中修改的字段更新到数据库中。- 如果主键不存在,它会将
user
插入到数据库中。
注意:
Save
会更新所有非零字段(即结构体中的字段如果是空值,可能不会被更新),并且会更新所有字段,即使你没有显式修改某个字段。- 如果你希望只更新某些字段,应该使用
Updates
或Update
方法。
更新部分字段:db.Updates
方法
db.Updates
方法允许你更新结构体中的 部分字段,而不是全部字段。它是一个更精确的更新方法,通常用于仅更新结构体中某些修改了的字段。
示例
func main() {
var user User
db.First(&user, 1) // 查找主键为 1 的用户
db.Model(&user).Updates(User{Name: "Bob Updated", Age: 35})
}
在这个例子中:
db.Model(&user).Updates(User{Name: "Bob Updated", Age: 35})
只会更新user
结构体中的Name
和Age
字段。db.Model(&user)
表明更新的是user
结构体对应的数据库记录。Updates
方法中的参数可以是一个结构体(如User{Name: "Bob Updated"}
),也可以是一个map[string]interface{}
(键是字段名,值是要更新的值)。
注意:
Updates
会忽略零值字段(如空字符串、零整数等),如果某个字段的值为零,它不会被更新。db.Model(&user)
用于指定要更新的模型或表。Updates
会将修改过的字段进行更新,但不会更新模型中未指定的字段。
单个字段更新:db.Update
方法
如果你只需要更新某个单独的字段,可以使用 db.Update
方法。该方法用于 更新单个字段,是 db.Updates
的简化版本,适合只更新单一字段的场景。
示例
func main() {
var user User
db.First(&user, 1) // 查找主键为 1 的用户
db.Model(&user).Update("Age", 40) // 只更新 Age 字段
}
db.Model(&user).Update("Age", 40)
会将Age
字段更新为40
,其他字段保持不变。Update
方法适用于你只需要更新单个字段的情况。
注意:
Update
方法只更新指定的字段,不会影响其他字段。- 如果字段的值为零值,
Update
也会更新该字段(没有零值忽略
的机制)。
批量更新:db.UpdateColumn
和 db.UpdateColumns
GORM 还提供了 UpdateColumn
和 UpdateColumns
方法,主要用于 批量更新 字段。这些方法与 Update
方法类似,但它们不会触发 GORM 的钩子(如 BeforeSave
、AfterSave
等)。
UpdateColumn
示例
db.Model(&user).UpdateColumn("Age", 45)
UpdateColumn
不会触发 GORM 的 BeforeSave
和 AfterSave
钩子,因此适用于需要绕过这些钩子的情况。
UpdateColumns
示例
db.Model(&user).UpdateColumns(map[string]interface{}{"Age": 50, "Name": "Charlie Updated"})
UpdateColumns
会根据传入的字段进行批量更新。与 Update
不同,它不会触发模型的钩子。
注意:
- 这两个方法直接更新字段,不会对字段的零值进行忽略。
- 它们只进行 单字段的原子更新,不会涉及到多表关联等操作。
条件更新:db.Where
和 db.Updates
你可以在更新时通过 Where
方法指定更新的条件。Where
方法可以与 Updates
或 Update
一起使用,以便进行条件更新。
示例
db.Model(&User{}).Where("age > ?", 30).Updates(User{Name: "Updated Name"})
- 这个示例中,
Where("age > ?", 30)
限定了更新条件,只有年龄大于 30 的用户才会被更新。 Updates(User{Name: "Updated Name"})
更新所有符合条件的用户的Name
字段。
注意:
Where
可以帮助你构造复杂的更新条件,但也可以根据需要单独使用(例如,按 ID 更新某些记录)。
批量更新(多个记录)
你可以使用 db.Model()
方法和 db.Updates()
方法来批量更新多个记录。下面是一个批量更新的示例:
示例
db.Model(&User{}).Where("age > ?", 30).Updates(User{Name: "Batch Update"})
- 这个例子会更新所有
age > 30
的用户,将它们的Name
字段修改为"Batch Update"
。
注意:
Updates
会更新所有符合条件的记录,而不是只更新一个记录。
使用事务更新多个记录
如果你需要确保多个更新操作的原子性,可以将更新操作放入一个事务中。在 GORM 中,事务通过 db.Begin()
开始,db.Commit()
提交,db.Rollback()
回滚。
示例
tx := db.Begin()
// 执行多个更新操作
tx.Model(&User{}).Where("age > ?", 30).Updates(User{Name: "Transactional Update"})
tx.Model(&User{}).Where("name = ?", "Bob").Update("Age", 40)
if err := tx.Commit().Error; err != nil {
tx.Rollback()
fmt.Println("Error:", err)
return
}
db.Begin()
开始一个事务。tx.Commit()
提交事务,tx.Rollback()
在出错时回滚事务,确保所有操作的原子性。
4.4 删除记录
删除记录可以使用 Delete
方法:
func deleteUser(db *gorm.DB) {
var user User
db.First(&user, 1) // 查找要删除的用户
// 删除用户
result := db.Delete(&user)
if result.Error != nil {
panic("Failed to delete user")
}
fmt.Println("User deleted:", user.ID)
}
5. 关系与关联查询
GORM 支持表之间的关系映射。比如,我们有 User
和 Post
之间的关系。一个用户可以有多个帖子,可以使用 has many
关系。
5.1 定义关联结构体
type Post struct {
ID uint
Title string
Body string
UserID uint // 外键
User User // 关联的 User
}
5.2 关联查询
假设我们有 User
和 Post
两个表,你可以使用 Preload
来加载关联的 Post
数据。
func getUserWithPosts(db *gorm.DB) {
var user User
db.Preload("Posts").First(&user, 1)
fmt.Println("User:", user.Name)
fmt.Println("Posts:", user.Posts)
}
5.3 创建关联记录
当你插入一个带有关联的记录时,可以使用 Create
方法来同时插入主表和从表数据:
func createUserWithPosts(db *gorm.DB) {
user := User{Name: "Alice", Age: 28, Posts: []Post{
{Title: "Post 1", Body: "This is the first post"},
{Title: "Post 2", Body: "This is the second post"},
}}
db.Create(&user)
fmt.Println("User and Posts created:", user)
}
6. 事务
在 GORM 中,事务(Transaction) 是一个非常重要的概念,尤其是在需要确保多个数据库操作要么全部成功,要么全部失败的情况下。事务能够保证操作的原子性、一致性、隔离性和持久性(即 ACID 特性)。如果在事务中的某个操作失败,事务可以回滚,使得数据库回到事务开始之前的状态。
事务(Transaction)是一组数据库操作,它们要么全部执行,要么在发生错误时全部不执行。事务在数据库操作中提供了 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation) 和 持久性(Durability)(合称为ACID特性):
- 原子性(Atomicity):事务中的所有操作要么都执行,要么都不执行。即事务是不可分割的整体。
- 一致性(Consistency):事务执行前后,数据库的状态必须是一致的,符合数据库的完整性约束。
- 隔离性(Isolation):一个事务的执行不会被其他事务干扰。事务的执行是相互隔离的。
- 持久性(Durability):一旦事务提交,其对数据库的更改是永久性的,不会丢失。
在 GORM 中,你可以通过 db.Begin()
来启动一个事务,使用 tx.Commit()
来提交事务,使用 tx.Rollback()
来回滚事务。以下是 GORM 中事务的常见用法。
6.1. 开始事务:db.Begin()
你可以通过 db.Begin()
启动一个事务。这个方法会返回一个事务对象(*gorm.DB
类型),通过这个对象你可以执行数据库操作。
tx := db.Begin() // 开始事务
db.Begin()
会创建一个事务。- 返回的
tx
是事务对象,所有的数据库操作都应通过tx
来执行,而不是直接使用db
。
6.2 执行事务中的操作
在事务中,你可以进行一系列的数据库操作。所有的操作都应该通过事务对象 tx
来执行,而不是直接通过 db
执行。
示例
tx := db.Begin() // 开始事务
// 执行多个数据库操作
if err := tx.Create(&user).Error; err != nil {
tx.Rollback() // 操作失败,回滚事务
return err
}
if err := tx.Model(&user).Update("Age", 30).Error; err != nil {
tx.Rollback() // 操作失败,回滚事务
return err
}
tx.Create(&user)
会在事务中插入一条记录。tx.Model(&user).Update("Age", 30)
会在事务中更新该记录。
6.3 提交事务:tx.Commit()
当所有的操作都执行成功时,可以调用 tx.Commit()
提交事务,将所有的变更永久保存到数据库。
if err := tx.Commit().Error; err != nil {
tx.Rollback() // 提交失败,回滚事务
return err
}
tx.Commit()
会提交事务,执行所有的操作并将它们持久化到数据库。
6.4 回滚事务:tx.Rollback()
如果在事务过程中遇到错误,应该调用 tx.Rollback()
来回滚事务。这样,所有在事务中执行的操作都会撤销,数据库将恢复到事务开始前的状态。
if err := tx.Rollback().Error; err != nil {
fmt.Println("Error during rollback:", err)
return err
}
tx.Rollback()
会撤销事务中的所有操作。
6.5 在事务中使用错误处理
通常,事务中的操作需要进行错误处理。只要有任何一项操作失败,应该调用 tx.Rollback()
进行回滚。
tx := db.Begin()
// 执行操作 1
if err := tx.Create(&user).Error; err != nil {
tx.Rollback() // 错误发生,回滚事务
return err
}
// 执行操作 2
if err := tx.Model(&user).Update("Age", 30).Error; err != nil {
tx.Rollback() // 错误发生,回滚事务
return err
}
// 所有操作成功,提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback() // 提交失败,回滚事务
return err
}
6.6 事务中的多表操作
在事务中,你可以操作多个表,只要使用同一个事务对象 tx
,所有的表操作都将在一个事务内完成。
示例:多表操作
tx := db.Begin()
// 插入用户表
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
// 更新订单表
if err := tx.Model(&order).Update("Status", "Shipped").Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
- 这里插入了一个用户并更新了订单状态,所有操作都在同一个事务中进行。
6.7 事务的嵌套
GORM 不直接支持嵌套事务(即在一个事务中开启另一个事务)。但是,你可以通过手动管理事务嵌套。在嵌套事务中,只有最外层的事务会决定是否提交或回滚。
tx := db.Begin()
// 外部事务操作
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
nestedTx := tx.Begin() // 开始嵌套事务
// 嵌套事务操作
if err := nestedTx.Model(&order).Update("Status", "Shipped").Error; err != nil {
nestedTx.Rollback() // 嵌套事务回滚
tx.Rollback() // 外部事务回滚
return err
}
nestedTx.Commit() // 嵌套事务提交
tx.Commit() // 外部事务提交
- 上述代码演示了如何在一个事务中手动开启一个嵌套事务。嵌套事务的提交和回滚会影响最外层事务。
6.8 事务中的并发问题
在事务中使用并发操作时,必须小心并发引起的 数据竞争 和 死锁 问题。GORM 默认使用 隔离级别 为 ReadCommitted
,你可以通过配置数据库的事务隔离级别来避免一些并发问题。
tx := db.Begin().Set("gorm:query_option", "LOCK IN SHARE MODE")
// 事务操作
此时,LOCK IN SHARE MODE
会在查询时加锁,避免其他事务修改同一行数据,防止数据不一致。
总结
GORM 是一个功能强大且易于使用的 Go 语言 ORM 库,能够让开发者以面向对象的方式与数据库交互,减少了 SQL 语句的编写和管理的复杂度。它适合需要处理数据库的 Go 项目,特别是那些涉及大量数据操作、需要事务支持和多表关联的应用。