gorm golang对象关系映射框架
安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
连接数据库
sqlite
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
mysql
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
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{})
其他数据库或高级用法:https://gorm.io/zh_CN/docs/connecting_to_the_database.html
声明模型
例:
type Product struct {
gorm.Model
Code string
Price uint
}
GORM 通过将 Go 结构体(Go structs) 映射到数据库表来简化数据库交互。
模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql
包中的Scanner和Valuer接口)。
约定
- 主键:GORM 使用一个名为
ID
的字段作为每个模型的默认主键。 - 表名:默认情况下,GORM 将结构体名称转换为
snake_case
并为表名加上复数形式。 例如,一个User
结构体在数据库中的表名变为users
。 - 列名:GORM 自动将结构体字段名称转换为
snake_case
作为数据库中的列名。 - 时间戳字段:GORM使用字段
CreatedAt
和UpdatedAt
来自动跟踪记录的创建和更新时间。
gorm.Model
GORM提供了一个预定义的结构体,名为gorm.Model
,其中包含常用字段:
// gorm.Model 的定义
type Modelstruct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
- 将其嵌入在您的结构体中: 您可以直接在您的结构体中嵌入
gorm.Model
,以便自动包含这些字段。 这对于在不同模型之间保持一致性并利用GORM内置的约定非常有用,请参考嵌入结构。 包含的字段:
ID
:每个记录的唯一标识符(主键)。CreatedAt
:在创建记录时自动设置为当前时间。UpdatedAt
:每当记录更新时,自动更新为当前时间。DeletedAt
:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。
文档:https://gorm.io/zh_CN/docs/models.html
迁移(Migration)
迁移(Migration)是什么?
迁移是指对数据库结构进行版本控制的机制,它允许您:
- 以编程方式定义数据库表结构
- 跟踪数据库模式的变化历史
- 在不同环境(开发/测试/生产)之间保持数据库结构一致
- 回滚到之前的数据库版本
迁移与创建数据库的区别
特性 | 迁移(Migration) | 创建数据库 |
---|---|---|
范围 | 表结构、字段、索引等 | 整个数据库实例 |
功能 | 修改现有结构、版本控制 | 创建新的数据库容器 |
粒度 | 细粒度(表/字段级) | 粗粒度(数据库级) |
目的 | 模式演进 | 数据库初始化 |
GORM中的迁移实现
在GORM中,迁移通常通过AutoMigrate
方法实现:
// 自动迁移模型结构到数据库表
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 这会在数据库中创建对应的表(如果不存在)或修改表结构以匹配模型
迁移的典型用途
- 初始化表结构:创建新表
- 添加字段:向现有表添加新列
- 修改字段:更改字段类型或约束
- 删除字段:从表中移除列
- 创建索引:添加或删除索引
高级迁移功能
GORM还支持更复杂的迁移操作:
// 使用Migrator接口进行精细控制
m := db.Migrator()
// 检查表是否存在
m.HasTable(&User{})
// 创建表
m.CreateTable(&User{})
// 添加列
m.AddColumn(&User{}, "CreditCardNumber")
// 修改列
m.AlterColumn(&User{}, "Name")
// 删除列
m.DropColumn(&User{}, "Age")
基本操作(CRUD)
// Create
db.Create(&Product{Code: "D42", Price: 100})
// Read
var product Product
db.First(&product, 1) // 根据整型主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
// Update - 将 product 的 price 更新为 200
db.Model(&product).Update("Price", 200)
// Update - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - 删除 product
db.Delete(&product, 1)
注:
Product是一个结构体
type Product struct {
gorm.Model
Code string
Price uint
}
对于CRUD的每一个操作都是传入一个地址,直接修改作用的对象
create
例如对于create来说,就是传入一个Product类型的结构体。传入的结构体实例不需要为每个字段都赋值。
基本赋值规则:
- 部分赋值:可以只为结构体的部分字段赋值,其他字段将保持零值或默认值
零值处理:未赋值的字段会根据类型自动设置为相应的零值:
- 数字类型:0
- 字符串:""
- 布尔:false
- 指针:nil
- 时间:0001-01-01 00:00:00 +0000 UTC
示例代码
type User struct {
ID uint
Name string
Age int
Email *string // 指针类型
IsActive bool
CreatedAt time.Time
}
// 只设置部分字段
user := User{
Name: "张三",
Age: 25,
// 其他字段未设置
}
result := db.Create(&user) // 成功插入,未设置字段使用零值
特殊情况处理
指针字段:
- 如果字段是指针类型且未赋值,GORM会将其作为NULL插入
- 如果想明确设置NULL,可以使用指针的nil值
// 明确设置Email为NULL user := User{ Name: "李四", Email: nil, // 将存储为NULL }
默认值:
- 可以在模型定义中使用
gorm
标签设置默认值 - 未赋值时会使用默认值而非零值
type Product struct { Code string Price float64 `gorm:"default:100"` // 默认值100 }
- 可以在模型定义中使用
忽略字段:
- 使用
gorm:"-"
标签可以完全忽略字段,不会被插入
type User struct { Name string Temp string `gorm:"-"` // 不会存入数据库 }
- 使用
数据库约束的影响
NOT NULL约束:
- 如果字段在数据库中有NOT NULL约束且没有默认值
- 尝试插入时会返回错误
主键:
- 如果使用自动递增主键,可以省略ID字段
- 如果想手动设置ID,可以显式赋值
最佳实践
- 只设置业务需要的字段,让其他字段保持零值
- 对于可选字段,考虑使用指针类型以便明确表示NULL
- 重要的必填字段应该在模型层或数据库层添加验证
- 对于有默认值的字段,可以省略赋值
read
对于read,可以现在外面定义一个新的对应类型的变量,然后将这个变量传入,因为传的是地址,所以执行完,read之后对于product就是我们需要的一个变量,对应着数据库表中特定的某一行。
update
不可以直接使用未经过Read赋值的product变量进行Update操作。这样会导致更新失败或产生意外行为。
Update操作的工作机制
GORM的Update操作需要两个关键信息:
- 要更新的模型实例:用于确定更新哪条记录
- 更新内容:指定要修改的字段和值
为什么需要先Read
主键标识:
- Update操作需要知道要更新哪条记录,这通常通过模型的主键(ID)来确定
- 未经Read的product变量没有有效的ID值
模型状态:
- GORM会跟踪模型的状态(是否从数据库加载)
- 未加载的模型无法正确执行更新
具体问题分析
var product Product // 未赋值的product变量
db.Model(&product).Update("Price", 200)
这种情况下:
product
的ID字段为0(零值)- GORM会尝试更新ID=0的记录,这通常不是您想要的
- 如果没有ID=0的记录,则不会更新任何数据
正确做法
先查询再更新 (推荐):
var product Product db.First(&product, 1) // 或通过其他条件查询 db.Model(&product).Update("Price", 200)
直接指定条件更新 (不依赖模型实例):
// 更新所有code="D42"的记录 db.Model(&Product{}).Where("code = ?", "D42").Update("Price", 200) // 更新主键为1的记录 db.Model(&Product{}).Where("id = ?", 1).Update("Price", 200)
特殊情况
如果明确知道要更新的记录ID,也可以这样:
product := Product{ID: 1} // 只设置ID
db.Model(&product).Update("Price", 200)
但这种方法:
- 不如先查询再更新直观
- 无法利用GORM的模型状态跟踪功能
- 可能引发并发问题
最佳实践
- 对于单个记录更新,总是先查询获取完整模型
- 对于批量更新,使用Where条件明确指定范围
- 避免使用零值模型进行更新操作
- 重要更新操作前检查受影响的行数
错误示例
// 错误用法1: 完全未初始化的模型
var p Product
db.Model(&p).Update("Price", 200) // 不会按预期工作
// 错误用法2: 只有部分字段的模型
p := Product{Code: "D42"}
db.Model(&p).Update("Price", 200) // 仍然不知道更新哪条记录
delete
在GORM中执行Delete操作时,如果已经有一个完整赋值的product实例,通常不需要再额外传入primary key。以下是详细说明:
基本删除方式
- 使用已赋值的模型实例删除(推荐)
var product Product
db.First(&product, 1) // 先查询获取完整记录
// 方式1:直接使用实例删除
db.Delete(&product) // 不需要再传ID
这种方式:
- GORM会自动使用模型中的主键值作为删除条件
- 是最简洁、最安全的删除方式
- 推荐在日常开发中使用
- 同时传入实例和主键(冗余方式)
// 方式2:虽然可以运行,但冗余
db.Delete(&product, 1) // 传入实例同时再传ID
这种方式:
- 可以运行但没必要
- 如果传入的ID与实例中的ID不一致,可能导致意外行为
- 不推荐使用
特殊情况处理
批量删除
如果需要批量删除,可以不使用实例:
// 删除所有price>100的记录
db.Where("price > ?", 100).Delete(&Product{})
// 删除主键为1的记录(不依赖实例)
db.Delete(&Product{}, 1)
软删除考虑
如果模型启用了软删除(有DeletedAt
字段):
// 软删除会自动设置DeletedAt时间
db.Delete(&product)
// 要真正硬删除可以加Unscoped
db.Unscoped().Delete(&product)
为什么不需要重复传主键?
GORM的智能处理:
- 当传入模型实例时,GORM会自动提取其主键值
- 内部会生成类似
WHERE id = ?
的条件
数据一致性:
- 避免主键值不一致的风险
- 减少人为错误的可能性
代码简洁性:
- 减少冗余参数
- 提高代码可读性
最佳实践建议
查询后删除:
var product Product if err := db.First(&product, 1).Error; err != nil { // 处理错误 } db.Delete(&product)
批量删除:
// 明确条件 db.Where("code LIKE ?", "D%").Delete(&Product{})
避免的做法:
// 不推荐 - 可能误删 db.Delete(&Product{}, someVariable) // 危险 - 可能清空整个表 db.Delete(&Product{})
关于Unscoped()
基本概念
Unscoped()
是GORM提供的一个链式调用方法,用于绕过GORM的默认作用域限制,特别是针对软删除(Soft Delete)功能。
核心作用
绕过软删除机制:
- 默认情况下,GORM会对有
DeletedAt
字段的模型启用软删除 - 使用
Unscoped()
可以操作已被软删除的记录
- 默认情况下,GORM会对有
执行"硬删除":
- 真正从数据库中物理删除记录
- 而不是仅仅设置
deleted_at
时间戳
查询所有记录:
- 包括已被软删除的记录
使用场景
- 真正删除记录(硬删除)
// 普通删除(如果是软删除模型,只是设置deleted_at)
db.Delete(&user)
// 硬删除(真正从数据库删除)
db.Unscoped().Delete(&user)
- 查询包含已删除的记录
// 默认查询会排除已软删除的记录
db.Find(&users)
// 查询所有记录(包括已软删除的)
db.Unscoped().Find(&users)
- 恢复软删除的记录
// 将deleted_at设置为NULL来恢复记录
db.Unscoped().Model(&user).Update("DeletedAt", nil)
实现原理
GORM的软删除是通过在SQL查询中自动添加条件实现的:
- 默认查询会自动添加:
WHERE deleted_at IS NULL
- 使用
Unscoped()
后会移除这个条件
注意事项
数据安全:
- 硬删除是不可逆的操作
- 生产环境慎用,确保有备份机制
关联删除:
- 如果模型有关联关系,硬删除可能导致外键约束问题
- 需要考虑级联删除或手动处理关联记录
审计要求:
- 某些业务场景要求保留所有历史记录
- 硬删除可能违反合规要求
最佳实践
// 1. 硬删除前先确认
if isAdmin {
db.Unscoped().Delete(&sensitiveData)
}
// 2. 批量硬删除时添加条件限制
db.Unscoped().Where("created_at < ?", time.Now().AddDate(-5,0,0)).Delete(&OldRecords{})
// 3. 开发环境可以设置全局禁用软删除
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&TestData{})
与其他方法的对比
方法 | 作用 | 影响 |
---|---|---|
Delete() | 普通删除 | 对软删除模型是逻辑删除 |
Unscoped().Delete() | 硬删除 | 物理删除记录 |
Where().Delete() | 条件删除 | 遵循模型作用域 |
Unscoped().Where().Delete() | 条件硬删除 | 无视软删除限制 |
总结:Unscoped()
是GORM中处理软删除边界的强大工具,但需要谨慎使用,特别是在生产环境中。理解它的工作原理有助于您更好地控制数据删除行为。