第四章 Gorm v1.0
第一部分 快速入门
Gorm 是一个功能强大的Go语言ORM(对象关系映射)库,支持多种主流数据库。
一、项目初始化
在开始使用 Gorm 操作各种数据库之前,需要先安装 Gorm 核心库,打开终端执行以下命令:
# 初始化Go模块
go mod init app
# 安装Gorm
go get -u gorm.io/gorm
二、数据库驱动
使用Gorm时,针对不同的数据库,还需要安装对应的驱动
常用数据库驱动安装方式如下:
go get -u gorm.io/driver/mysql
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/mongodb
第二部分 基础功能
一、连接数据库
在进行数据库操作前,建立与数据库的连接是首要步骤,GORM 提供了统一的入口方法 Open 来初始化连接,签名如下:
/// 初始化数据库连接
// dialector: 代表数据库驱动适配器接口,用于适配不同类型的数据库
// opts: 可变参数,用于传入 GORM 的配置选项,最常用的是 &gorm.Config{}
func Open(dialector Dialector, opts ...Option) (db *DB, err error)
对于不同数据库的驱动适配和连接参数存在差异,以下举例了 SQLite、MySQL 和 PostgreSQL 三种常用关系型数据库 和 非关系型数据库 MongoDB 的连接操作:
package main
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func connectSQLite() (*gorm.DB, error) {
// 连接SQLite数据库,若数据库文件不存在则自动创建
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func connectMySQL() (*gorm.DB, error) {
// 数据源名称格式:user:pass@tcp(addr:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
package main
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func connectPostgreSQL() (*gorm.DB, error) {
dsn := "host=localhost user=postgres password=password dbname=testdb port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
package main
import (
"gorm.io/driver/mongodb"
"gorm.io/gorm"
)
func connectPostgreSQL() (*gorm.DB, error) {
dsn := "mongodb://localhost:27017/testdb?directConnection=true"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
二、数据表与模型定义
在 Gorm 中,数据表的结构是通过模型(结构体)来定义的,模型与数据表之间存在映射关系,通过定义模型,可以指定数据表的字段名、数据类型、约束条件等信息。
1、基础模型定义
Gorm 提供了一个基础模型 gorm.Model
,包含了常用的字段:ID
(主键)、CreatedAt
(创建时间)、UpdatedAt
(更新时间)、DeletedAt
(删除时间,用于逻辑删除),可以通过嵌入该模型来快速定义包含这些基础字段的模型。
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
// 嵌入 gorm.Model 的自定义模型
type User struct {
gorm.Model // 嵌入基础模型,包含 ID、CreatedAt、UpdatedAt、DeletedAt 字段
Name string // 用户名
Age int // 年龄
Email string // 邮箱
Address string // 地址
}
2、字段标签
通过字段标签可以对模型字段进行更详细的配置,如指定字段名、数据类型等
常用的 Gorm 标签如下:
// column: 指定数据库中的字段名,若不指定则默认使用结构体字段名的下划线分隔命名(UserName 对应数据库字段 user_name)
type User struct {
gorm.Model
UserName string `gorm:"column:user_name"` // 数据库字段名为 user_name
}
// type: 指定数据库字段的数据类型
type Product struct {
gorm.Model
Name string `gorm:"type:varchar(100)"` // 字符串类型,长度 100
Price float64 `gorm:"type:decimal(10,2)"` // decimal 类型,总长度 10,小数位 2
Stock int `gorm:"type:int unsigned"` // 无符号 int 类型
}
// primaryKey: 指定字段为主键
type Order struct {
OrderID string `gorm:"primaryKey"` // 自定义主键,字符串类型
UserID uint
Amount float64
CreatedAt time.Time
}
// unique: 指定字段设置唯一索引
type User struct {
gorm.Model
Email string `gorm:"unique"` // 邮箱唯一
}
// not null: 指定字段设置非空索引
type User struct {
gorm.Model
Name string `gorm:"not null"` // 用户名不为空
}
// index: 为字段创建索引,提高查询效率
type User struct {
gorm.Model
Age int `gorm:"index"` // 为年龄字段创建索引
Name string `gorm:"index:idx_name_age"` // 创建名为 idx_name_age 的索引
}
// default: 指定字段的默认值
type User struct {
gorm.Model
Status int `gorm:"default:1"` // 状态默认值为 1
}
3、模型迁移
模型迁移是指根据定义的结构体(模型)自动创建、更新数据库表结构的过程。它能帮助开发者快速将代码中的模型映射到数据库表,无需手动编写 SQL 语句创建表结构,尤其适合快速开发和迭代。
GORM提供AutoMigrate方法实现模型迁移,会根据模型结构体的定义,自动执行以下操作:
- 若表不存在,则创建表(根据模型字段和标签生成表结构)
- 若表已存在,会新增缺失的字段和索引(但不会删除已有字段或索引,避免数据丢失)
// 对单个模型进行迁移
db.AutoMigrate(&User{})
// 对多个模型进行迁移
db.AutoMigrate(&User{}, &Product{})
三、事务操作
事务是数据库操作中保证数据一致性的重要机制,Gorm 对事务提供了简洁的支持
1、事务的基本流程
// 通过 `db.Begin()` 方法开启一个事务
// 该方法返回一个事务对象 `tx`,后续的操作都通过该对象进行
tx := db.Begin()
if tx.Error != nil {
panic("开启事务失败: " + tx.Error.Error())
}
// 在事务中执行各种数据库操作,如创建、更新、删除等。这些操作如果出现错误,需要进行回滚。
/// 示例:在事务中创建用户
if err := tx.Create(&User{Name: "事务测试用户", Age: 28}).Error; err != nil {
tx.Rollback() // 回滚事务
panic("事务中创建数据失败: " + err.Error())
}
// 当所有事务内的操作都成功执行后,通过 `tx.Commit()` 方法提交事务,使所有操作生效。
if err := tx.Commit().Error; err != nil {
panic("提交事务失败: " + err.Error())
}
2、事务的注意事项
- 事务操作必须使用事务对象
tx
来执行,而不是原始的db
对象,否则操作不会被纳入事务管理。 - 在事务执行过程中,只要有任何一个操作发生错误,都需要调用
tx.Rollback()
进行回滚,以保证数据的一致性。 - 事务的范围应尽可能小,避免长时间占用事务资源,影响数据库的并发性能。
四、增删查改(CRUD)
增删查改是数据库操作的基础,Gorm 为这些操作提供了简洁易用的 API,以下是详细介绍。
1、创建操作
创建操作用于向数据库中插入新的数据记录,Gorm 提供了Create
方法来实现创建功能。
// 创建一个用户并插入到数据库
user := User{Name: "张三", Age: 25, Email: "zhangsan@example.com"}
result := db.Create(&user) // 注意传递指针,以便Gorm可以更新ID等字段
// 查看创建结果
if result.Error != nil {
panic("创建数据失败: " + result.Error.Error())
}
fmt.Printf("创建成功,插入的记录数: %dn", result.RowsAffected) // RowsAffected 表示受影响的行数
fmt.Printf("新创建用户的ID: %dn", user.ID) // 创建成功后,Gorm会自动将生成的ID赋值给模型的ID字段
// 多个用户数据
users := []User{
{Name: "李四", Age: 30, Email: "lisi@example.com"},
{Name: "王五", Age: 28, Email: "wangwu@example.com"},
}
// 批量创建用户
result := db.Create(&users)
if result.Error != nil {
panic("批量创建数据失败: " + result.Error.Error())
}
fmt.Printf("批量创建成功,插入的记录数: %dn", result.RowsAffected)
2、查询操作
查询操作用于从数据库中获取数据记录。Gorm 提供了丰富的查询方法,满足不同的查询需求。
①获取查询结果
1.查询单条记录并获取结果
// 查询第一条记录,按照主键升序排序。如果查询不到记录,会返回 `ErrRecordNotFound` 错误。
var user User
result := db.First(&user, 1) // 查询ID为1的用户
// 也可以通过条件查询:db.First(&user, "name = ?", "张三")
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
fmt.Println("未找到记录")
} else {
panic("查询数据失败: " + result.Error.Error())
}
}
fmt.Printf("查询到的用户: %+vn", user)
// 查询任意一条记录,不指定排序方式
var user User
db.Take(&user)
// 查询最后一条记录,按照主键降序排序
var user User
db.Last(&user)
2.查询多条记录并获取结果
// 查询满足条件的所有记录
var users []User
// 查询所有年龄大于25的用户
result := db.Where("age > ?", 25).Find(&users)
if result.Error != nil {
panic("查询数据失败: " + result.Error.Error())
}
fmt.Printf("查询到的用户数量: %dn", len(users))
for _, u := range users {
fmt.Printf("用户: %+vn", u)
}
②指定结果列
var users []User
// 指定只查询 name 和 age 列
db.Select("name", "age").Find(&users)
③条件查询
var users []User
// 主键条件:当Where的参数是单一的数值时,Gorm 会默认将其作为主键的值进行查询
db.Where(1).Find(&users)
// 字符串条件
db.Where("age > ? AND role = ?", 18, "user").Find(&users)
// 模糊查询
db.Where("name LIKE ?", "%张%").Find(&users)
// 范围查询
db.Where("age BETWEEN ? AND ?", 20, 30).Find(&users)
// 查询 age 不等于 18 的用户
db.Not("age = ?", 18).Find(&users)
// 查询 age > 30 或 role = "admin" 的用户
db.Where("age > 30").Or("role = ?", "admin").Find(&users)
// 结构体条件仅匹配非零值,适合精确匹配
db.Where(User{Role: "admin", Age: 30}).Find(&users)
// 等价于:SELECT * FROM users WHERE role = "admin" AND age = 30;
// map条件所有键值对都会作为条件,支持零值
db.Where(map[string]interface{}{"role": "user", "age": 0}).Find(&users)
// 等价于:SELECT * FROM users WHERE role = "user" AND age = 0;
④排序
var users []User
// 按 age 升序(默认升序)
db.Order("age").Find(&users)
// 等价于:SELECT * FROM users ORDER BY age;
// 按 age 降序
db.Order("age DESC").Find(&users)
// 多字段排序(先按 role 升序,再按 age 降序)
db.Order("role ASC, age DESC").Find(&users)
⑤分组与聚合
// 定义接收结果的结构体(需与查询字段对应)
type RoleCount struct {
Role string
Total int
}
var roleCounts []RoleCount
// 按 role 分组,统计每个角色的用户数量
db.Model(&User{}).
Select("role, COUNT(*) as total").
Group("role").
Find(&roleCounts)
// 等价于:SELECT role, COUNT(*) as total FROM users GROUP BY role;
// 统计用户数 > 10 的角色
db.Model(&User{}).
Select("role, COUNT(*) as total").
Group("role").
Having("total > ?", 10). // 筛选条件
Find(&roleCounts)
// 等价于:SELECT role, COUNT(*) as total FROM users GROUP BY role HAVING total > 10;
⑥分页
分页查询,Limit 限制返回条数,Offset 指定跳过的条数
var users []User
pageSize := 10 // 每页 10 条
pageNum := 2 // 第 2 页(从 1 开始)
// 计算偏移量:(页码-1)*每页条数
db.Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&users)
// 等价于:SELECT * FROM users LIMIT 10 OFFSET 10;
3、更新操作
更新操作用于修改数据库中已有的数据记录。Gorm 提供了多种更新方式。
1.更新单个字段
使用
Update
方法更新指定的单个字段
// 更新ID为1的用户的年龄为26
result := db.Model(&User{}).Where("id = ?", 1).Update("Age", 26)
if result.Error != nil {
panic("更新数据失败: " + result.Error.Error())
}
fmt.Printf("更新成功,受影响的行数: %dn", result.RowsAffected)
2.更新多个字段
使用
Updates
方法可以同时更新多个字段,参数可以是结构体或map类型
// 注:不特殊指定更新字段时,结构体中零值字段不会被更新
result := db.Model(&User{}).Where("id = ?", 1).Updates(User{Age: 27, Email: "zhangsan_new@example.com"})
if result.Error != nil {
panic("更新数据失败: " + result.Error.Error())
}
// map 中的所有键值对都会被更新
result := db.Model(&User{}).Where("id = ?", 1).Updates(map[string]interface{}{"Age": 28, "Email": "zhangsan_map@example.com"})
if result.Error != nil {
panic("更新数据失败: " + result.Error.Error())
}
// 更新操作中使用 Select() 可以强制指定更新字段,包括零值字段
user := User{Name: "张三", Age: 0, Email: ""} // Age和Email为零值
// 指定更新所有字段
result := db.Model(&User{}).Where("id = ?", 1).Select("*").Updates(user)
// 指定更新Email字段
result := db.Model(&User{}).Where("id = ?", 1).Select("email").Updates(user)
// 定义模型时,将需要支持零值更新的字段声明为指针类型
type User struct {
gorm.Model
Name string
Age *int // 年龄字段定义为int指针
Email string
}
// 更新年龄为0(零值)
zeroAge := 0
db.Model(&User{}).Where("id = ?", 1).Updates(User{
Age: &zeroAge, // 传递零值指针
})
4、删除操作
删除操作用于从数据库中移除数据记录,Gorm 支持物理删除和逻辑删除。
/// 物理删除: 直接从数据库中删除记录,数据无法恢复
// 删除ID为1的用户
result := db.Delete(&User{}, 1)
// 也可以通过条件删除:db.Delete(&User{}, "age < ?", 18)
if result.Error != nil {
panic("删除数据失败: " + result.Error.Error())
}
fmt.Printf("删除成功,受影响的行数: %dn", result.RowsAffected)
/// 逻辑删除: 设置 `DeletedAt` (gorm.Model中定义) 字段为当前时间,查询时会自动过滤掉已删除的记录
// 模型定义(已包含在gorm.Model中)
type User struct {
gorm.Model // 包含ID, CreatedAt, UpdatedAt, DeletedAt字段
Name string
Age int
Email string
}
// 逻辑删除ID为1的用户
result := db.Delete(&User{}, 1)
if result.Error != nil {
panic("删除数据失败: " + result.Error.Error())
}
// 查询时会自动过滤已删除的记录
var users []User
db.Find(&users) // 不会包含已逻辑删除的用户
// 如果需要查询包括已删除的记录,可以使用Unscoped
db.Unscoped().Find(&users) // 会包含已逻辑删除的用户
五、原生SQL语句
1、查询操作
Gorm 支持直接执行原生 SQL 语句,适用于复杂查询等场景
var users []User
// 执行原生 SQL 并映射到 users
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
2、更新操作
Gorm 同样支持直接执行非查询操作(如更新、删除)的SQL命令
// 批量更新年龄 > 30 的用户角色为 "senior"
result := db.Exec("UPDATE users SET role = ? WHERE age > ?", "senior", 30)
// 获取受影响的行数
fmt.Println(result.RowsAffected) // 输出:更新的行数