gorm笔记

gorm笔记
安安GORM 操 MySQL
GORM 是 Go 语言中一个功能强大的 ORM 库,支持 MySQL 等多种数据库。它简化了数据库操作,提供模型定义、迁移、CRUD、关联、钩子、事务等功能。本教程基于 GORM 官方文档,旨在帮助你达到能独立开发项目的水平。我们将逐步讲解核心概念、代码示例和最佳实践。假设你有基本的 Go 知识和 MySQL 环境。
1. 安装 GORM 和 MySQL 驱动
首先,在你的 Go 项目中安装 GORM 核心库和 MySQL 驱动:
1 | go get -u gorm.io/gorm |
- 最佳实践:使用 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 | import ( |
配置选项
使用 mysql.New(mysql.Config{}) 自定义驱动:
DefaultStringSize: 256:默认字符串字段大小。DisableDatetimePrecision: true:禁用 datetime 精度(MySQL 5.6 前不支持)。DontSupportRenameIndex: true:重命名索引时删除并重建(MySQL 5.7 前不支持)。DontSupportRenameColumn: true:重命名列时使用 change(MySQL 8 前不支持)。
示例:
1 | db, err := gorm.Open(mysql.New(mysql.Config{ |
连接池配置
GORM 使用 database/sql 管理连接池,优化性能:
1 | sqlDB, err := db.DB() |
最佳实践
- 在生产环境中,使用环境变量存储 DSN,避免硬编码凭证。
- 监控连接池:过多的连接可能导致 MySQL 负载过高。
- 对于现有 SQL 连接:
gorm.Open(mysql.New(mysql.Config{Conn: sqlDB}), &gorm.Config{})。
3. 定义模型
模型是 Go 结构体,与数据库表对应。GORM 支持基本类型、指针(可空)、自定义类型(实现 Scanner/Valuer 接口)。
基本示例
1 | import ( |
约定和标签
约定:
- 主键:默认
ID。 - 表名:结构体名 snake_case + 复数(
User->users)。 - 列名:字段名 snake_case。
- 时间戳:
CreatedAt、UpdatedAt自动跟踪。
- 主键:默认
标签(Tags):
primaryKey:指定主键。not null:非空。unique:唯一。default:'value':默认值。size:255:长度。index:索引(见性能部分)。autoCreateTime/autoUpdateTime:自动时间戳,支持 UNIX 时间(int)。- 权限:
<-:create(只写)、->(只读)、-(忽略)。
嵌入结构体:
1
2
3
4
5
6
7
8type Address struct {
City string
}
type User struct {
gorm.Model
Address Address `gorm:"embedded;embeddedPrefix:addr_"`
}
// 列:addr_city
最佳实践
- 使用
gorm.Model嵌入标准字段。 - 对于大项目,定义自定义基模型以添加通用字段(如 TenantID)。
- 避免非导出字段(小写),GORM 会忽略它们。
4. 数据库迁移
迁移用于创建/更新表结构。
AutoMigrate
自动迁移模型:
1 | db.AutoMigrate(&User{}) |
- 创建表、缺失外键、约束、列、索引。
- 修改列类型(如果大小/精度变化)。
- 不删除 未用列(保护数据)。
选项:
1 | db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) |
禁用外键:
1 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ |
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
3user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // INSERT INTO users ...
// result.Error, result.RowsAffected批量:
1
2
3users := []User{{Name: "A"}, {Name: "B"}}
db.Create(&users)
// 或分批:db.CreateInBatches(&users, 100)选择字段:
1
2db.Select("Name", "Age").Create(&user)
// 忽略:db.Omit("Birthday").Create(&user)
最佳实践:大批量数据使用 CreateInBatches 避免 SQL 过长。整合钩子验证数据。
查询(Read)
支持条件、排序、分页、预加载。
单条:
1
2
3var user User
db.First(&user, 10) // SELECT ... WHERE id=10 LIMIT 1
// 检查未找到:if errors.Is(db.Error, gorm.ErrRecordNotFound) {}所有:
1
2var 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 | type User struct { |
Has One
一对一,外键在子表。
1 | type User struct { |
Has Many
一对多,外键在子表。
1 | type User struct { |
Many To Many
多对多,使用连接表。
1 | type User struct { |
创建关联:
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
2db.Preload("Orders").Preload("CreditCard").Find(&users)
// 嵌套:db.Preload("Orders.Items").Find(&users)Joins(单查询,适合一对一):
1
2db.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
11func (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
12tx := 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
7db.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
3tx.SavePoint("sp1")
// 操作
tx.RollbackTo("sp1")
禁用默认事务:&gorm.Config{SkipDefaultTransaction: true}(性能提升,但牺牲一致性)。
最佳实践:复杂操作(如转账)用事务。嵌套用于子操作独立回滚。
10. 错误处理
始终检查错误。
基本:
1
2
3
4
5
6
7if 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 stringgorm:”index”` - 唯一:
gorm:"uniqueIndex" - 复合:相同索引名,如
Name stringgorm:”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
2var 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
4sql := 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),大事务拆分,迁移版本控制。

