Gorm 实践记录以及使用

208 阅读16分钟

基本概念


原生方法

我们先了解 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 的驱动管理器中。每个驱动会注册一个特定的名字(如 mysqlpostgres)。这是通过驱动包的 init 函数完成的。

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

**2.找到对应驱动:**调用 sql.Open(driverName, dataSourceName) 时,database/sql 会根据 driverName 找到之前注册的驱动实现(如 MySQLDriver)。 3.调用驱动的方法:

  • sql.Open 返回的是一个 sql.DB 对象,它是一个连接池的句柄,而不是一个直接的数据库连接。
  • 当需要真正访问数据库时(如执行 PingQuery 等操作),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 类型的实例 u1u2,并通过 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. 回调控制更新行为

可以通过 BeforeUpdateAfterUpdate 回调函数自定义更新行为:

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() 方法会忽略字段的零值。若需更新零值字段,可使用 SelectUpdateColumn

// 强制更新零值字段
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;