gorm笔记

GORM 操 MySQL

GORM 是 Go 语言中一个功能强大的 ORM 库,支持 MySQL 等多种数据库。它简化了数据库操作,提供模型定义、迁移、CRUD、关联、钩子、事务等功能。本教程基于 GORM 官方文档,旨在帮助你达到能独立开发项目的水平。我们将逐步讲解核心概念、代码示例和最佳实践。假设你有基本的 Go 知识和 MySQL 环境。

1. 安装 GORM 和 MySQL 驱动

首先,在你的 Go 项目中安装 GORM 核心库和 MySQL 驱动:

1
2
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
  • 最佳实践:使用 Go Modules 管理依赖,确保项目中使用一致的版本。安装后,导入包:import "gorm.io/gorm"import "gorm.io/driver/mysql"

2. 连接到 MySQL 数据库

连接是第一步,使用 gorm.Open 函数和 DSN(Data Source Name)字符串。

DSN 格式

标准 DSN 示例:

1
user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
  • user:pass:用户名和密码。
  • tcp(127.0.0.1:3306):主机和端口。
  • dbname:数据库名。
  • 参数:
    • charset=utf8mb4:支持完整 UTF-8 编码(推荐,避免 emoji 等问题)。
    • parseTime=True:正确解析 time.Time 类型。
    • loc=Local:使用本地时区。

连接代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)

func main() {
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接数据库失败: " + err.Error())
}
// 使用 db 进行操作
}

配置选项

使用 mysql.New(mysql.Config{}) 自定义驱动:

  • DefaultStringSize: 256:默认字符串字段大小。
  • DisableDatetimePrecision: true:禁用 datetime 精度(MySQL 5.6 前不支持)。
  • DontSupportRenameIndex: true:重命名索引时删除并重建(MySQL 5.7 前不支持)。
  • DontSupportRenameColumn: true:重命名列时使用 change(MySQL 8 前不支持)。

示例:

1
2
3
4
5
6
7
8
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: dsn,
DefaultStringSize: 191, // 对于 utf8mb4,防止索引过长
DisableDatetimePrecision: true,
DontSupportRenameIndex: true,
DontSupportRenameColumn: true,
SkipInitializeWithVersion: false,
}), &gorm.Config{})

连接池配置

GORM 使用 database/sql 管理连接池,优化性能:

1
2
3
4
5
6
7
sqlDB, err := db.DB()
if err != nil {
panic(err)
}
sqlDB.SetMaxIdleConns(10) // 空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间

最佳实践

  • 在生产环境中,使用环境变量存储 DSN,避免硬编码凭证。
  • 监控连接池:过多的连接可能导致 MySQL 负载过高。
  • 对于现有 SQL 连接:gorm.Open(mysql.New(mysql.Config{Conn: sqlDB}), &gorm.Config{})

3. 定义模型

模型是 Go 结构体,与数据库表对应。GORM 支持基本类型、指针(可空)、自定义类型(实现 Scanner/Valuer 接口)。

基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"gorm.io/gorm"
"time"
"database/sql"
)

type User struct {
gorm.Model // 嵌入:ID (uint), CreatedAt, UpdatedAt, DeletedAt (软删除)
Name string `gorm:"size:255;index"` // 字符串,长度 255,添加索引
Age uint8 `gorm:"default:18"` // 默认值
Email *string `gorm:"unique"` // 可空,唯一
Birthday *time.Time
MemberNumber sql.NullString // 可空字符串
}

约定和标签

  • 约定

    • 主键:默认 ID
    • 表名:结构体名 snake_case + 复数(User -> users)。
    • 列名:字段名 snake_case。
    • 时间戳:CreatedAtUpdatedAt 自动跟踪。
  • 标签(Tags)

    • primaryKey:指定主键。
    • not null:非空。
    • unique:唯一。
    • default:'value':默认值。
    • size:255:长度。
    • index:索引(见性能部分)。
    • autoCreateTime / autoUpdateTime:自动时间戳,支持 UNIX 时间(int)。
    • 权限:<-:create(只写)、->(只读)、-(忽略)。
  • 嵌入结构体

    1
    2
    3
    4
    5
    6
    7
    8
    type Address struct {
    City string
    }
    type User struct {
    gorm.Model
    Address Address `gorm:"embedded;embeddedPrefix:addr_"`
    }
    // 列:addr_city

最佳实践

  • 使用 gorm.Model 嵌入标准字段。
  • 对于大项目,定义自定义基模型以添加通用字段(如 TenantID)。
  • 避免非导出字段(小写),GORM 会忽略它们。

4. 数据库迁移

迁移用于创建/更新表结构。

AutoMigrate

自动迁移模型:

1
2
db.AutoMigrate(&User{})
// 或多个:db.AutoMigrate(&User{}, &Product{})
  • 创建表、缺失外键、约束、列、索引。
  • 修改列类型(如果大小/精度变化)。
  • 不删除 未用列(保护数据)。

选项:

1
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

禁用外键:

1
2
3
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})

Migrator 接口(自定义迁移)

