Skip to content

第四章 Gorm v1.0

第一部分 快速入门

Gorm 是一个功能强大的Go语言ORM(对象关系映射)库,支持多种主流数据库。

一、项目初始化

在开始使用 Gorm 操作各种数据库之前,需要先安装 Gorm 核心库,打开终端执行以下命令:

go
# 初始化Go模块
go mod init app
# 安装Gorm
go get -u gorm.io/gorm

二、数据库驱动

使用Gorm时,针对不同的数据库,还需要安装对应的驱动

常用数据库驱动安装方式如下:

shell
go get -u gorm.io/driver/mysql
shell
go get -u gorm.io/driver/postgres
shell
go get -u gorm.io/driver/sqlite
shell
go get -u gorm.io/driver/mongodb

第二部分 基础功能

一、连接数据库

在进行数据库操作前,建立与数据库的连接是首要步骤,GORM 提供了统一的入口方法 Open 来初始化连接,签名如下:

go
/// 初始化数据库连接
// dialector:  代表数据库驱动适配器接口,用于适配不同类型的数据库
// opts:       可变参数,用于传入 GORM 的配置选项,最常用的是 &gorm.Config{}
func Open(dialector Dialector, opts ...Option) (db *DB, err error)

对于不同数据库的驱动适配和连接参数存在差异,以下举例了 SQLite、MySQL 和 PostgreSQL 三种常用关系型数据库 和 非关系型数据库 MongoDB 的连接操作:

go
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
}
go
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
}
go
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
}
go
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(删除时间,用于逻辑删除),可以通过嵌入该模型来快速定义包含这些基础字段的模型。

go
// gorm.Model 的定义
type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}
go
// 嵌入 gorm.Model 的自定义模型
type User struct {
    gorm.Model // 嵌入基础模型,包含 ID、CreatedAt、UpdatedAt、DeletedAt 字段
    Name       string        // 用户名
    Age        int           // 年龄
    Email      string        // 邮箱
    Address    string        // 地址
}

2、字段标签

通过字段标签可以对模型字段进行更详细的配置,如指定字段名、数据类型等

常用的 Gorm 标签如下:

go
// column: 指定数据库中的字段名,若不指定则默认使用结构体字段名的下划线分隔命名(UserName 对应数据库字段 user_name)
type User struct {
    gorm.Model
    UserName string `gorm:"column:user_name"` // 数据库字段名为 user_name
}
go
// 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 类型
}
go
// primaryKey: 指定字段为主键
type Order struct {
    OrderID  string `gorm:"primaryKey"` // 自定义主键,字符串类型
    UserID   uint
    Amount   float64
    CreatedAt time.Time
}
go
// unique: 指定字段设置唯一索引
type User struct {
    gorm.Model
    Email string `gorm:"unique"` // 邮箱唯一
}
go
// not null: 指定字段设置非空索引
type User struct {
    gorm.Model
    Name string `gorm:"not null"` // 用户名不为空
}
go
// index: 为字段创建索引,提高查询效率
type User struct {
    gorm.Model
    Age   int    `gorm:"index"` // 为年龄字段创建索引
    Name  string `gorm:"index:idx_name_age"` // 创建名为 idx_name_age 的索引
}
go
// default: 指定字段的默认值
type User struct {
    gorm.Model
    Status int `gorm:"default:1"` // 状态默认值为 1
}

3、模型迁移

模型迁移是指根据定义的结构体(模型)自动创建、更新数据库表结构的过程。它能帮助开发者快速将代码中的模型映射到数据库表,无需手动编写 SQL 语句创建表结构,尤其适合快速开发和迭代。

GORM提供AutoMigrate方法实现模型迁移,会根据模型结构体的定义,自动执行以下操作:

  • 若表不存在,则创建表(根据模型字段和标签生成表结构)
  • 若表已存在,会新增缺失的字段和索引(但不会删除已有字段或索引,避免数据丢失)
go
// 对单个模型进行迁移
db.AutoMigrate(&User{})

// 对多个模型进行迁移
db.AutoMigrate(&User{}, &Product{})

三、事务操作

事务是数据库操作中保证数据一致性的重要机制,Gorm 对事务提供了简洁的支持

