GORM基础入门使用教程

目录

前言

基本CRUD

插入数据

[1. 插入单条记录](#1. 插入单条记录)

[2. 批量插入](#2. 批量插入)

[3. 高级用法](#3. 高级用法)

删除数据

[1. 删除的几种方式](#1. 删除的几种方式)

[2. 删除模式](#2. 删除模式)

[1. 软删除 (默认行为)](#1. 软删除 (默认行为))

[2. 物理删除 (永久删除)](#2. 物理删除 (永久删除))

更新数据

[1. 使用 Struct 更新 (忽略零值)](#1. 使用 Struct 更新 (忽略零值))

[2. 使用 Map 更新 (包含所有值)](#2. 使用 Map 更新 (包含所有值))

查询数据

[查询方法:First, Find, Last, Take](#查询方法:First, Find, Last, Take)

[1. 字符串条件](#1. 字符串条件)

[2. Struct & Map 条件](#2. Struct & Map 条件)

[3. 其他常用条件](#3. 其他常用条件)

其他查询方法

关联模式

[1. Belongs To (属于)](#1. Belongs To (属于))

[2. Has One (拥有一个)](#2. Has One (拥有一个))

[3. Has Many (拥有多个)](#3. Has Many (拥有多个))

[4. Many To Many (多对多)](#4. Many To Many (多对多))

关联操作的一些技巧

[预加载 (Preload) - 解决 N+1 查询问题](#预加载 (Preload) - 解决 N+1 查询问题)

[关联模式 - 精细化操作](#关联模式 - 精细化操作)

自定义外键和引用

关联模式总结

上下文Context

[1. 基础用法:设置超时](#1. 基础用法:设置超时)

[2. 在 Web 中间件中使用](#2. 在 Web 中间件中使用)

[3. 在钩子函数中的使用](#3. 在钩子函数中的使用)

Transaction (事务)

方式一:标准事务 (推荐)

方式二:手动事务 (Begin/Commit/Rollback)

进阶:嵌套事务 (SavePoint)


前言

ORM是一种编程技术,针对各类语言,ORM框架可以将数据库表和代码中的类(结构体)建立映射,从而支持开发者通过操作对象完成对数据库的CURD。

ORM的三大核心思想

  • 数据库表和类(结构体)映射。

  • 表中列字段和类(结构体)属性映射。

  • 对象操作转换为SQL语句。

三大核心思想实际就是对应了数据库的三个核心模块:表结构、列结构、SQL

ORM框架的优点

  • 避免手写SQL语句,提高开发效率。

  • 规避了手写SQL语句容易出错的问题。

  • 支持多种数据库,代码具有移植性。

ORM框架的缺点

  • ORM框架自动生成的SQL语句不如手写SQL高效。

  • 存在学习成本,需要开发者了解对应ORM框架的使用规则。

其中Gorm库是go语言中一个非常强大的ORM(对象关系映射)库,它能够帮助我们很好的操作数据库和映射数据库表为go对象。

基本CRUD

插入数据

Create()方法用于将数据插入到数据库中。它既支持插入单条记录,也支持批量插入。

1. 插入单条记录
Go 复制代码
user := User{Name: "Alice", Age: 30}
result := db.Create(&user)
2. 批量插入
Go 复制代码
users := []User{
    {Name: "Bob", Age: 25},
    {Name: "Charlie", Age: 35},
}
db.Create(&users)
3. 高级用法
  • 指定字段插入 : 使用Select()方法只插入指定的字段。

    Go 复制代码
    // 只插入 Name 和 Age 字段,其他字段使用数据库默认值
    db.Select("Name", "Age").Create(&user)
  • 忽略字段插入 : 使用Omit()方法忽略不想插入的字段。

    Go 复制代码
    // 插入时忽略 "Role" 字段
    db.Omit("Role").Create(&user)

删除数据

1. 删除的几种方式
  • 根据主键删除

    Go 复制代码
    // 删除主键为 10 的记录
    db.Delete(&User{}, 10)
    // 批量删除主键为 1, 2, 3 的记录
    db.Delete(&User{}, []int{1, 2, 3})
  • 根据条件删除

    Go 复制代码
    // 删除所有 age < 18 的用户
    db.Where("age < ?", 18).Delete(&User{})
  • 删除已查询出的对象

    Go 复制代码
    var user User
    db.First(&user, 10) // 先查询出 ID 为 10 的用户
    db.Delete(&user)    // 再删除该对象
2. 删除模式
1. 软删除 (默认行为)

如果你的模型中嵌入了 gorm.Model (它包含 ID, CreatedAt, UpdatedAt, DeletedAt 字段),GORM 在执行 Delete 时并不会真正地从数据库中移除记录,而是将 **deleted_at**字段更新为当前时间。

  • 普通查询会自动过滤 : 之后的常规查询(如 Find, First )会自动带上 **WHERE deleted_at IS NULL**条件,所以你看不到这条被删除的数据。

  • 查询被软删除的数据 : 可以使用 Unscoped() 方法来查询包含已软删除的记录。

    Go 复制代码
    var user User
    // 查询所有 age=20 的用户,包括已软删除的
    db.Unscoped().Where("age = ?", 20).Find(&user)
2. 物理删除 (永久删除)

如果你想真正地、永久地从数据库中删除一条记录,必须使用 **Unscoped()**方法。

Go 复制代码
// 永久删除 ID 为 10 的用户
db.Unscoped().Delete(&User{}, 10)

更新数据

更新数据一般使用Updates()方法, 其接受两种类型的参数:结构体(struct)和映射(map),它们处理零值的方式截然不同。

1. 使用 Struct 更新 (忽略零值)

当你传入一个结构体时,GORM 默认只会更新其中的非零值字段。零值(如0, " ", false, nil)会被自动忽略,数据库中对应字段的值将保持不变。

Go 复制代码
// 假设 user 的 ID 为 1
user := User{Name: "Bob", Age: 0, Active: false}
db.Model(&user).Updates(user)
// 生成的 SQL: UPDATE users SET name='Bob' WHERE id=1;
// 注意:Age 和 Active 字段因为是零值,所以没有被更新。
2. 使用 Map 更新 (包含所有值)

当你传入一个 map[string]interface{} 时,GORM 会更新 Map 中指定的所有键值对,包括零值。

Go 复制代码
// 假设 user 的 ID 为 1
db.Model(&user).Updates(map[string]interface{}{
    "name":   "Bob",
    "age":    0,
    "active": false,
})
// 生成的 SQL: UPDATE users SET name='Bob', age=0, active=false WHERE id=1;
// 所有字段都被更新了,包括零值。

如果想要更新一个字段为零值,使用 map 是最直接的方式。同时更新也可以选中对应或者忽略相应字段进行更新:

  1. 使用Select()指定更新字段
Go 复制代码
// 只更新 Name 字段,即使 Age 有值也会被忽略
db.Model(&user).Select("name").Updates(User{Name: "Bob", Age: 100})
// 生成的 SQL: UPDATE users SET name='Bob' WHERE id=1;
  1. 使用Omit()忽略更新字段
Go 复制代码
// 更新除 Age 之外的所有非零值字段
db.Model(&user).Omit("age").Updates(User{Name: "Bob", Age: 100})
// 生成的 SQL: UPDATE users SET name='Bob' WHERE id=1;

查询数据

查询方法:First, Find, Last, Take

这四个是 GORM 中基础用法,用于执行查询并将结果填充到你的结构体或切片中。

方法 查询数量 默认排序 找不到记录时
First 单条 按主键升序 (ORDER BY id ASC) 返回 ErrRecordNotFound 错误
Last 单条 按主键降序 (ORDER BY id DESC) 返回 ErrRecordNotFound 错误
Take 单条 无排序 返回 ErrRecordNotFound 错误
Find 多条 (或单条) 无排序 不返回错误,RowsAffected 为 0

使用示例:

Go 复制代码
var user User
var users []User

// 1. First: 查询第一条记录
db.First(&user) 
// SQL: SELECT * FROM users ORDER BY id LIMIT 1;

// 2. Last: 查询最后一条记录
db.Last(&user)  
// SQL: SELECT * FROM users ORDER BY id DESC LIMIT 1;

// 3. Take: 查询任意一条记录(不保证顺序,性能稍好)
db.Take(&user)  
// SQL: SELECT * FROM users LIMIT 1;

// 4. Find: 查询所有记录
db.Find(&users) 
// SQL: SELECT * FROM users;

// Find 也可以查询单条,但不会报错
db.Where("id = ?", 1).Find(&user) 

在调用上述核心方法前,可以使用链式方法来构建复杂的查询条件。

1. 字符串条件

最接近原生 SQL 的方式,功能最强大。

Go 复制代码
// 等于
db.Where("name = ?", "jinzhu").First(&user)

// IN 查询
db.Where("id IN ?", []int{1, 2, 3}).Find(&users)

// LIKE 模糊查询
db.Where("name LIKE ?", "%jin%").Find(&users)

// 范围查询
db.Where("age >= ? AND age < ?", 18, 30).Find(&users)
2. Struct & Map 条件

GORM 会自动将结构体或 Map 中非零值的字段作为查询条件。

Go 复制代码
// Struct 查询
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SQL: SELECT * FROM users WHERE name = 'jinzhu' AND age = 20 LIMIT 1;

// Map 查询
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)

注意 :如果结构体的主键字段有值,FirstFind 等方法会自动将其作为查询条件。

3. 其他常用条件
Go 复制代码
// Not 条件
db.Not("name", "jinzhu").Find(&users) // name != 'jinzhu'

// Or 条件
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)

其中有一个 GORM 最容易遇到的问题。就是当使用一个已经包含主键值的结构体进行查询时,GORM 会自动将主键条件加入到 SQL 中,这可能导致查不到预期的数据。

错误示例:

Go 复制代码
user := User{ID: 10} // 假设你想用这个 user 变量去查 ID=20 的数据
db.First(&user, 20)  // 你以为会查 ID=20,但实际上...
// 生成的 SQL: SELECT * FROM users WHERE id = 10 ORDER BY id LIMIT 1;
// 因为 GORM 把 user.ID=10 也作为条件拼进去了!

正确做法:

  1. 清空结构体 :在复用结构体变量前,将其重置为零值。

    Go 复制代码
    user = User{} 
    db.First(&user, 20)
  2. 使用 Where :显式指定条件,避免歧义。

    Go 复制代码
    db.Where("id = ?", 20).First(&user)
其他查询方法
  • Count : 获取匹配的记录总数。

    Go 复制代码
    var count int64
    db.Model(&User{}).Where("active = ?", true).Count(&count)
  • Pluck : 只查询指定的单个列,并填充到切片中。

    Go 复制代码
    var names []string
    db.Model(&User{}).Where("active = ?", true).Pluck("name", &names)
    // SQL: SELECT name FROM users WHERE active = true;
  • Select : 指定要查询的字段,常与 ScanFind 配合使用。

    Go 复制代码
    var results []User
    db.Select("name", "age").Where("id > ?", 10).Find(&results)
  • Raw : 执行原生 SQL 查询,用于处理 GORM 无法直接表达的复杂查询。

    Go 复制代码
    var results []User
    db.Raw("SELECT * FROM users WHERE name = ?", "jinzhu").Scan(&results)

总而言之,Find 是最通用的查询方法,而**First/Last/Take** 用于获取单条记录。配合**WhereNotOr**等链式方法,可以构建出绝大多数业务所需的查询。

关联模式

GORM 的关联模式是其 ORM 能力的核心,它能够以声明式的方式定义模型之间的关系,并自动处理复杂的数据库操作。

GORM 将关联关系分为以下四种基本类型:

1. Belongs To (属于)

核心思想: 多的一方属于一的一方。外键定义在当前模型(拥有者)的表中。

典型场景: 订单(Order)属于用户(User),评论(Comment)属于文章(Article)。

模型定义:

Go 复制代码
type User struct {
    ID   uint
    Name string
}

type Order struct {
    ID     uint
    UserID uint      // 外键字段,默认命名规则是 "模型名 + ID"
    User   *User     // 关联字段,指向 User 模型
    Amount float64
}

在这个例子中,Order 模型 Belongs To Userorders 表中会有一个 user_id 外键。

2. Has One (拥有一个)

核心思想: 一的一方拥有一个一的关联模型。外键定义在关联模型(被拥有者)的表中。

典型场景: 用户(User)拥有一个个人资料(Profile),公民(Citizen)拥有一个身份证(IDCard)。

模型定义:

Go 复制代码
type User struct {
    ID      uint
    Name    string
    Profile Profile // 关联字段
}

type Profile struct {
    ID     uint
    UserID uint   // 外键字段,指向 User 模型的 ID
    Bio    string
}

User 模型 Has One Profile profiles 表中会有一个 user_id 外键。

3. Has Many (拥有多个)

核心思想: "一"的一方拥有多个"多"的关联模型。外键定义在关联模型(被拥有者)的表中。

典型场景: 用户(User)拥有多个订单(Order),文章(Article)拥有多个评论(Comment)。

模型定义:

Go 复制代码
type User struct {
    ID     uint
    Name   string
    Orders []Order // 关联字段,使用切片表示"多"
}

type Order struct {
    ID     uint
    UserID uint   // 外键字段,指向 User 模型的 ID
    Amount float64
}

User 模型 Has Many Orderorders 表中会有一个 user_id 外键。

4. Many To Many (多对多)

核心思想: 两个模型互相拥有多个对方。这种关系需要一个中间连接表(Join Table)来实现。

典型场景: 用户(User)和角色(Role),文章(Article)和标签(Tag)。

模型定义:

Go 复制代码
type User struct {
    ID    uint
    Name  string
    Roles []Role `gorm:"many2many:user_roles;"` // 关联字段,指定中间表名
}

type Role struct {
    ID   uint
    Name string
    // 反向关联,中间表名必须一致
    Users []User `gorm:"many2many:user_roles;"`
}

GORM 会自动创建并使用 user_roles 这个中间表,该表通常包含 user_idrole_id 两个外键。

关联操作的一些技巧

预加载 (Preload) - 解决 N+1 查询问题

当你查询主模型并访问其关联数据时,如果没有预加载,GORM 会为每一条关联数据单独发起一次数据库查询,这就是著名的N+1 查询问题,会严重影响性能。

反模式 (N+1 查询):

Go 复制代码
var orders []Order
db.Find(&orders) // 1次查询
for _, order := range orders {
    fmt.Println(order.User.Name) // 循环 N 次,触发 N 次查询
}
// 总共执行了 1 + N 次数据库查询

正模式 (使用 Preload):

Go 复制代码
var orders []Order
// Preload 会使用 JOIN 或额外的 IN 查询,一次性加载所有关联数据
db.Preload("User").Find(&orders)
// 总共只执行 2 次数据库查询
关联模式 - 精细化操作

GORM 提供了Association() 方法,让你可以对关联数据进行更精细化的操作,如添加、删除、统计等。

Go 复制代码
var user User
db.First(&user, 1)

// 1. 查找关联
var roles []Role
db.Model(&user).Association("Roles").Find(&roles)

// 2. 添加关联
role := Role{ID: 5, Name: "editor"}
db.Model(&user).Association("Roles").Append(&role)

// 3. 替换关联 (删除旧的,添加新的)
newRoles := []Role{{ID: 6, Name: "admin"}}
db.Model(&user).Association("Roles").Replace(newRoles)

// 4. 删除关联 (仅解除关系,不删除数据)
db.Model(&user).Association("Roles").Delete(&role)

// 5. 统计关联数量
var count int64
db.Model(&user).Association("Roles").Count(&count)
自定义外键和引用

GORM 的默认命名规则(如 UserID)很方便,但你也可以通过结构体标签gorm: "..."进行自定义。

Go 复制代码
type Author struct {
    ID   uint
    Name string
}

type Article struct {
    ID        uint
    AuthorID  uint    // 自定义外键字段名
    Author    *Author `gorm:"foreignKey:AuthorID"` // 指定外键
    Content   string
}

你也可以自定义引用哪个字段,而不是默认的主键 ID

Go 复制代码
type User struct {
    ID       uint
    Username string
    Profile  Profile `gorm:"references:Username"` // 引用 Username 字段
}

type Profile struct {
    ID       uint
    UserName string // 外键字段,关联到 User.Username
    Bio      string
}

关联模式总结

表格

关联类型 关系描述 外键位置
Belongs To 当前模型属于另一个模型 当前模型表
Has One 当前模型拥有一个关联模型 关联模型表
Has Many 当前模型拥有多个关联模型 关联模型表
Many To Many 两个模型互相关联 中间连接表

上下文Context

Context(上下文) 是你控制查询生命周期(比如超时控制、链路追踪)的"遥控器";而 Transaction(事务) 则是保证数据一致性(要么全成功,要么全失败)的"安全网"。

在 Web 开发中,Context 非常重要。它主要用于:

  1. 控制超时:防止烂 SQL 把数据库连接池拖死。
  2. 传递追踪 ID:在日志中串联整个请求链路。
  3. 取消操作:如果用户取消了请求,数据库查询也应该立即停止
1. 基础用法:设置超时

GORM 支持通过 WithContext() 方法传递上下文。

这是最常见的场景。比如规定这个查询必须在 2 秒内完成,否则强制取消。

Go 复制代码
import (
    "context"
    "time"
)

// 创建一个 2 秒超时的 Context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 务必在操作结束后释放资源

var user User
// 将 ctx 传递给 GORM
err := db.WithContext(ctx).First(&user, 10).Error

if err != nil {
    if err == context.DeadlineExceeded {
        // 处理超时错误
        fmt.Println("查询超时了!")
    } else {
        // 处理其他错误
        fmt.Println("查询出错:", err)
    }
}
2. 在 Web 中间件中使用

通常在 HTTP 请求进来时,把 Request 自带的 Context 传给 GORM。这样当客户端断开连接时,数据库查询也会自动终止。

Go 复制代码
// 假设在 Gin 的 Handler 中
func GetUserHandler(c *gin.Context) {
    var user User
    // 直接使用 c.Request.Context()
    // 这样如果用户浏览器关闭,这个查询也会立刻停止,不再浪费数据库资源
    err := db.WithContext(c.Request.Context()).First(&user, 1).Error
    
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, user)
}
3. 在钩子函数中的使用

我们也可以在 **BeforeCreate()**等钩子函数中获取 Context,用于记录日志或做权限校验。

Go 复制代码
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    // 从 tx.Statement.Context 获取上下文
    ctx := tx.Statement.Context
    
    // 比如从上下文中取出 TraceID 记录到日志里
    traceID := ctx.Value("trace_id")
    fmt.Println("正在创建用户,TraceID:", traceID)
    return nil
}

Transaction (事务)

事务的核心是 ACID 特性。在 GORM 中,处理事务主要有两种方式:推荐的标准方式(闭包)和手动方式。

方式一:标准事务 (推荐)

这是最安全、最简洁的写法。GORM 会自动帮你处理Commit(提交)和Rollback(回滚)。我们只需要关注业务逻辑,如果函数返回error,事务就会自动回滚。

场景:用户转账

A 给 B 转账 100 元。步骤:1. A 扣钱;2. B 加钱。这两步必须同时成功,或者同时失败。

Go 复制代码
// db 是全局的 *gorm.DB 实例
func Transfer(tx *gorm.DB, fromID, toID uint, amount float64) error {
    // 1. 开启事务 (使用闭包方式)
    return tx.Transaction(func(tx *gorm.DB) error {
        var fromUser, toUser User

        // 2. 锁定并查询转出账户 (使用 tx 而不是 db!)
        // ForUpdate 是数据库层面的行锁,防止并发扣款
        if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&fromUser, fromID).Error; err != nil {
            return err // 返回 error,自动回滚
        }

        // 3. 检查余额
        if fromUser.Balance < amount {
            return errors.New("余额不足") // 返回 error,自动回滚
        }

        // 4. 扣款
        if err := tx.Model(&fromUser).Update("balance", fromUser.Balance - amount).Error; err != nil {
            return err
        }

        // 5. 加款
        if err := tx.Model(&toUser).First(&toUser, toID).Update("balance", toUser.Balance + amount).Error; err != nil {
            return err
        }

        // 6. 没有任何错误返回,自动提交事务
        return nil
    })
}

关键点:

  • 使用 tx :在闭包内部,必须使用传入的 tx 对象执行操作,不能用全局的 db,否则操作不会在同一个事务里。
  • 自动回滚:只要return error,GORM 就会帮我们回滚。
方式二:手动事务 (Begin/Commit/Rollback)

这种方式更灵活,但也更繁琐,容易忘记回滚。通常用于复杂的逻辑控制,或者需要在事务中调用其他函数时。

Go 复制代码
func ManualTransaction() error {
    // 1. 开始事务
    tx := db.Begin()
    
    // 2. 必须用 defer 保证异常发生时能回滚
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    // 3. 执行操作 (记得用 tx)
    if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
        tx.Rollback() // 出错手动回滚
        return err
    }

    // ... 更多操作 ...

    // 4. 提交事务
    if err := tx.Commit().Error; err != nil {
        tx.Rollback() // 提交失败也要回滚
        return err
    }
    
    return nil
}
进阶:嵌套事务 (SavePoint)

GORM 支持嵌套事务。外层事务开启后,内层事务会创建一个保存点(SavePoint)。如果内层失败,可以只回滚到保存点,而不影响外层已经成功的操作。

Go 复制代码
db.Transaction(func(tx *gorm.DB) error {
    // --- 外层事务 ---
    tx.Create(&User{Name: "OuterUser"})

    // --- 内层事务 ---
    // 如果这里出错,只会回滚 InnerUser,OuterUser 依然存在
    return tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&User{Name: "InnerUser"})
        // return errors.New("模拟内层失败") // 如果取消注释,InnerUser 会被回滚
        return nil 
    })
})

这两个模块配合使用效果更佳:比如在事务中传入带有超时控制的 Context,既保证了数据安全,又防止了死锁导致的长时间阻塞。

相关推荐
升职佳兴2 小时前
SQL 进阶4:查询从未下单的用户与 NOT EXISTS 完整解析
数据库·sql
呆萌很2 小时前
【GO】结构体定义练习题
golang
光泽雨2 小时前
数据库中的DCL
数据库
星辰_mya2 小时前
【无标题】
数据库·后端·面试·架构师
一条闲鱼_mytube3 小时前
【深入理解】HTTP/3 与 QUIC 协议:从原理到 Go 语言实战
网络协议·http·golang
Yvonne爱编码3 小时前
数据库---Day6 数据库约束
数据库
空太Jun3 小时前
Spring Security 自定义数据库认证(初尝试)
java·数据库·spring
麦德泽特3 小时前
基于 Go 语言的 Modbus 项目实战:构建高性能、可扩展的工业通信服务器
服务器·开发语言·golang·modbus·rtu