GORM数据库编程
Table of Contents
GORM 官方文档基础入门学习笔记
什么是 ORM
ORM 全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。
传统通过 SQL 语句来操作数据库,需要我们有足够的 SQL 语句基础,并且不同数据库有不同的 SQL 语言,有一定的门槛。然而 ORM 可以帮我们隔离 SQL 语言,操作数据库时不需要编写对应的 SQL 语言,只需要面向对象编程即可。
针对 Java、Python 等面向对象的编程语言来说,ORM 就是将一张表映射成一个类,表中的列映射成类中的一个子类(类中套类)。但是对于 Go 语言而言,表不是映射为一个类而是一个结构体(struct),表中的列可以映射成 struct 中不同的类型,同时可以使用 struct 中的 tag 来描述数据库中列的各种属性。
Go 语言中常用的 ORM 框架有以下这些:
我们不用太去纠结应该选择哪一个 ORM 框架,熟悉了其中一个,其他的 orm 迁移成本很低,我们就选择一个 star 数量最高的,不会有出错的框架就行了,它们之间整体差异不会很大。我们更应该关注 SQL 语言本身,它远比 ORM 框架要重要的多。
ORM 有何优缺点?
优点:
- 提高了开发效率。
- 屏蔽 SQL 细节。可以自动对实体 Entity 对象与数据库中的 Table 进行字段与属性的映射;不用直接 SQL 编码,屏蔽各种数据库之间的差异。
缺点:
- ORM 会牺牲程序的执行效率和会固定思维模式
- 太过依赖 ORM 会导致 SQL 理解不够
- 对于固定的 ORM 依赖过重,导致切换到其他的 ORM 代价高
我们需要以 SQL 为主,ORM 为辅。ORM 主要目的是为了增加代码可维护性和开发效率。对于 SQL 我们一定要学好:
- group by
- 子查询
- having 子句
连接数据库
GORM 官方文档,其官方文档写的十分详细和规范,可以方便我们自行学习。本学习笔记将使用 MySQL 数据库进行演示。
示例代码:
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 参考 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{})
}
注意:想要正确的处理
time.Time
,您需要带上parseTime
参数, (更多参数) 要支持完整的 UTF-8 编码,您需要将charset=utf8
更改为charset=utf8mb4
查看 此文章 获取详情。
MySQL 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:
db, err := gorm.Open(mysql.New(mysql.Config{
// DSN data source name
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local",
// string 类型字段的默认长度
DefaultStringSize: 256,
// 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DisableDatetimePrecision: true,
// 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameIndex: true,
// 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
DontSupportRenameColumn: true,
// 根据当前 MySQL 版本自动配置
SkipInitializeWithVersion: false,
}), &gorm.Config{})
创建数据表
根据 GORM 文档中的说明,GORM 有一个默认的 logger 实现,我们将设置一个全局的 logger,用于将我们后续执行操作的 SQL 语言都打印出来,边学习 GORM 的同时,学习其 SQL 语句的写法。搞清楚 GORM 各个 API 背后真正执行的 SQL 语句是什么。
配置 logger,并在连接时通过配置参数传入。
newLogger := logger.New(
// io writer
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
// Ignore ErrRecordNotFound error for logger
IgnoreRecordNotFoundError: true,
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Enable color
},
)
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{
Logger: newLogger,
})
接着我们就可以快速入门 GORM 的写法,首先定义一个表结构,将表结构直接生成对应的 MySQL 表,这也是 migrations 功能。
定义表结构体:
type Product struct {
gorm.Model
Code sql.NullString
Price uint
}
gorm.Model
是 GORM 内置的一个结构体,存放着数据库表中常用的字段,如 id、创建时间、删除时间、更新时间等,我们可以直接拿来使用,作为表结构体的一部分。
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
在使用之前不要忘了现在数据库中创建一个 gorm_test
的数据库,选择 utf8mb4
字符集,排序规则选择 utf8mb4_general_ci
。
接着,我们使用 _ = db.AutoMigrate(&Product{})
语句就可以自动在数据库中创建一个空表。由于这段代码会执行多条 SQL 语句。如果设置 logger 正确,会输出执行了什么 SQL 语句。
整体代码如下:
type Product struct {
gorm.Model
Code sql.NullString
Price uint
}
func main() {
dsn := "root:root@tcp(192.168.0.104:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: true,
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
_ = db.AutoMigrate(&Product{}) //此处会有sql语句
}
输出的 SQL 语句:
SELECT DATABASE()
SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'gorm_test%' ORDER BY SCHEMA_NAME='gorm_test' DESC,SCHEMA_NAME limit 1
SELECT count(*) FROM information_schema.tables WHERE table_schema = 'gorm_test' AND table_name = 'products' AND table_type = 'BASE TABLE'
CREATE TABLE `products` (`id` bigint unsigned AUTO_INCREMENT,`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`code` longtext,`price` bigint unsigned,PRIMARY KEY (`id`),INDEX `idx_products_deleted_at` (`deleted_at`))
db.AutoMigrate()
可以同时传入两个结构体,同时生成两个数据表,包括关联表。
数据的增删改查
接着我们可以尝试编写增删改查的语句。
// 新增
db.Create(&Product{Code: sql.NullString{"D42", true}, Price: 100})
// 读取
var product Product
db.First(&product, 1) // 根据整形主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
// 更新 - 将 product 的 price 更新为 200
db.Model(&product).Update("Price", 200)
// 更新 - 更新多个字段
db.Model(&product).Updates(Product{Price: 200, Code:sql.NullString{"", true}}) // 仅更新非零值字段
// 如果我们去更新一个product 只设置了price:200
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - 删除 product
db.Delete(&product, 1) // 并没有执行delete语句 执行了update语句 逻辑删除 添加删除时间
以上 Go 代码转换成的 SQL 语句如下,我们可以发现 GORM 自动为我们加上了很多需求和限制,比如自动更改了数据更新时间,自动加上限制 1 条,使用逻辑删除(所有删除语句都不是真的删除数据而是使用逻辑删除,设置删除标志)等等。
INSERT INTO `products` (`created_at`,`updated_at`,`deleted_at`,`code`,`price`) VALUES ('2025-03-03 19:52:46.757','2025-03-03 19:52:46.757',NULL,'D42',100)
SELECT * FROM `products` WHERE `products`.`id` = 1 AND `products`.`deleted_at` IS NULL ORDER BY `products`.`id` LIMIT 1
SELECT * FROM `products` WHERE code = 'D42' AND `products`.`deleted_at` IS NULL AND `products`.`id` = 1 ORDER BY `products`.`id`
LIMIT 1
UPDATE `products` SET `price`=200,`updated_at`='2025-03-03 19:52:46.78' WHERE `products`.`deleted_at` IS NULL AND `id` = 1
UPDATE `products` SET `updated_at`='2025-03-03 19:52:46.784',`code`='F42',`price`=200 WHERE `products`.`deleted_at` IS NULL AND `id` = 1
UPDATE `products` SET `code`='F42',`price`=200,`updated_at`='2025-03-03 19:52:46.787' WHERE `products`.`deleted_at` IS NULL AND `id` = 1
UPDATE `products` SET `deleted_at`='2025-03-03 19:52:46.791' WHERE `products`.`id` = 1 AND `products`.`id` = 1 AND `products`.`deleted_at` IS NULL
零值更新问题
在上面的更新语句中,我们了解到 updates
语句只能更新非零值的字段, 而 update
语句则不受影响。为什么需要这样?因为 updates
需要传入 struct
或者 map[string]interface{}
参数,如果使用 struct
更新时,比如在 product 结构体中只传入了 Price 的值,那么其他值就会是默认值(Golang 语法决定的),这样在后续数据库操作过程中可能就会存在问题。
因此,在 ORM 框架中,使用 updates
方法通过传入 struct
的方式进行更新操作时,只能更新非零值字段,其他默认零值的字段传入,将会被忽略,不会出现在最后的 SQL 语句中。具体介绍见官方文档:更新 | GORM
我们将上述语句改成以下的代码,那么 price 和 code 都不会被更新,因为它们都是各自属性的零值 (uint 类型零值为 0,string 类型零值为空字符串)。
db.Model(&product).Updates(Product{Price: 0, Code:""})
那么如果我们需要将零值更新进数据库怎么办?就可以像上一节中的代码一样,将可能传入零值的字段定义为 sql.NullString
这个类型。它通过 Valid
这个变量来指定这个字符串是否为空。
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
如果将 Valid
设置为 true
,就可以将零值(如 code 的空字符串 ""
)通过 SQL 语句更新到数据库中。
db.Model(&product).Updates(Product{Price: 200, Code:sql.NullString{"", true}})
除了 NullString
这个类型外,还有 NullBool
、NullFloat64
、NullInt32
、NullInt64
、NullTime
这些类型,因为这些类型都有零值。使用它们时需要引入 database/sql
包。
还有一种方式可以使零值也能更新,就是将类型设置为指针类型,传入参数时需要传入结构体的指针。比如以下这种方式,在最终的 SQL 语句中也会设置 email 为零值。
type user struct {
email *string
}
empty := ""
db.Model(&User{ID:1}).Updates(User{Email: &empty})
总结一下,如果想要在数据库中写入零值,那么对应的字段类型就需要使用以上这几种 NUll~
类型,或者使用该类型的指针类型。
sql.NullString
*string
表结构定义的细节
在使用 GORM 时,会有一些默认的约定:
- 主键:GORM 使用一个名为
ID
的字段作为每个模型的默认主键。 - 表名:默认情况下,GORM 将结构体名称转换为
snake_case
(蛇形命名)并为表名加上复数形式。例如,一个Product
结构体在数据库中的表名变为products
。 - 列名:GORM 自动将结构体字段名称转换为
snake_case
(蛇形命令)作为数据库中的列名。例如创建时间CreatedAt
在数据库的列中变为created_at
。 - 时间戳字段:GORM 使用字段
CreatedAt
和UpdatedAt
来自动跟踪记录的创建和更新时间。
通过设置结构体的方式定义一个数据库表时,我们可以通过结构体各个字段的 tag 来对各个字段进行约束。这叫做字段标签,多个字段标签用 ;
隔开,比如以下例子:
type User struct {
UserID uint `gorm:"primarykey"`
Name string `gorm:"column:user_name;type:varchar(50);index:idx_user_name;unique;default:'bobby'"`
}
我们将 UserID
设置为主键;将 Name
在数据表中的列名设置为 user_name
,将其类型设置为数据库中的 varchar(50)
定长字符,创建索引 idx_user_name
,并定义成唯一键,最后将默认值设为 bobby
。
这些都是数据库中的知识点,一定要学好数据库,否则难以理解。
创建记录
user := User{Name: "liuchao", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
在 db.Create()
这条语句执行之前,User
这个结构体中是没有 ID
这个字段的,执行 SQL 语句后,会设置一个 ID 值(作为主键),并将其写入 User
中,这也是为什么需要传入 User
的地址,因为要对这个对象进行改动。如果在 User
中已经通过 tag 来设置主键后,就不会自动添加 ID
字段了。
我们还可以使用 Create()
创建多项记录,但是这样使用并不常见,最常用的方式是下方的批量插入的方式。
users := []*User{
{Name: "liuchao", Age: 18, Birthday: time.Now()},
{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(users) // pass a slice to insert multiple row
result.Error // returns error
result.RowsAffected // returns inserted records count
如果我们有大量的数据需要插入,那么我们就需要用到批量插入了。
要高效地插入大量记录,需要切片传递给 Create
方法。 GORM 将生成一条 SQL 来插入所有数据(因此性能较好),以返回所有主键值,并触发 Hook
方法。当这些记录可以被分割成多个批次时,GORM 会开启一个事务 </0>
来处理它们。
var users = []User{{Name: "bobby1"}, {Name: "bobby2"}, {Name: "bobby3"}}
db.Create(&users)
for _, user := range users {
user.ID // 1,2,3
}
我们可以通过 db.CreateInBatches
方法来指定批量插入的批次大小。比如有 10000 条数据需要插入,批次大小设置为 100,那么 GORM 就会生成 100 条 SQL 语句,分别通过 100 次将这 1000 条数据插入数据库中。
var users = []User{{Name: "bobby_1"}, ...., {Name: "bobby_10000"}}
// batch size 100
db.CreateInBatches(users, 100)
为什么要分批次呢?因为 SQL 语句有长度限制,太长的 SQL 语句不能被执行,分批次插入的方式在大数据量的情况下更常见。
还有一种更简便的插入方式是通过 map
类型来插入,不创建结构体实例,直接传入一个 map
即可,比如下方这个例子。
// 用 `[]map[string]interface{}` 的方式单行插入
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})
// 用 `[]map[string]interface{}{}` 的方式多行插入
db.Model(&User{}).Create([]map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})
map
方式创建更灵活,struct
方式创建更好维护。各有优缺点。
最后还有一种关联创建,一条创建语句同时可以创建两个表记录。
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
type User struct {
gorm.Model
Name string
CreditCard CreditCard
}
db.Create(&User{
Name: "liuchao",
CreditCard: CreditCard{Number: "123456"},
})
// INSERT INTO `users` ...
// INSERT INTO `credit_cards` ...
同时 GORM 会为 CreditCard
中的 UserID
自动创建一个外键,关联到 User
中的 ID
字段。
这是因为 UserID
是一个 uint
类型的字段,它的命名符合 GORM 的外键命名规则(即 <struct name in singular form>_id
)。GORM 会自动识别这个字段为外键,并将其与 User
表的主键 id
关联起来。
其 SQL 语句为:
INSERT INTO `credit_cards` (`created_at`,`updated_at`,`deleted_at`,`number`,`user_id`) VALUES ('2025-03-03 21:03:34.475','2025-03-03 21:03:34.475',NULL,'123456',1) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)
INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`) VALUES ('2025-03-03 21:03:34.472','2025-03-03 21:03:34.472',NULL,'liuchao')
查询记录
GORM 提供了 First
、Take
、Last
方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1
条件,且没有找到记录时,它会返回 ErrRecordNotFound
错误。
// 获取第一条记录(主键升序)
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;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
errors.Is()
方法可以用于准确判断两者是否是同一种错误类型。
如果想避免 ErrRecordNotFound
错误,你可以使用 Find
,比如 db.Limit(1).Find(&user)
,Find
方法可以接受 struct 和 slice的数据。
也可以通过主键查询。主键查询时不用指定字段名,默认就为主键查询。
如果主键是数字类型,您可以使用 内联条件 来检索对象。当使用字符串时,需要额外的注意来避免 SQL 注入;查看 Security 部分来了解详情。
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3})
// SELECT * FROM users WHERE id IN (1,2,3);
如果主键是字符串 (例如像 uuid),查询将被写成如下:
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";
这种方式实际上也能够代替下方的
Where()
语句,但是不太常用,First Find Last
都可以。
当目标对象有一个主键值时,将使用主键构建查询条件,例如:
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;
var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;
如果需要查询全部对象,则需要使用 Find()
方法。
// Get all records
result := db.Find(&users)
// SELECT * FROM users;
result.RowsAffected // returns found records count, equals `len(users)`
result.Error // returns error
在实际项目中,最常用的还是使用条件查询 Where()
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// Get all matched records
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';
我们通过传入结构体 &users
来指定需要查询的表名,同时需要注意 GORM 中的字段名是大小写不敏感的,也就是说 db.Where("Name = ?", "jinzhu").First(&user)
也是可以正确执行的。
请注意!db.Where("")
引号中一点要填入数据库表中的字段名,而不是代码结构体中的字段名。比如,有以下结构体
type User struct {
MyName string `gorm:"column:name`
}
则需要使用以下方式,才能被正确执行。
db.Where("name = ?", "jinzhu").First(&user)
如果不想使用数据表字段的名称,而是使用结构体中的名称,这时就需要使用到 struct 和 map 作为查询条件了,可以屏蔽具体的数据库细节。比如以下这几种方式,我们更建议使用这种方式,当这种情况不能满足需求时,再使用上面的方式,更加灵活,更接近于直接写 SQL 语句。
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
使用 struct 时,GORM 是不会处理零值的(忽略零值),也就是如果字段为零值,那么将不会在最终的 SQL 语句中被执行,而 map 方式则不会受到影响。比如以下示例:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
除此之外,还有许多的查询语句,具体可以见官方文档,同时一定要学好数据库,其中 group by
、having
和子查询等十分重要。
总结一下,常用的查询方法:
- string,灵活但不常用,接近 SQL 语句
- struct,推荐使用
- map,解决零值问题
Find()
可能会返回多条记录,因此需要使用[]User
类型来接收,而First()
和Last()
都只会返回单条记录,因此可以使用User
类型来接收。
更新记录
在上方我们已经学习过零值更新问题,实际上还有一个 Sava()
方法会保存所有的字段,即使字段是零值。
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save (&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
Save ()
是一个组合函数。如果保存值不包含主键,它将执行 Create
(创建新记录),否则它将执行 Update
(包含所有字段,其它字段自动设为零值)。所以如果不想将所有字段都修改,那么不推荐使用 Save ()
。
db.Save (&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu", 100,"0000-00-00 00:00:00","0000-00-00 00:00:00")
db.Save (&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1
需要注意,不要将 Save ()
和 Model ()
一同使用, 这是未定义的行为。因为 Save ()
已经可以通过传入的结构体名称来指定是哪个数据表,比如 Save (&user)
就是修改 user
表,这跟上方的查询语句 Find ()
、First ()
等语句一样。
但是对于接下来的 Update ()
和 Updates ()
方法,它们没有可以传递结构体名的地方,因此需要用到 Model ()
来指定更新哪个数据表的记录。
更新单个列。
当使用 Update
更新单列时,需要有一些条件,否则将会引起ErrMissingWhereClause
错误,查看 阻止全局更新 了解详情。当使用 Model
方法,并且它有主键值时,主键将会被用于构建条件,例如:
// 根据条件更新
db.Model (&User{}). Where ("active = ?", true). Update ("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// User 的 ID 是 `111`
db.Model (&user). Update ("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据条件和 model 的值进行更新
db.Model (&user). Where ("active = ?", true). Update ("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
如果传入的
user
结构体中有主键信息,则会自动作为 SQL 语句的条件,如果没有任何数据信息,则会通过Where ()
中的条件进行查找更新。
更新多个列。
Updates
方法支持 struct
和 map[string]interface{}
参数。当使用 struct
更新时,默认情况下 GORM 只会更新非零值的字段
// 根据 `struct` 更新属性,只会更新非零值的字段
db.Model (&user). Updates (User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// 根据 `map` 更新属性
db.Model (&user). Updates (map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
再次注意,使用 struct 更新时, GORM 将只更新非零值字段。解决方式是用
map
来更新属性,或者使用Select
声明字段来更新
如果您想要在更新时选择、忽略某些字段,您可以使用 Select
、Omit
// 选择 Map 的字段
// User 的 ID 是 `111`:
db.Model (&user). Select ("name"). Updates (map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
db.Model (&user). Omit ("name"). Updates (map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// 选择 Struct 的字段(会选中零值的字段)
db.Model (&user). Select ("Name", "Age"). Updates (User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
// 选择所有字段(选择包括零值字段的所有字段)
db.Model (&user). Select ("*"). Updates (User{Name: "jinzhu", Role: "admin", Age: 0})
// 选择除 Role 外的所有字段(包括零值字段的所有字段)
db.Model (&user). Select ("*"). Omit ("Role"). Updates (User{Name: "jinzhu", Role: "admin", Age: 0})
删除记录
删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如:
// Email 的 ID 是 `10`
db.Delete (&email)
// DELETE from emails where id = 10;
// 带额外条件的删除
db.Where ("name = ?", "jinzhu"). Delete (&email)
// DELETE from emails where id = 10 AND name = "jinzhu";
GORM 允许通过主键(可以是复合主键)和内联条件来删除对象,这与查询-内联条件(Query Inline Conditions) 是类似的。
db.Delete (&User{}, 10)
// DELETE FROM users WHERE id = 10;
db.Delete (&User{}, "10")
// DELETE FROM users WHERE id = 10;
db.Delete (&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
更为重要的是软删除功能,即逻辑删除(不删除表数据,只是通过删除时间来标记该条记录已被删除)。
如果你的模型包含了 gorm. DeletedAt
字段(该字段也被包含在gorm. Model
中),那么该模型将会自动获得软删除的能力!
当调用 Delete
时,GORM 并不会从数据库中删除该记录,而是将该记录的 DeleteAt
设置为当前时间,而后的一般查询方法也将无法查找到此条记录。
// user's ID is `111`
db.Delete (&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// Batch Delete
db.Where ("age = ?", 20). Delete (&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// Soft deleted records will be ignored when querying
db.Where ("age = 20"). Find (&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
如果你并不想嵌套gorm. Model
,你也可以像下方例子那样开启软删除特性:
type User struct {
ID int
Deleted gorm. DeletedAt
Name string
}
如果不想使用软删除,而是使用硬删除,即永久删除。可以使用 Unscoped
来永久删除匹配的记录。
db.Unscoped (). Delete (&order)
// DELETE FROM orders WHERE id=10;
无论更新、删除都可以先通过查询记录的方式拿到
user
(单个对象)或者users
(对象列表),再将其传入Model ()
或者Delete ()
中来确定需要更新或者删除的数据范围。
Belong To
在创建记录中的关联创建中,我们已经了解过如何将两个表通过外键联系。除此之外,还有很多种方式进行表的关联。
belongs to
会与另一个模型建立了一对一的连接。这种模型的每一个实例都“属于”另一个模型的一个实例。
例如,您的应用包含 user 和 company,并且每个 user 能且只能被分配给一个 company。下面的类型就表示这种关系。注意,在 User
对象中,有一个和 Company
一样的 CompanyID
。默认情况下, CompanyID
被隐含地用来在 User
和 Company
之间创建一个外键关系,因此必须包含在 User
结构体中才能填充 Company
内部结构体。
即 CompanyID int
和 Company Company
才可以共同表示一个外键,Company Company
没有实际作用,只是指明了外键连接的是哪张表。
在 User 表中创建记录时,如果不传入 company 字段则会报错(因为有外键),因此可以使用关联创建。GORM 会自动找到两个表对应的记录,并关联起来。
// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
gorm. Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}
要定义一个 belongs to 关系,数据库的表中必须存在外键。默认情况下,外键的名字,使用拥有者的类型名称加上表的主键的字段名字
例如,定义一个 User 实体属于 Company 实体,那么外键的名字一般使用 CompanyID。
GORM 同时提供自定义外键名字的方式,如下例所示。
type User struct {
gorm. Model
Name string
CompanyRefer int
Company Company `gorm: "foreignKey: CompanyRefer"`
// 使用 CompanyRefer 作为外键
}
type Company struct {
ID int
Name string
}
对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。如果不想默认使用主键作为外键,也可以使用标签 references
来更改它,例如:
type User struct {
gorm. Model
Name string
CompanyID string
Company Company `gorm: "references: Code"` // 使用 Code 作为引用
}
type Company struct {
ID int
Code string
Name string
}
如何解决关联查询的问题,如何同时查询两个表?
如果对一个关联的表使用之前的查询语句去查询两个表的内容,比如以下情况:
var user User
db.First (&user)
fmt.Println (user. Name, user. Company. Name)
则只会输出 user. Name
的值,而 user. Company. Name
的值为零值。这是因为 user. Company. Name
的数据属于另外一个表 company
,使用以上方式查询记录,在 SQL 语句中指定了数据表为 users
,SELECT * FROM users WHERE ……
,所以查询不到数据。
如果想查询出两个表的数据,需要使用 Preload
和 Joins
预加载 belongs to 关联的记录,查看 预加载 获取详情。
创建多条记录。
db.Create (&[]User{
{Name: "liuchao 1", Company: Company{Name: "company 1"}},
{Name: "liuchao 2", Company: Company{Name: "company 2"}},
{Name: "liuchao 3", Company: Company{Name: "company 3"}},
})
使用 Preload
方法查询。
var user User
db.Preload ("Company"). First (&user)
fmt.Println (user. Name, user. Company. Name)
使用 Joins
方法查询。
var user User
db.Joins ("Company"). First (&user)
fmt.Println (user. Name, user. Company. Name)
两者的区别在于 Preload
方法会执行多条 SQL 语句,而 Joins
方法只会执行一条 SQL 语句(使用 JOIN 操作 )。
Has Many
has many
与另一个模型建立了一对多的连接。不同于 has one
,拥有者可以有零或多个关联模型。
例如,您的应用包含 user 和 credit card 模型,且每个 user 可以有多张 credit card。
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm. Model
Name string
CreditCards []CreditCard
}
type CreditCard struct {
gorm. Model
Number string
UserID uint
}
与 Belong To 类型不同,此时的外键必须设置在
CreditCard
中,这是因为只有一张信用卡才能唯一对应一个用户,反之则不行。
要定义 has many
关系,同样必须存在外键。默认的外键名是拥有者的类型名加上其主键字段名
例如,要定义一个属于 User
的模型,则其外键应该是 UserID
。
此外,想要使用另一个字段作为外键,您可以使用 foreignKey
标签自定义它:
type User struct {
gorm. Model
CreditCards []CreditCard `gorm: "foreignKey: UserRefer"`
}
type CreditCard struct {
gorm. Model
Number string
UserRefer uint
}
GORM 通常使用拥有者的主键作为外键的值。对于上面的例子,它是 User
的 ID
字段。
为 user 添加 credit card 时,GORM 会将 user 的 ID
字段保存到 credit card 的 UserID
字段。
同样的,您也可以使用标签 references
来更改它,例如:
type User struct {
gorm. Model
MemberNumber string
CreditCards []CreditCard `gorm: "foreignKey: UserNumber; references: MemberNumber"`
}
type CreditCard struct {
gorm. Model
Number string
UserNumber string
}
在大型的系统、高并发的系统中一般不使用外键约束,因为外键约束对数据库性能有很大的影响,所以一般是在业务层面保证数据的一致性。同时外键约束也有很大的优点:,可以保持数据的完整性,即使是业务代码考虑不严谨。
创建测试记录。
db.Create (&[]User{
{Name: "liuchao 1", CreditCart: []CreditCart {{Number: "123456"}, {Number: "654321"}} },
{Name: "liuchao 2", CreditCart: []CreditCart {{Number: "abcdef"}, {Number: "fedcba"}} },
{Name: "liuchao 3", CreditCart: []CreditCart {{Number: "qwerty"}, {Number: "ytrewq"}} },
})
查询两个表的数据,同样可以使用 Preload
方法,但不能使用 Joins
方法(会报错)。
var user User
db.Preload ("CreditCards"). First (&user)
for _, card := range user. CreditCards{
fmt.Println (card. Number)
}
为什么不能使用 Joins
方法?
在 Belongs To 关系中,主表(如 User
)通过外键(如 CompanyID
)指向从表(如 Company
)。这种关系的特点是:
- 主表包含外键:
User
表中有一个外键CompanyID
,指向Company
表的主键。 - 一对一关系:每个
User
只能关联一个Company
。
但是在 Has Many 关系中,主表(如 User
)可以关联多个从表记录(如 CreditCart
)。这种关系的特点是:
- 从表包含外键:
CreditCart
表中有一个外键UserID
,指向User
表的主键。 - 一对多关系:一个
User
可以关联多个CreditCart
。
Joins ("CreditCard")
会生成类似这样的 SQL:
SELECT * FROM users
JOIN credit_cards ON users. id = credit_cards. user_id
对于有多个 CreditCard 的用户,会产生多条记录(每个 CreditCard 对应一行)。而 GORM 的 First ()
方法期望只处理单条记录,无法将多行 JOIN 结果正确映射到切片字段,导致反射错误。
Joins ("Company")
生成的 SQL:
SELECT * FROM users
JOIN companies ON users. company_id = companies. id
由于是一对一关系,JOIN 结果始终只有单行,可以完美映射到结构体的单个嵌套对象。
而Preload ("CreditCards")
会执行两条 SQL:
SELECT * FROM users; -- 先查询主表
SELECT * FROM credit_cards WHERE user_id IN (...); -- 再批量查询关联表
这种方式将关联数据以切片形式正确填充,避免了 JOIN 带来的行数膨胀问题。
当尝试用Joins ()
加载 Has Many 关联时,GORM 的反射机制试图将多行 JOIN 结果映射到切片字段,但由于First ()
只取第一条记录,导致:
- 反射试图操作切片值时出现类型不匹配
- 最终抛出
reflect: call of reflect. Value. Field on slice Value
所以
- 对于Has Many关系,坚持使用
Preload ()
- 对于Belongs To/Has One关系,可以安全使用
Preload ()
或Joins ()
- 对于Many To Many关系,需要使用
Association ()
Many To Many
Many To Many 多对多的关系会在很多实际开发的场景被用到。
Many to Many 会在两个 model 中添加一张连接表。
例如,您的应用包含了 user 和 language,且一个 user 可以说多种 language,多个 user 也可以说一种 language。
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm. Model
Name string
Languages []Language `gorm: "many 2 many: user_languages;"`
}
type Language struct {
gorm. Model
Name string
}
// 连接表:user_languages
// foreign key: user_id, reference: users. id
// foreign key: language_id, reference: languages. id
当使用 GORM 的 AutoMigrate
为 User
创建表时,GORM 会自动创建连接表。
对于 many 2 many
关系,连接表会同时拥有两个模型的外键,若要重写它们,可以使用标签 foreignKey
、references
、joinforeignKey
、joinReferences
。当然,您不需要使用全部的标签,你可以仅使用其中的一个重写部分的外键、引用。
type User struct {
gorm. Model
Profiles []Profile `gorm: "many 2 many: user_profiles; foreignKey: Refer; joinForeignKey: UserReferID; References: UserRefer; joinReferences: ProfileRefer"`
Refer uint `gorm: "index:, unique"`
}
type Profile struct {
gorm. Model
Name string
UserRefer uint `gorm: "index:, unique"`
}
// 连接表:user_profiles
// foreign key: user_refer_id, reference: users. refer
// foreign key: profile_refer, reference: profiles. user_refer
创建测试记录。
db.Create (&[]User{
{Name: "liuchao 1", Languages: []Language {{Name: "chinese"}, {Name: "english"}} },
{Name: "liuchao 2", Languages: []Language {{Name: "chinese"}, {Name: "japanese"}} },
{Name: "liuchao 3", Languages: []Language {{Name: "chinese"}, {Name: "korean"}} },
{Name: "liuchao 4", Languages: []Language {{Name: "english"}, {Name: "french"}} },
})
如果直接使用以上方式创建记录,那么 language 表中就会出现如下图所示的重复数据,这是因为每次创建用户时都直接新建了 Language
实例,而没有检测数据库中是否已存在同名语言记录(GORM 不会自动检测)。
第一种解决方式是在插入记录之前,先使用 FirstOrCreate
检查是否有同名的 language,如果有就不会创建新数据,直接使用数据表中的已有数据。
type User struct {
gorm.Model
Name string
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string `gorm:"index:,unique"` // 关键点:唯一约束
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&User{}, &Language{})
// 批量创建用户
users := []User{
{Name: "liuchao1", Languages: getOrCreateLanguages(db, "chinese", "english")},
{Name: "liuchao2", Languages: getOrCreateLanguages(db, "chinese", "japanese")},
{Name: "liuchao3", Languages: getOrCreateLanguages(db, "chinese", "korean")},
{Name: "liuchao4", Languages: getOrCreateLanguages(db, "english", "french")},
}
db.Create(&users)
}
func getOrCreateLanguages(db *gorm.DB, names ...string) []Language {
var languages []Language
for _, name := range names {
var lang Language
db.FirstOrCreate(&lang, Language{Name: name}) // 关键点:查询或创建
languages = append(languages, lang)
}
return languages
}
第二种解决方式就是在第一种方法的基础上使用 BeforeCreate
钩子,Before Create。
type User struct {
gorm.Model
Name string
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string `gorm:"index:,unique"` // 仍然建议加唯一约束
}
// BeforeCreate 钩子:自动处理语言记录的唯一性
func (u *User) BeforeCreate(tx *gorm.DB) error {
for i := range u.Languages {
lang := &u.Languages[i]
if err := tx.Where(Language{Name: lang.Name}).FirstOrCreate(lang).Error; err != nil {
return err
}
}
return nil
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&User{}, &Language{})
// 直接创建用户,无需手动处理语言
db.Create(&[]User{
{Name: "liuchao1", Languages: []Language{{Name: "chinese"}, {Name: "english"}}},
{Name: "liuchao2", Languages: []Language{{Name: "chinese"}, {Name: "japanese"}}},
{Name: "liuchao3", Languages: []Language{{Name: "chinese"}, {Name: "korean"}}},
{Name: "liuchao4", Languages: []Language{{Name: "english"}, {Name: "french"}}},
})
}
两种方式都是相同的原理,都能够实现下图的效果。
查询两个表中的数据,同样只能使用 Preload
,而不能使用 Joins
方法。原理与 Has Many 相同。
var user User
// 查找第一个user的languages
db.Preload("Languages").First(&user)
for _, language := range user.Languages{
fmt.Println(language.Name)
}
如果我已经取出一个 user,但是这个 user 我们之前没有使用 Preload
来加载对应的 Languages,即只有 user
的 gorm.Model
字段(如使用 db.First(&user)
单独对 user 表查询 ),那么可以使用 Association
,比如:
var user User
db.First(&user) // 只有user的数据,没有language的数据
var laguages []Language
_ = db.Model(&user).Association("Languages").Find(&languages)
for _, language := range laguages{
fmt.Println(language.Name)
}
自定义表名
前面我们都是使用 GORM 自动根据结构体创建的蛇形表名,实际上表名也可以自定义。自定义表名通常有两种情况:
- 我们自己定义表名是什么
- 统一的给所有的表名加上一个前缀
在 GORM 中可以通过给某一个 struct 添加 TableName
方法来自定义表名:
func (User) TableName() string{
return "my_user"
}
如果想统一给所有表名都加上前缀,则可以在打开数据库时传入 NamingStrategy
参数:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy:schema.NamingStrategy{
TablePrefix: "mxshop_",
},
Logger: newLogger,
})
需要注意,
TableName
和NamingStrategy
不能同时生效,TableName
优先级更大。
Before Create
有一种需求,比如在下方的表结构中,我们希望每个记录创建的时候自动加上当前时间,即加入到 AddTime 中。
type User struct {
gorm.Model
Name string
AddTime sql.NullTime
}
AddTime
使用sql.NullTime
而不使用time.time
的原因同样在于防止未传值导致零值报错问题。零值更新问题
这时我们可以使用 BeforeCreate
这个钩子,除此之外还有 AfterCreate
,同理保存、更新、删除都有类似的钩子方法。
func (u *User) BeforeCreate(tx *gorm.DB) (err error){
u.AddTime = time.Now()
return
}
完整示例代码
package main
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 增删改查
// type User struct {
// gorm.Model
// Name string
// Age uint
// }
// Belongs TO
// type User struct {
// gorm.Model
// Name string
// CompanyID int
// Company Company
// }
// type Company struct {
// ID int
// Name string
// }
// Has Many
// type User struct {
// gorm.Model
// Name string
// CreditCart []CreditCart
// }
// type CreditCart struct {
// gorm.Model
// Number string
// UserID uint
// }
// Many To Many
type User struct {
gorm.Model
Name string
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string `gorm:"index:,unique"`
}
func main() {
dsn := "root:123456@tcp(192.168.101.134:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
newlogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
Colorful: true, // Disable color
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newlogger,
})
if err != nil {
panic(err)
}
// 创建记录
// db.AutoMigrate(&User{})
// users := []User{
// {Name: "liuchao1", Age: 19},
// {Name: "liuchao2", Age: 20},
// {Name: "liuchao3", Age: 21},
// }
// db.Create(&users)
// 查询记录
// var user User
// var users []User
// db.First(&user, "age = ?", "21") // 查询id为1的user
// fmt.Println(user.ID, user.Name, user.Age)
// db.Where("age = ?", "21").First(&user)
// fmt.Println(user.ID, user.Name, user.Age)
// db.Where(&User{Name: "liuchao3"}).First(&users)
// fmt.Println(user.ID, user.Name, user.Age)
// db.Where(map[string]interface{}{"name": "liuchao2"}).Find(&users)
// for _, user := range users {
// fmt.Println(user.ID, user.Name, user.Age)
// }
// 更新记录
// db.Model(&User{}).Where("name = ?", "liuchao1").Update("age", 100)
// db.Model(&User{}).Where("id = ?", "1").Updates(User{Name: "liuchao5", Age: 200})
// 删除记录
// db.Delete(&User{}, []int{4, 5, 6, 7})
// db.Where("age < ?", 100).Delete(&User{})
// Belongs TO
// db.AutoMigrate(&User{}, &Company{})
// db.Create(&[]User{
// {Name: "liuchao1", Company: Company{Name: "company1"}},
// {Name: "liuchao2", Company: Company{Name: "company2"}},
// {Name: "liuchao3", Company: Company{Name: "company3"}},
// })
// var user User
// db.Preload("Company").First(&user, 1)
// fmt.Println(user.Name, user.Company.Name)
// db.Joins("Company").First(&user, 2)
// fmt.Println(user.Name, user.Company.Name)
// Has Many
// db.AutoMigrate(&User{}, &CreditCart{})
// db.Create(&[]User{
// {Name: "liuchao1", CreditCart: []CreditCart{{Number: "123456"}, {Number: "654321"}}},
// {Name: "liuchao2", CreditCart: []CreditCart{{Number: "abcdef"}, {Number: "fedcba"}}},
// {Name: "liuchao3", CreditCart: []CreditCart{{Number: "qwerty"}, {Number: "ytrewq"}}},
// })
// var user User
// db.Preload("CreditCart").First(&user)
// for _, cart := range user.CreditCart {
// fmt.Println(user.Name, cart.Number, cart.UserID)
// }
// Many To Many
// db.AutoMigrate(&User{}, &Language{})
// db.Create(&[]User{
// {Name: "liuchao1", Languages: []Language{{Name: "chinese"}, {Name: "english"}}},
// {Name: "liuchao2", Languages: []Language{{Name: "chinese"}, {Name: "japanese"}}},
// {Name: "liuchao3", Languages: []Language{{Name: "chinese"}, {Name: "korean"}}},
// {Name: "liuchao4", Languages: []Language{{Name: "english"}, {Name: "french"}}},
// })
var user User
// db.Preload("Languages").First(&user)
// for _, language := range user.Languages{
// fmt.Println(language.Name)
// }
db.First(&user)
var languages []Language
_ = db.Model(&user).Association("Languages").Find(&languages)
for _, language := range languages {
fmt.Println(user.Name, language.Name)
}
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
for i := range u.Languages {
lang := &u.Languages[i]
if err := tx.Where(Language{Name: lang.Name}).FirstOrCreate(lang).Error; err != nil {
return err
}
}
return nil
}