1、事务的基本流程

go
// 通过 `db.Begin()` 方法开启一个事务
// 该方法返回一个事务对象 `tx`,后续的操作都通过该对象进行
tx := db.Begin()

if tx.Error != nil {
   panic("开启事务失败: " + tx.Error.Error())
}
go
// 在事务中执行各种数据库操作,如创建、更新、删除等。这些操作如果出现错误,需要进行回滚。
/// 示例:在事务中创建用户
if err := tx.Create(&User{Name: "事务测试用户", Age: 28}).Error; err != nil {
   tx.Rollback() // 回滚事务
   panic("事务中创建数据失败: " + err.Error())
}
go
// 当所有事务内的操作都成功执行后,通过 `tx.Commit()` 方法提交事务,使所有操作生效。
if err := tx.Commit().Error; err != nil {
   panic("提交事务失败: " + err.Error())
}

2、事务的注意事项

  • 事务操作必须使用事务对象 tx 来执行,而不是原始的 db 对象,否则操作不会被纳入事务管理。
  • 在事务执行过程中,只要有任何一个操作发生错误,都需要调用 tx.Rollback() 进行回滚,以保证数据的一致性。
  • 事务的范围应尽可能小,避免长时间占用事务资源,影响数据库的并发性能。

四、增删查改(CRUD)

增删查改是数据库操作的基础,Gorm 为这些操作提供了简洁易用的 API,以下是详细介绍。

1、创建操作

创建操作用于向数据库中插入新的数据记录,Gorm 提供了Create方法来实现创建功能。

go
// 创建一个用户并插入到数据库
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字段
go
// 多个用户数据
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.查询单条记录并获取结果

go
// 查询第一条记录,按照主键升序排序。如果查询不到记录,会返回 `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)
go
// 查询任意一条记录,不指定排序方式
var user User
db.Take(&user)
go
// 查询最后一条记录,按照主键降序排序
var user User
db.Last(&user)

2.查询多条记录并获取结果

go
// 查询满足条件的所有记录
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)
}
②指定结果列
go
var users []User

// 指定只查询 name 和 age 列
db.Select("name", "age").Find(&users)
③条件查询
go
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)
go
// 查询 age 不等于 18 的用户
db.Not("age = ?", 18).Find(&users)
go
// 查询 age > 30 或 role = "admin" 的用户
db.Where("age > 30").Or("role = ?", "admin").Find(&users)
go
// 结构体条件仅匹配非零值,适合精确匹配
db.Where(User{Role: "admin", Age: 30}).Find(&users)
// 等价于:SELECT * FROM users WHERE role = "admin" AND age = 30;
go
// map条件所有键值对都会作为条件,支持零值
db.Where(map[string]interface{}{"role": "user", "age": 0}).Find(&users)
// 等价于:SELECT * FROM users WHERE role = "user" AND age = 0;
④排序
go
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)
⑤分组与聚合
go
// 定义接收结果的结构体(需与查询字段对应)
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;
go
// 统计用户数 > 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 指定跳过的条数

go
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 方法更新指定的单个字段

go
// 更新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类型

go
// 注:不特殊指定更新字段时,结构体中零值字段不会被更新
result := db.Model(&User{}).Where("id = ?", 1).Updates(User{Age: 27, Email: "zhangsan_new@example.com"})

if result.Error != nil {
   panic("更新数据失败: " + result.Error.Error())
}
go
// 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())
}
go
// 更新操作中使用 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)
go
// 定义模型时,将需要支持零值更新的字段声明为指针类型
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 支持物理删除和逻辑删除。

go
/// 物理删除: 直接从数据库中删除记录,数据无法恢复
// 删除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)
go
/// 逻辑删除: 设置 `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 语句,适用于复杂查询等场景

go
var users []User
// 执行原生 SQL 并映射到 users
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)

2、更新操作

Gorm 同样支持直接执行非查询操作(如更新、删除)的SQL命令

go
// 批量更新年龄 > 30 的用户角色为 "senior"
result := db.Exec("UPDATE users SET role = ? WHERE age > ?", "senior", 30)
// 获取受影响的行数
fmt.Println(result.RowsAffected) // 输出:更新的行数