用于精细控制:

  • 创建表:db.Migrator().CreateTable(&User{})
  • 添加列:db.Migrator().AddColumn(&User{}, "Name")
  • 重命名:db.Migrator().RenameColumn(&User{}, "Name", "NewName")
  • 视图:db.Migrator().CreateView(&View{}, gorm.ViewOption{Query: db.Model(&User{}).Where("age > ?", 20)})
  • 约束:db.Migrator().CreateConstraint(&User{}, "fk_users_company")

索引和约束(见性能部分)

最佳实践

  • 开发中使用 AutoMigrate,生产中使用自定义迁移脚本(版本控制,如 goose 或 migrator)。
  • 测试迁移:运行在测试数据库,确保无数据丢失。

5. CRUD 操作

创建(Create)

支持单个、批量、选择字段。

  • 单条:

    1
    2
    3
    user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
    result := db.Create(&user) // INSERT INTO users ...
    // result.Error, result.RowsAffected
  • 批量:

    1
    2
    3
    users := []User{{Name: "A"}, {Name: "B"}}
    db.Create(&users)
    // 或分批:db.CreateInBatches(&users, 100)
  • 选择字段:

    1
    2
    db.Select("Name", "Age").Create(&user)
    // 忽略:db.Omit("Birthday").Create(&user)

最佳实践:大批量数据使用 CreateInBatches 避免 SQL 过长。整合钩子验证数据。

查询(Read)

支持条件、排序、分页、预加载。

  • 单条:

    1
    2
    3
    var user User
    db.First(&user, 10) // SELECT ... WHERE id=10 LIMIT 1
    // 检查未找到:if errors.Is(db.Error, gorm.ErrRecordNotFound) {}
  • 所有:

    1
    2
    var users []User
    db.Find(&users)
  • 条件:

    • 字符串:db.Where("name = ? AND age > ?", "Jinzhu", 18).Find(&users)
    • 结构体:db.Where(&User{Name: "Jinzhu"}).Find(&users)(忽略零值)
    • Map:db.Where(map[string]interface{}{"name": "Jinzhu", "age": 0}).Find(&users)
    • Not/Or:db.Not("name = ?", "Jinzhu").Or("age > ?", 20).Find(&users)
  • 排序/分页:

    1
    db.Order("age desc").Limit(10).Offset(20).Find(&users)
  • 选择字段:

    1
    db.Select("name, age").Find(&users)
  • Group/Having:

    1
    db.Group("name").Having("count(name) > 1").Find(&users)

最佳实践:使用预加载避免 N+1 查询(见下)。对于复杂查询,使用子查询或原始 SQL。

更新(Update)

需要条件避免全局更新。

  • 单列:

    1
    db.Model(&User{}).Where("id = ?", 10).Update("name", "NewName")
  • 多列:

    • 结构体:db.Model(&user).Updates(User{Name: "New", Age: 20})(更新非零值)
    • Map:db.Model(&user).Updates(map[string]interface{}{"name": "New", "age": 20})
  • 批量:

    1
    db.Model(User{}).Where("age > ?", 20).Updates(User{Age: 21})
  • 保存所有字段:

    1
    db.Save(&user)

避免陷阱:无条件更新需 AllowGlobalUpdate: true。使用 Select 指定字段。

删除(Delete)

支持软删除(默认,如果有 DeletedAt)。

  • 单条:

    1
    db.Delete(&user, 10)  // 软删除:更新 deleted_at
  • 批量:

    1
    db.Where("age > ?", 20).Delete(&User{})
  • 硬删除:

    1
    db.Unscoped().Delete(&user)
  • 查询软删除:

    1
    db.Unscoped().Where("age = ?", 20).Find(&users)

最佳实践:生产中优先软删除,便于恢复数据。整合事务确保原子性。

6. 关联(Associations)

GORM 支持 Belongs To、Has One、Has Many、Many To Many。

Belongs To

子模型属于父模型,外键在子表。

1
2
3
4
5
6
7
8
9
type User struct {
gorm.Model
CompanyID uint
Company Company `gorm:"foreignKey:CompanyID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
}
type Company struct {
gorm.Model
Name string
}

Has One

一对一,外键在子表。

1
2
3
4
5
6
7
8
9
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"foreignKey:UserID"`
}
type CreditCard struct {
gorm.Model
UserID uint
Number string
}

Has Many

一对多,外键在子表。

1
2
3
4
5
6
7
8
type User struct {
gorm.Model
Orders []Order `gorm:"foreignKey:UserID"`
}
type Order struct {
gorm.Model
UserID uint
}

Many To Many

多对多,使用连接表。

1
2
3
4
5
6
7
8
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
}

创建关联:

1
db.Create(&user)  // 自动保存关联

管理:

  • Append:db.Model(&user).Association("Languages").Append(&Language{Name: "EN"})
  • Replace/Clear/Delete

最佳实践:定义外键约束(OnDelete: CASCADE 等)。使用迁移创建外键。

7. 预加载(Eager Loading)

