基本概念
原生方法
我们先了解 go 如何操作数据库的?
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql" // 导入 MySQL 驱动
)
type User struct{}
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/test")
// 执行 sql 语句
rows, err := db.Query("select * from user")
if err != nil {
panic(err)
}
defer rows.Close() // 避免资源泄露
// 数据、错误处理:解析并打印
var users []User
for rows.Next() {
var user User
err := rows.Scan(&user)
if err != nil {
panic(err)
}
users = append(users, user)
}
// 补充代码
if err := rows.Err(); err != nil { // 一定要对 rows.Err() 进行处理
panic(err)
}
// 打印 users
for _, user := range users {
fmt.Println(user)
}
}
database/sql 是一个接口包,它的目的是统一不同的数据库的操作方式,也就是一个数据库操作协议。在 Go 中操作数据库通常使用标准库中的 database/sql 包搭配特定的数据库驱动。 驱动是连接 database/sql 标准库和特定数据库系统之间的桥梁。它是database/sql接口的实现类,不需要直接使用,而是使用’_’来省略,加载init()函数,实现具体的细节即可。 1.**注册驱动:**数据库驱动需要在程序启动时通过 sql.Register 方法将自己注册到 database/sql 的驱动管理器中。每个驱动会注册一个特定的名字(如 mysql 或 postgres)。这是通过驱动包的 init 函数完成的。
func init() {
sql.Register("mysql", &MySQLDriver{})
}
**2.找到对应驱动:**调用 sql.Open(driverName, dataSourceName) 时,database/sql 会根据 driverName 找到之前注册的驱动实现(如 MySQLDriver)。 3.调用驱动的方法:
sql.Open返回的是一个sql.DB对象,它是一个连接池的句柄,而不是一个直接的数据库连接。- 当需要真正访问数据库时(如执行
Ping、Query等操作),database/sql会调用驱动的具体实现方法,如Open方法。
Gorm
在原生的 database/sql 中,开发者需要手动编写 SQL 查询,处理数据类型转换,并显式管理连接和事务等。Gorm 通过对这些操作进行封装,减少了手动操作的复杂性。
rows, err := db.Query("SELECT id, name FROM users WHERE age > ?", 18)
if err != nil {
panic(err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name); err != nil {
panic(err)
}
users = append(users, user)
}
ORM(对象关系映射)是一种将面向对象的数据模型与关系型数据库之间进行转换的技术。它能够把数据库中的表、行、列等数据结构映射成对应的对象,使得开发者能够使用面向对象的方式来操作数据库。
GORM(Go ORM)是一个用于Go语言的开源ORM库。它提供了一组简洁的接口和方法,让开发者可以方便地进行数据库操作,而无需手动编写SQL语句。GORM支持多种关系型数据库,如MySQL、PostgreSQL、SQLite等,并且提供了丰富的查询、关联、事务等功能,使得开发者能够更高效地进行数据库开发。
var users []User
db.Where("age > ?", 18).Find(&users)
快速入门
在本节主要讲述在Go 项目中引入GORM并简单使用
引入依赖包
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
它们分别对应于 database/sql 和 go-sql-driver。
连接MySQL
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func main() {
db, err := gorm.Open("mysql", "user:password@(localhost)/dbname?charset=utf8mb4&parseTime=True&loc=Local")
defer db.Close()
}
gorm.Open()第一个参数是要连接的数据存储类型,值可以为mysql、postgres、sqlite、SQL Server等。第二个参数则是连接数据库所需要的账号密码等配置。
defer db.Close()当所有关于数据库操作结束时关闭连接。
这两段代码一般写在一起,确保数据库能够安全连接与关闭。
增删改查操作
下面主要对数据库db1中UserInfo进行增删改查操作。
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
// UserInfo 用户信息
type UserInfo struct {
ID uint
Name string 'gorm:"default:'小王子'"'
Gender string
Hobby string
}
func main() {
db, err := gorm.Open("mysql", "root:root1234@(127.0.0.1:13306)/db1?charset=utf8mb4&parseTime=True&loc=Local")
if err!= nil{
panic(err)
}
defer db.Close()
// 自动迁移
db.AutoMigrate(&UserInfo{})
u1 := UserInfo{1, "七米", "男", "篮球"}
u2 := UserInfo{2, "沙河娜扎", "女", "足球"}
// 创建记录
db.Create(&u1)
db.Create(&u2)
// 查询
var u = new(UserInfo)
db.First(u)
fmt.Printf("%#v\n", u)
var uu UserInfo
db.Find(&uu, "hobby=?", "足球")
fmt.Printf("%#v\n", uu)
// 更新
db.Model(&u).Update("hobby", "双色球")
// 删除
db.Delete(&u)
}
1.结构体定义
借助GORM,结构体和表实现映射关系,结构体实例和对应表的记录实现映射关系。GORM也提供了一些字段标签。上例中,’gorm:"default:'小王子'"’当我们插入一个userInfo对象(name未赋值时),数据库会在该字段插入默认值”小王子”。
2.增删改查方法
这里面最重要的就是db.AutoMigrate()方法。它用于自动创建或更新数据库表结构。它会根据定义的模型结构体自动创建或更新数据库表,以保证数据库和模型的结构一致。当首次运行一个程序时,如果数据库中不存在对应的表,db.AutoMigrate() 方法会自动创建这些表。如果表已经存在,并且模型结构发生了变化(比如新增字段、修改字段类型等),它也会自动更新表结构,使其与模型结构保持一致。
两个 UserInfo 类型的实例 u1 和 u2,并通过 db.Create(&u1) 和 db.Create(&u2) 分别将它们保存到数据库中。
db.First(u) 进行查询,将查找到的第一条记录(表的第一各记录)赋值给 u 变量,并使用 fmt.Printf 打印出 u 的内容。
db.Find(&uu, "hobby=?", "足球") 进行条件查询,将满足条件 "hobby=' 足球 '" 的第一条记录赋值给 uu 变量,并使用 fmt.Printf 打印出 uu 的内容。
使用 db.Model(&u).Update("hobby", "双色球") 进行更新操作,将 u 的 "hobby" 字段的值改为 "双色球"。
使用 db.Delete(&u) 进行删除操作,将 u 对应的记录从数据库中删除。
声明模型
前面我们已经初步使用了Model结构体与数据库的映射了。现在我们需要对结构体名称约定以及标签进行详细说明。
模型定义
模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成:
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
DeleteAt gorm.DeleteAt `gorm:"index"`
}
💡 注意:在 Go 语言中,结构体字段的可见性由字段名的首字母大小写决定。结构体的属性首字母均使用大写。
在GORM中,约定大于配置。
- 默认情况下,数据库采用的是蛇形蛇形命名(如create_at),其中表名为复数形式,列名为单数形式。对应的,我们只需要在定义 Model 时使用驼峰命名(如CreateAt)即可(表名去掉复数,列名仍保持单数),它们会自动建立映射关系,而无需手动配置。
- ID/Id 字段为主键,如果为数字,则为自增主键
- CreatedAt 字段,创建时,保存当前时间
- UpdatedAt 字段,创建、更新时,保存当前时间
- gorm.DeleteAt 字段,默认开启 soft delete 模式
如果你有一个名为 User 的结构体,其中包含一个字段 MemberNumber,那么 GORM 会自动将该字段映射到数据库表 users 的 member_number 列。
嵌入结构体
在一个model类中嵌入子结构体,那么这个子结构体的属性实际上就是model的属性。GORM将常用的ID、CreateAt、UpdateAt、DeleteAt四个字段进行了封装。
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
我们定义一个model时可以如下编写:
type User struct {
gorm.Model
Name string
}
// 等效于
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
}
GORM默认ID作为主键,另外一条记录创建的时间、更新的时间、删除的时间往往也是必要的。使用gorm.Model会实现对ID、CreateAt、UpdateAt、DeleteAt四个字段进行封装。嵌入结构体的方式使得我们能更便捷的定义Model。
列名、主键、表名映射
在GORM中有大量的约定,如果我们在设置中无法使用这些约定,可以考虑使用映射。
1.列名映射
前面我们介绍了属性与数据库列名之间的映射关系,前者使用大驼峰命名法(XxxXxx),后者使用蛇形命名法(xxx-xxx)。如果它们之间不遵守上述约定呢?此时就需要指明映射关系:借助于刚刚提到的标签column
type Animal struct {
AnimalId int64 `gorm:"column:beast_id"` // set column name to `beast_id`
Birthday time.Time `gorm:"column:day_of_the_beast"` // set column name to `day_of_the_beast`
Age int64 `gorm:"column:age_of_the_beast"` // set column name to `age_of_the_beast`
}
2.主键名映射
考虑到每个表都会设置一个自增ID作为主键,因此GORM框架约定ID字段默认就是主键。如果我们不使用ID作为属性,可以使用gorm:"primary_key"作为映射。
type User struct {
ID string // 名为`ID`的字段会默认作为表的主键
Name string
}
// 使用`AnimalID`作为主键
type Animal struct {
AnimalID int64 `gorm:"primary_key"`
Name string
Age int64
}
3.表名映射
在快速入门中,我们学习了db.AutoMigrate()方法,它能够创建和更新相关数据库。实际上创建的数据库名称为结构体名称+“s”(例如user实体→users表)。
可以使用table()方法实现表别名
// 使用User结构体创建名为`deleted_users`的表
db.Table("deleted_users").CreateTable(&User{})
var deleted_users []User
db.Table("deleted_users").Find(&deleted_users)
//// SELECT * FROM deleted_users;
db.Table("deleted_users").Where("name = ?", "jinzhu").Delete()
//// DELETE FROM deleted_users WHERE name = 'jinzhu';
结构体标签
声明 model 时,tag 是可选的,GORM 支持以下 tag: tag 名大小写不敏感,但建议使用 camelCase 风格。
以下是GORM结构体字段标签的分类表格:
| 类别 | 标签 | 说明 |
|---|---|---|
| 主键 | primary_key | 指定字段作为表的主键 |
| 索引 | index | 创建普通索引 |
unique | 创建唯一索引 | |
unique_index | 创建唯一索引 | |
| 外键 | foreignkey | 指定字段为外键并指定关联的表和外键约束 |
| 默认值 | default | 设定字段的默认值 |
auto_increment | 字段会自动增加并且在插入数据时生成一个唯一的值 | |
auto_create | 在插入数据时自动创建一个值,例如创建时间 | |
auto_update | 在更新数据时自动更新一个值,例如更新时间 | |
| 类型 | column | 设定字段的数据库类型,例如将字段类型设置为varchar(255) |
type | 设定字段的Go语言类型,例如将字段类型设置为time.Time | |
| 大小 | size | 设定字段的最大字符数或字节数 |
| 空值 | not null | 设置字段不允许为空值 |
null | 设置字段允许为空值 | |
| 字段名 | column:<name> | 设定字段在数据库中的名称,例如将数据库字段名设置为user_name |
| 备注 | comment | 添加备注,用于更详细地描述字段或提供其他信息 |
| 唯一约束 | gorm:"unique,<constraint_name>" | 创建唯一约束,可以指定约束名称(MySQL/MariaDB/PostgreSQL) |
gorm:"index,<constraint_name>" | 创建索引约束,可以指定约束名称(MySQL/MariaDB/PostgreSQL) | |
gorm:"foreignkey,<constraint_name>" | 创建外键约束,可以指定约束名称(MySQL/MariaDB/PostgreSQL) | |
| 外键约束 | gorm:"references:<table>,<foreign_key>,<on_delete>,<on_update>" | 创建外键约束并设置参考表、外键列、删除操作和更新操作,外键列可以使用'.'来访问与rels关系中定义的字段(MySQL/MariaDB/PostgreSQL) |
| 时间戳 | gorm:"autoCreateTime" | 在插入数据时自动创建一个时间戳字段 |
gorm:"autoUpdateTime" | 在更新数据时自动更新一个时间戳字段 | |
| 关联关系 | gorm:"association_foreignkey" | 在多对多关系中,被assocation定义的struct的外键字段名 |
gorm:"foreignkey" | 在many2many的source中定义的字段 | |
gorm:"many2many:<join_table>,<foreign_key>,<association_foreign_key>" | 定义join table、外键字段名和关联外键字段名 |
一个标准的标签说明如下:
type User struct {
gorm.Model
Name string
Age sql.NullInt64
Birthday *time.Time
Email string `gorm:"type:varchar(100);unique_index"`
Role string `gorm:"size:255"` // 设置字段大小为255
MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)唯一并且不为空
Num int `gorm:"AUTO_INCREMENT"` // 设置 num 为自增类型
Address string `gorm:"index:addr"` // 给address字段创建名为addr的索引
IgnoreMe int `gorm:"-"` // 忽略本字段
}
详细操作
本节将对增删改查进行具体的描述。
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func main() {
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
defer db.Close()
// db.Xx
}
上面例子中, db 变量为 *gorm.DB 对象
创建操作
1.基本使用
使用 NewRecord() 查询主键是否存在,主键为空使用 Create() 创建记录:
user := User{Name: "q1mi", Age: 18}
db.NewRecord(user) // 主键为空返回`true`
db.Create(&user) // 创建user
db.NewRecord(user) // 创建`user`后返回`false`
db.Create(&user)相当于执行sql语句“INSERT INTO users("name","age") values('q1mi','18');”
**注意:**所有字段的零值,比如 0, "",false 或者其它零值,都不会保存到数据库内,但会使用他们的默认值。 如果想避免这种情况,可以考虑使用指针或实现 Scanner/Valuer 接口,比如:
- 使用指针方式实现零值存入数据库
// 使用指针
type User struct {
ID int64
Name *string `gorm:"default:'小王子'"`
Age int64
}
user := User{Name: new(string), Age: 18))}
db.Create(&user) // 此时数据库中该条记录name字段的值就是''
- 使用 Scanner/Valuer 接口方式实现零值存入数据库
// 使用 Scanner/Valuer
type User struct {
ID int64
Name sql.NullString `gorm:"default:'小王子'"` // sql.NullString 实现了Scanner/Valuer接口
Age int64
}
user := User{Name: sql.NullString{"", true}, Age:18}
db.Create(&user) // 此时数据库中该条记录name字段的值就是''
查询操作
1.一般查询
user := new(User)
// 根据主键查询第一条记录
db.First(&user)
//// SELECT * FROM users ORDER BY id LIMIT 1;
// 随机获取一条记录
db.Take(&user)
//// SELECT * FROM users LIMIT 1;
// 根据主键查询最后一条记录
db.Last(&user)
//// SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 查询所有的记录
db.Find(&users)
//// SELECT * FROM users;
// 查询指定的某条记录(仅当主键为整型时可用)
db.First(&user, 10)
//// SELECT * FROM users WHERE id = 10;
查询操作会将结果放置在结构体实例中。因此我们的结构体实例必须使用new开创空间(注意初始化、new和make的区别)。
2.Where 条件
- 普通 SQL 查询
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
//// SELECT * FROM users WHERE name = 'jinzhu' limit 1;
// Get all matched records
db.Where("name = ?", "jinzhu").Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu';
// <>
db.Where("name <> ?", "jinzhu").Find(&users)
//// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)
//// SELECT * FROM users WHERE name in ('jinzhu','jinzhu 2');
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
//// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
//// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
//// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
- Struct & Map 查询
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// 主键的切片
db.Where([]int64{20, 21, 22}).Find(&users)
//// SELECT * FROM users WHERE id IN (20, 21, 22);
**提示:**当通过结构体进行查询时,GORM 将会只通过非零值字段查询,这意味着如果你的字段值为 0,'',false 或者其他零值时,将不会被用于构建查询条件,例如:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu";
你可以使用指针或实现 Scanner/Valuer 接口来避免这个问题.
// 使用指针
type User struct {
gorm.Model
Name string
Age *int
}
// 使用 Scanner/Valuer
type User struct {
gorm.Model
Name string
Age sql.NullInt64 // sql.NullInt64 实现了 Scanner/Valuer 接口
}
- Not 条件
作用与 Where 类似的情形如下:
db.Not("name", "jinzhu").First(&user)
//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1;
// Not In
db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users)
//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
//// SELECT * FROM users WHERE id NOT IN (1,2,3);
db.Not([]int64{}).First(&user)
//// SELECT * FROM users;
// Plain SQL
db.Not("name = ?", "jinzhu").First(&user)
//// SELECT * FROM users WHERE NOT(name = "jinzhu");
// Struct
db.Not(User{Name: "jinzhu"}).First(&user)
//// SELECT * FROM users WHERE name <> "jinzhu";
- Or 条件
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
- 内联条件
作用与 Where 查询类似,当内联条件与多个立即执行方法一起使用时,内联条件不会传递给后面的立即执行方法。
// 根据主键获取记录 (只适用于整形主键)
db.First(&user, 23)
//// SELECT * FROM users WHERE id = 23 LIMIT 1;
// 根据主键获取记录, 如果它是一个非整形主键
db.First(&user, "id = ?", "string_primary_key")
//// SELECT * FROM users WHERE id = 'string_primary_key' LIMIT 1;
// Plain SQL
db.Find(&user, "name = ?", "jinzhu")
//// SELECT * FROM users WHERE name = "jinzhu";
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;
// Struct
db.Find(&users, User{Age: 20})
//// SELECT * FROM users WHERE age = 20;
// Map
db.Find(&users, map[string]interface{}{"age": 20})
//// SELECT * FROM users WHERE age = 20;
删除操作
GORM 支持通过模型实例或指定条件删除记录。
1. 使用模型实例删除
使用 Delete() 方法删除记录:
// 删除一条记录
db.Delete(&user)
//// DELETE FROM users WHERE id = 10;
注意: GORM 的默认行为是软删除,即在表中增加 deleted_at 字段后,执行 Delete() 方法会更新该字段而不是直接删除记录。只有在未启用软删除时,记录才会从数据库中被物理删除。
type User struct {
gorm.Model
Name string
Age int
}
user := User{ID: 10}
db.Delete(&user)
// 对于软删除表,相当于更新该条记录的 `deleted_at` 字段。
2. 指定条件删除
通过条件删除匹配的记录:
// 删除所有 `age = 20` 的记录
db.Where("age = ?", 20).Delete(&User{})
//// DELETE FROM users WHERE age = 20;
// 使用原始 SQL 删除
db.Exec("DELETE FROM users WHERE age = ?", 20)
//// DELETE FROM users WHERE age = 20;
3. 永久删除
软删除模式下,若想彻底删除记录,可以使用 Unscoped:
// 永久删除记录
db.Unscoped().Delete(&user)
//// DELETE FROM users WHERE id = 10;
// 永久删除所有记录
db.Unscoped().Where("age = ?", 20).Delete(&User{})
//// DELETE FROM users WHERE age = 20;
更新操作
GORM 提供了多种方法来更新记录,包括更新单个字段、多个字段或通过条件更新。
1. 更新单个字段
// 更新单个字段
db.Model(&user).Update("name", "new_name")
//// UPDATE users SET name = 'new_name' WHERE id = 10;
// 更新时可以使用表达式
db.Model(&user).Update("age", gorm.Expr("age + ?", 1))
//// UPDATE users SET age = age + 1 WHERE id = 10;
2. 更新多个字段
使用 Updates() 方法批量更新字段:
// 更新多个字段
db.Model(&user).Updates(User{Name: "new_name", Age: 30})
//// UPDATE users SET name = 'new_name', age = 30 WHERE id = 10;
// 使用 Map 更新
db.Model(&user).Updates(map[string]interface{}{"name": "new_name", "age": 30})
//// UPDATE users SET name = 'new_name', age = 30 WHERE id = 10;
3. 条件更新
通过条件更新匹配的记录:
// 条件更新
db.Model(&User{}).Where("name = ?", "jinzhu").Update("age", 18)
//// UPDATE users SET age = 18 WHERE name = 'jinzhu';
// 批量更新多个字段
db.Model(&User{}).Where("age > ?", 18).Updates(map[string]interface{}{"name": "updated_name", "age": 20})
//// UPDATE users SET name = 'updated_name', age = 20 WHERE age > 18;
4. 回调控制更新行为
可以通过 BeforeUpdate 和 AfterUpdate 回调函数自定义更新行为:
func (user *User) BeforeUpdate(tx *gorm.DB) (err error) {
// 自定义逻辑
fmt.Println("Before updating user")
return
}
func (user *User) AfterUpdate(tx *gorm.DB) (err error) {
// 自定义逻辑
fmt.Println("After updating user")
return
}
5. 忽略零值字段
默认情况下,Updates() 方法会忽略字段的零值。若需更新零值字段,可使用 Select 或 UpdateColumn:
// 强制更新零值字段
db.Model(&user).Updates(User{Name: "", Age: 0})
//// UPDATE users SET name = '', age = 0 WHERE id = 10;
// 指定更新字段
db.Model(&user).Select("name").Updates(User{Name: "", Age: 0})
//// UPDATE users SET name = '' WHERE id = 10;
// 使用 UpdateColumn 强制更新零值字段
db.Model(&user).UpdateColumn("name", "")
//// UPDATE users SET name = '' WHERE id = 10;