Object Relational Mapping, ORM 对象关系映射,用于将数据库中的数据与代码中的结构体进行映射,不同语言的实现方式略有区别,但是目的基本相同。
GoLang 中有多种映射库,这里简单介绍常见的 GORM 使用方式。
简介
ORM 实际上就是对数据库的操作进行封装,屏蔽数据库操作细节,从而简化开发,提高效率,GoLang 的 ORM 可以参考 gorm.io,其使用方法简单介绍如下。
GORM 倾向于使用约定而不是配置,默认使用 ID 作为主键,字段名的蛇型作为列名,结构体的蛇型复数作为表名,而且内部通过 gorm.Model
结构体提供了 CreatedAt
、UpdatedAt
字段跟踪创建、更新时间,通过 DeletedAt
作为逻辑删除时间。
例如结构体 UserInfo
实际转换为 user_infos
,字段名UserName
将转换为 user_name
,以此类推。
示例
如下是一个简单的示例,可以先手动创建表。
CREATE TABLE IF NOT EXISTS `users` (
`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` char(64) NOT NULL COMMENT '用户名',
`age` int NOT NULL COMMENT '用户的年龄',
`gender` enum('no','male','female') DEFAULT 'no' COMMENT '性别'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO users(id, name, age, gender) VALUES(1, "andy", 18, "male");
CREATE TABLE IF NOT EXISTS `roles` (
`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` char(64) NOT NULL COMMENT '角色名'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO roles(id, name) VALUES(1, "admin");
CREATE TABLE IF NOT EXISTS `map_user_role` (
`user_id` int NOT NULL,
`role_id` int NOT NULL,
PRIMARY KEY(user_id, role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO map_user_role(user_id, role_id) VALUES(1, 1);
通过 GORM 执行 SQL 如下。
package main
import (
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
dblog "gorm.io/gorm/logger"
)
type UserInfo struct {
ID int
Name string
Age int
Gender string
}
func (*UserInfo) TableName() string {
return "users"
}
func main() {
dsn := "root:msandbox@tcp(127.0.0.1:8027)/test?charset=utf8&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: dblog.Default.LogMode(dblog.Info),
})
if err != nil {
log.Printf("Open database failed, %v.", err)
return
}
//db.Debug().AutoMigrate(&UserInfo{}) // 迁移Schema字段类型未指定
// Create
db.Create(&UserInfo{Name: "Andy", Age: 18, Gender: "male"})
// 只写入指定字段,或者通过 Omit() 忽略字段
db.Select("Name", "Age").Create(&UserInfo{Name: "Andy", Age: 18, Gender: "male"})
// Read
var user UserInfo
db.First(&user, 1) // 根据主键查找
db.First(&user, "name = ?", "Andy") // 通过索引查找
// Update
db.Model(&user).Update("Price", 20) // 将 Age 更新为 20
db.Model(&user).Updates(&UserInfo{Name: "andy", Age: 20}) // 仅更新非零值字段
db.Model(&user).Updates(map[string]interface{}{"Name": "andy", "Age": "20"})
// Delete
db.Delete(&UserInfo{}, 1) // 根据ID删除
}
其中 gorm.io/driver/mysql
是 MySQL 驱动,实际上就是 github.com/go-sql-driver/mysql
只是进行了重新命名。
gorm 用 tag 来标识 MySQL 里面的约束,创建索引只需要直接指定列即可,如果需要多列组合索引,直接让索引的名字相同即可。
常用技巧
创建表
可以通过 db.HasTable()
来判断表是否存在,其入参可以使用两种形式:A) 字符串;B) 模型的地址类型。其判断方式是直接查询 INFORMATION_SCHEMA.TABLES
表中的数据。
if ok := DB.HasTable("foos"); ok {
t.Errorf("Table should not exist, but does")
}
if ok := DB.HasTable(&Foo{}); ok {
t.Errorf("Table should not exist, but does")
}
定义模型时,必须指定字段的首字母为大写,否则无法创建字段,同时可以使用 gorm tag
进行制定,可以参考 Declaring Models,不过有些调试起来比较复杂,还是直接创建比较好。
db.CreateTable(&User{})
r1 := db.DropTable("Users")
r2 := db.DropTable(&User{})
默认创建的表名为复数形式,例如 User
创建后的表名为 users
,如果不想创建复数形式的表名,可以通过如下的语句设置。
db.SingularTable(true)
如果要自己定义,可以通过如下方式修改。
type UserInfo struct {} // 默认表名是user_infos
// 设置UserInfo的表名为users
func (UserInfo) TableName() string {
return "users"
}
func (u UserInfo) TableName() string {
if u.Role == "admin" {
return "admin_users"
} else {
return "users"
}
}
更新时间
gorm 提供了三个与时间相关的字段,会在操作时自动更新,包括了 CreatedAt
UpdatedAt
DeletedAt
。
----- 字段CreatedAt用于存储记录的创建时间
db.Create(&user) // 将会设置CreatedAt为当前时间
----- 要更改它的值, 需要使用Update
db.Model(&user).Update("CreatedAt", time.Now())
----- 字段UpdatedAt用于存储记录的修改时间
db.Save(&user) // 将会设置UpdatedAt为当前时间
db.Model(&user).Update("name", "jinzhu") // 将会设置UpdatedAt为当前时间
如果要使用不同的字段,可以在 tag
中配置 autoCreateTime
、autoUpdateTime
标签,要保存时间戳而不是 time
,只需简单地将 time.Time
修改为 int
即可。
type User struct {
CreatedAt time.Time // Set to current time if it is zero on creating
UpdatedAt int // Set to current unix seconds on updating or if it is zero on creating
Updated int64 `gorm:"autoUpdateTime:nano"` // Use unix nano seconds as updating time
Updated int64 `gorm:"autoUpdateTime:milli"`// Use unix milli seconds as updating time
Created int64 `gorm:"autoCreateTime"` // Use unix seconds as creating time
}
嵌入结构体
上述示例中嵌入结构体可以直接使用,可以增加 gorm:"embedded"
显示表示嵌入,或者通过 gorm:"embedded;embeddedPrefix:author_"
指定前缀。
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等价于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}
连接池
使用的是标准库 database/sql
维护的连接池。
// 设置空闲连接池中连接的最大数量
db.SetMaxIdleConns(10)
// 设置打开数据库连接的最大数量
db.SetMaxOpenConns(100)
// 设置了连接可复用的最大时间
db.SetConnMaxLifetime(time.Hour)
高级技巧
Clause
GORM 内部会使用 SQL Builder 生成 SQL,而对于每个操作都会创建一个 *gorm.Statement
对象,所有的 GORM API 都是在为 Statement 添加或修改 Clause,最后根据这些 Clause 生成 SQL 语句。
例如,当执行 Limit
时会在 Statement 中添加以下 Clause 语句。
// Limit specify the number of records to be retrieved
func (db *DB) Limit(limit int) (tx *DB) {
tx = db.getInstance()
tx.Statement.AddClause(clause.Limit{Limit: limit})
return
}
然后在最后的 Callback 中构建最终的 SQL 语句,所以,可自定义 Clause 并与 GORM 一起使用。
不同数据库 Clause 可能会生成不同的 SQL 语句,例如:
db.Offset(10).Limit(5).Find(&users)
// SQL Server
SELECT * FROM "users" OFFSET 10 ROW FETCH NEXT 5 ROWS ONLY
// MySQL
SELECT * FROM `users` LIMIT 5 OFFSET 10
Upsert
MySQL 提供了 ON DUPLICATE KEY UPDATE
特有功能的支持,也就是如果 Insert 已经存在的记录,那么就执行 Update 操作,可以简化这一场景的实现逻辑,其它数据库也有类似的逻辑。
import "gorm.io/gorm/clause"
// 在冲突时,什么都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// 在`id`冲突时,将列更新为默认值
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL
// 使用SQL语句
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));
// 在`id`冲突时,将列更新为新值
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL
// 在冲突时,更新除主键以外的所有列到新值。
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "age"="excluded"."age", ...;
详见 Gorm Upsert 中的介绍。