避免 N+1 查询问题。

  • Preload(多查询):

    1
    2
    db.Preload("Orders").Preload("CreditCard").Find(&users)
    // 嵌套:db.Preload("Orders.Items").Find(&users)
  • Joins(单查询,适合一对一):

    1
    2
    db.Joins("Company").Find(&users)
    // 条件:db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)

最佳实践:大型项目中,总使用预加载或 Joins 优化查询。监控 SQL 日志。

8. 钩子(Hooks)

钩子是 CRUD 前/后执行的函数。

  • 示例(在模型中定义):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
    if u.Age < 18 {
    return errors.New("年龄太小")
    }
    return nil
    }
    func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
    // 日志记录
    log.Println("用户更新:", u.ID)
    return nil
    }

钩子列表:BeforeSave, BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate, BeforeDelete, AfterDelete, AfterFind。

跳过钩子:db.Session(&gorm.Session{SkipHooks: true}).Create(&user)

实际应用

  • 验证:BeforeCreate 检查数据有效性。
  • 日志:AfterUpdate 记录变更。
  • 加密:BeforeSave 加密密码。

最佳实践:钩子中返回错误会回滚事务。用于审计、默认值设置。

9. 事务(Transactions)

确保数据一致性。

  • 默认:Create/Update/Delete 在事务中。

  • 手动:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    tx := db.Begin()
    defer func() {
    if r := recover(); r != nil {
    tx.Rollback()
    } else if err != nil {
    tx.Rollback()
    } else {
    tx.Commit()
    }
    }()
    tx.Create(&user)
    tx.Create(&order)
  • 嵌套:

    1
    2
    3
    4
    5
    6
    7
    db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&user1)
    return tx.Transaction(func(tx2 *gorm.DB) error {
    tx2.Create(&user2)
    return errors.New("rollback user2") // 只回滚 user2
    })
    })
  • 保存点:

    1
    2
    3
    tx.SavePoint("sp1")
    // 操作
    tx.RollbackTo("sp1")

禁用默认事务:&gorm.Config{SkipDefaultTransaction: true}(性能提升,但牺牲一致性)。

最佳实践:复杂操作(如转账)用事务。嵌套用于子操作独立回滚。

10. 错误处理

始终检查错误。

  • 基本:

    1
    2
    3
    4
    5
    6
    7
    if err := db.Where("id = ?", 1).First(&user).Error; err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
    // 未找到
    } else {
    // 其他错误
    }
    }
  • 常见错误:

    • gorm.ErrRecordNotFound:未找到。
    • gorm.ErrDuplicatedKey:重复键(启用 TranslateError)。
    • gorm.ErrForeignKeyViolated:外键违反。
  • 数据库特定:解析如 MySQL 的 *mysql.MySQLError。

启用 TranslateError:&gorm.Config{TranslateError: true}

最佳实践:用 errors.Is 检查特定错误。记录错误日志,便于调试。

11. 性能优化:索引和约束

索引

在模型中使用标签:

  • 简单:Name string gorm:”index”`
  • 唯一:gorm:"uniqueIndex"
  • 复合:相同索引名,如 Name string gorm:”index:idx_user”,`Age int `gorm:"index:idx_user"
  • 优先级:gorm:"index:idx_user,priority:1"
  • 选项:gorm:"index:,type:btree,sort:desc,where:age>18"

迁移时自动创建。

约束

  • 检查:gorm:"check:age > 18"
  • 外键:见关联部分。

最佳实践:为频繁查询字段加索引(WHERE、JOIN、ORDER)。监控慢查询,调整索引。复合索引顺序影响性能(高选择性字段先)。

12. 原始 SQL 操作

对于复杂查询,使用 Raw/Exec。

  • Raw 查询:

    1
    2
    var user User
    db.Raw("SELECT * FROM users WHERE name = ?", "Jinzhu").Scan(&user)
  • Exec 执行:

    1
    db.Exec("UPDATE users SET age = age + 1 WHERE id = ?", 1)
  • ToSQL(调试):

    1
    2
    3
    4
    sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
    return tx.Model(&User{}).Where("id = ?", 1).Updates(User{Age: 20})
    })
    fmt.Println(sql) // 生成 SQL,不执行

命名参数:

1
db.Where("name = @name", sql.Named("name", "Jinzhu")).Find(&users)

最佳实践:GORM API 优先,原始 SQL 用于聚合、复杂 JOIN。防范 SQL 注入,使用占位符。

13. 项目级最佳实践

  • 结构:分层(模型、仓库、服务)。仓库封装 DB 操作。
  • 配置:使用 viper 等加载 DSN,从环境变量读取。
  • 测试:用 testify 测试模型/查询。Mock DB 或用 SQLite 测试。
  • 性能:启用 Logger(&gorm.Config{Logger: logger.Default.LogMode(logger.Info)})监控 SQL。使用连接池,索引优化。
  • 安全:避免全局更新,处理错误,加密敏感数据。
  • 高级:上下文(Context)用于超时/取消。泛型 API(Go 1.18+)类型安全。
  • 常见问题:时区问题(用 UTC),大事务拆分,迁移版本控制。
[up主专用,视频内嵌代码贴在这]