-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
GORM V2 Release Note Draft CN
GORM 2.0 是基于用户过去几年中的反馈进行思考后的重写,在该发行版本中将会引入不兼容 API 改动。
(本文为草稿文案,仍在更新中... 本文将仅包含部分较重要更新,其它请参阅文档)
主要更新:
- 性能优化
- 代码模块化
- Context、批量插入、Prepared Statment、DryRun 模式、Join Preload, Find 到 Map 支持
- 关联关系,替换 Join Table,Association 模式对批量数据支持及优化
- SQL Builder 优化, Upsert, Locking
- 插入时间、更新时间可同时支持多字段,并加入unix (nano) second 支持
- 字段权限设置:只读、只创建、只更新、只写、完全忽略
- 全新的 Migrator, Logger
- 统一命名策略系统 (统一设置表名、字段名、join table、外键、约束、索引名规则)
- 更好的数据类型定义支持 (例如 JSON)
- 全新插件系统, Hooks API
- GORM 代码迁移至组织 github.com/go-gorm, go import 地址更改为
gorm.io/gorm
,如想继续使用原版本请保持引用github.com/jinzhu/gorm
- 迁移不同数据库 driver 为独立项目,方便对于某 driver 进行功能扩展或添加新 driver 支持,例如 github.com/go-gorm/sqlserver
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlserver"
)
func init() {
// 初始化 gorm DB 时,可以通过 Config 修改配置,例如 NowFunc,不再允许通过全局变量方式进行修改,后续介绍中还包含部分其它选项介绍
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{NowFunc: func() time.Time { return time.Now().Local() }})
db, err := gorm.Open(mysql.Open("gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True"), &gorm.Config{})
db, err := gorm.Open(postgres.Open("user=gorm password=gorm DB.name=gorm port=9920 TimeZone=Asia/Shanghai"), &gorm.Config{})
// 终端用户 API 尽量保持了兼容,大多数 API 和之前版本用法相同,详情请参考文档
db.Create(&user)
db.First(&user, 1)
db.Model(&user).Update("Age", 18)
db.Model(&user).Omit("Role").Updates(map[string]interface{}{"Name": "jinzhu", "Role": "admin"})
db.Delete(&user)
}
- 所有 SQL 操作都提供了 Context 提供了支持
// 设置该请求的 Context
DB.Context(ctx).Find(&users)
// 在 Context 方法设置 context 后,后续的请求都将使用该 context
// 方便将 context 放在统一入口处理设置,例如:
tx := DB.Context(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")
- Logger 也会接受 context,方便日志追踪处理
- 将 slice 数据传入
Create
创建时,会创建单条 SQL 语句插入全部数据,并将主键值回填 - 如果数据包含关联对象等,也会创建一条
SQL
对关联中的新对象进行插入 - 批量插入数据都会调用数据定义的
Hooks
方法
var users = []User{...}
DB.Create(&users)
for _, user := range users {
user.ID // 1,2,3,4,5
}
- Prepared Statement 模式中,对于执行的
SQL
会生成 prepared statement 并缓存,加速后续程序执行性能 - Prepared Statement 模式可全局打开或者针对会话打开
// 在初始化时打开全局模式,所有的操作都会进行 prepared statement 缓存
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
// 单会话模式
DB.Session(&Session{PrepareStmt: true}).First(&users, 1)
// 连续会话模式
tx := DB.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
根据当前的 driver 生成相应的 SQL
,但不执行,方便检查、测试、使用生成的 SQL
// 全局模式
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{DryRun: true})
stmt := db.Find(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1
stmt.Vars //=> []interface{}{1}
// 会话模式
stmt := DB.Session(&Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars //=> []interface{}{1}
- 之前可通过
Preload
方法预加载关联时,其会产生 2 条 SQL 来查询不同的表,通过Join
模式,将只会产生一条 Join SQL 加载数据 - Join Preload 也会对批量数据查询也会生效,会处理
NULL
值问题
DB.Joins("Company").Joins("Manager").Joins("Account").First(&user, 1)
DB.Joins("Company").Joins("Manager").Joins("Account").First(&user, "users.name = ?", "jinzhu")
DB.Joins("Company").Joins("Manager").Joins("Account").Find(&users, "users.id IN ?", []int{1,2,3,4,5})
将查询结果查询到 map[string]interface{}
或者 []map[string]interface{}
中,方便某些情况下的查询
var result map[string]interface{}
DB.Model(&User{}).First(&result, "id = ?", 1)
var results []map[string]interface{}
DB.Model(&User{}).Find(&results)
- 对于 Belongs To, 单表 Belongs To, Has One, Has One Polymorphic, Has Many, Has Many Polymorphic, 单表 Has Many, Many2Many, 单表 Many2Many 关系支持重新进行了实现,避免部分极端情况下判断关联错误
- 简化通过
tag
设置外键逻辑 (仅需要在没有使用默认命名规则时,才需要通过 tag 设置)
type Profile struct {
gorm.Model
Refer string
Name string
}
type User struct {
gorm.Model
Profile Profile `gorm:"ForeignKey:ProfileID;References:Refer"`
ProfileID int
}
// 如果 ForeignKey 的字段为本对象所有,则为 Belongs To 关系, References 为所引用的关联对象的字段
// 如果 ForeignKey 的字段为关联对象所有时,则为 Has One/Many 关系, References 为本对象引用字段
// 如果定义了many2many,ForeignKey 为关联表引用的本对象的主键, JoinForeignKey 为关联表与本对象间的外键,
// References 为关联表引用的关联对象的主键, JoinReferences 为关联表与关联对象间的外键
// 如果有设置多个外键,可以用逗号隔开
type Tag struct {
ID uint `gorm:"primary_key"`
Locale string `gorm:"primary_key"`
Value string
}
type Blog struct {
ID uint `gorm:"primary_key"`
Locale string `gorm:"primary_key"`
Subject string
Body string
Tags []Tag `gorm:"many2many:blog_tags;"`
// 默认用使用对象的全部主键 (ID, Locale) 来创建关联表
SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"`
// 对于 BLog, Tag 都将只使用 ID 做为主键
LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"`
// 对于 Blog 使用ID, Locale作为主键, Tag 只使用ID做为主键
}
可以更简单的来设置 Many2Many
的 JoinTable
,替换后的 JoinTable
可获得普通模型 Model
的全部功能,例如添加更多字段,Soft Delete
,调用 Hooks
等功能
type Person struct {
ID int
Name string
Addresses []Address `gorm:"many2many:person_addresses;"`
}
type Address struct {
ID uint
Name string
}
type PersonAddress struct {
PersonID int
AddressID int
CreatedAt time.Time
DeletedAt gorm.DeletedAt
}
func (PersonAddress) BeforeCreate(db *gorm.DB) error {
// ...
}
// PersonAddress 必须包含 Many2Many 所需要的外键字段,否则会返回错误
err := DB.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})
- GORM 删掉了
Related
方法,避免部分用户容易发生未指定或指定了错误的外键导致的错误,请使用gorm.Model(&user).Association("Pets").Find(&pets)
替换该方法 -
Association
加入了对批量数据的支持,例如:
// 找出所有用户包含的全部角色
gorm.Model(&users).Association("Role").Find(&roles)
// 把用户A 从所有用户拥有的 Team 中删除
gorm.Model(&users).Association("Team").Delete(&userA)
// 查询所有用户中的 team 一共有多少人员 (不计重复人员)
gorm.Model(&users).Association("Team").Count()
// 对于 Append, Replace 方法,如果想操作批量数据,参数的长度必须与批量数据长度相同
// 例如:有三个用户,将 userA 添加到 user1 组,将 userB 添加到 user2 组,将 userA, user, userC 添加到 user3 组
var users = []User{user1, user2, user3}
gorm.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// 将 user1 的组员重置为只有 userA,将 user2 组员重置为 userB, 将 user3 组员重置为 userA, userB, userC
gorm.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
- GORM 内部全部采取了 SQL Builder 的方式来生成执行的
SQL
。 - GORM 执行一次操作过程中,会创建一个
Statement
对象,所有的 GORM API 都将为这个Statement
添加Clause
, 最终 GORM 将会在 callbacks 中根据这次Statement
所包含的Clauses
生成最终 SQL 语句并执行。 - 对于不同的数据库来说,相同的 Clause 可能会生成的不同的 SQL
以 Upsert
举例:
// clause.OnConflict 对于不同数据库 (sqlite, mysql, postgres, sql server) 提供了兼容的 upsert 支持
DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&users)
DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"name": "jinzhu", "age": 18}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT ***; Sql Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL
DB.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users) // SELECT * FROM `users` FOR UPDATE
DB.Clauses(clause.Locking{
Strength: "SHARE",
Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users) // SELECT * FROM `users` FOR SHARE OF `users`
type User struct {
CreatedAt time.Time // 创建对象时如果该字段无值,再更新为当前时间
UpdatedAt int // 更新对象时,将该字段更新为当前 unix second),创建对象时如果该字段无值,更新为当前时间
Updated int64 `gorm:"autoupdatetime:nano"` // 为自定义字段使用 unix nano second 做为更新时间
Created int64 `gorm:"autocreatetime"` // 为自定义字段使用 unix second 做为插入时间
}
type User struct {
Name string `gorm:"<-:create"` // 允许从数据库读取,创建时插入到数据库,更新时将忽略该字段
Name string `gorm:"<-:update"` // 允许从数据库读取,更新时更新到数据库,创建时将忽略该字段
Name string `gorm:"<-"` // 允许从数据库读取,创建,更新时插入到数据库
Name string `gorm:"->:false;<-:create"` // 允许创建时插入到数据库,更新时、查询时将忽略该字段
Name string `gorm:"->"` // 允许从数据库读取,不允许创建、更新
Name string `gorm:"-"` // 不允许用户读取,创建及更新
}
- Migrator 在创建表时将会自动创建数据库的外键 (在使用
DropTable
删除表时会忽略/删除相关外键约束,来避免删除失败) - Migrator 更加独立,对各数据库提供了更好的支持,保证统一的 API 接口,因此可基于其设计更方便的
Migrator Tool
(例如: sqlite 不支持ALTER COLUMN
,DROP COLUMN
,在做相关操作时,会在事务内使用修改后的表信息新建临时表,导入原数据,并在删除原表后,将临时表重命名为原表名。) - 支持通过 Tag 设置 Check 约束
- 更丰富的 tag 设置索引选项
type UserIndex struct {
Name string `gorm:"check:named_checker,(name <> 'jinzhu')"`
Name2 string `gorm:"check:(age > 13)"`
Name4 string `gorm:"index"`
Name5 string `gorm:"index:idx_name,unique"`
Name6 string `gorm:"index:,sort:desc,collate:utf8,type:btree,length:10,where:name3 != 'jinzhu'"`
}
可以在初始化数据库时设置统一的命名逻辑接口 (字段名,表名,join table名,外键,Check 约束, 索引名),方便修改命名规则 (例如添加表名前缀)
https://github.com/go-gorm/gorm/blob/master/schema/naming.go#L14
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{TablePrefix: "t_", SingularTable: true},
})
Logger 除了上面提到的 context 支持,logger 还做了如下优化
- 自定义/关掉 log 中的颜色
- 加入 Slow SQL 的显示,默认时长 100ms
- 优化了 log 在不同数据库生成的 SQL 格式,使其可以复制粘帖执行
DB.Delete(&User{}) // 返回错误
DB.Model(&User{}).Update("role", "admin") // 返回错误
DB.Where("1=1").Delete(&User{}) // 全部删除
默认 GORM 的更新,插入,删除等操作都在一个事务中,以保证数据的一致性,因为可以在 hooks 中检查错误时返回错误并 rollback 所有操作。不过对于不需要该功能的应用程序来说性能会受到影响,新版本将提供在初始化时关闭相应功能的选项
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{SkipDefaultTransaction: true})
不再需要必须在 Hooks 里必须执行 SetColumn
才能修改插入到数据库的值,可以直接修改对象的值 (注意方法的接受值必须为指针)
func (user *User) BeforeUpdate(tx *gorm.DB) error {
user.Age = 18
return nil
}
Before/After Create/Update/Save/Find/Delete 等钩子必须定义为 func(tx *gorm.DB) error
类型的方法, 如果定义为其它类型,则会打印警告日志,并且不会生效
func (user *User) BeforeCreate(tx *gorm.DB) error {
// 可能通过 db.Statement 修改当前操作
tx.Statement.Select("Name", "Age")
tx.Statement.AddClause(clause.OnConflict{DoNothing: true})
// 使用传入的 *gorm.DB 执行的新操作将会与同前操作在同一事务内,但不会拥有的当前操作定义的 clauses
var role Role
err := tx.First(&role, "name = ?", user.Role).Error // SELECT * FROM roles WHERE name = "admin"
return err
}
TableName 方法将不再支持动态的表名生成,避免遇到一系列容易出错的问题. (批量插入数据包含不同表名, 空对象查询时预期使用了某字段动态生成表名...),会在第一次执行时缓存该值给后续该类对象使用
func (User) TableName() string {
return "t_user"
}
GORM 优化对了自定义类型的支持,可以定义一个数据结构来来支持所有的数据库 driver。
以下面的 JSON 为样例 (已支持 mysql, postgres, 代码参考: https://github.com/go-gorm/datatypes/blob/master/json.go)
import "gorm.io/datatypes"
type User struct {
gorm.Model
Name string
Attributes datatypes.JSON
}
DB.Create(&User{
Name: "jinzhu",
Attributes: datatypes.JSON([]byte(`{"name": "jinzhu", "age": 18, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)),
}
// 查询用户的 attributes 有没有 role 字段
DB.First(&user, datatypes.JSONQuery("attributes").HasKey("role"))
// 查询用户的 attributes 有没有 orgs->orga 字段
DB.First(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))
// 创建对象时
DB.Select("Name", "Age").Create(&user) // 使用 select 的字段创建对象
DB.Omit([]string{"Name", "Age"}).Create(&user) // 忽略 omit 的字段创建对象
// 更新对象时
DB.Model(&user).Select("Name", "Age").Updates(map[string]interface{}{"name": "jinzhu", "age": 18, "role": "admin"})
DB.Model(&user).Omit([]string{"Role"}).Update(User{Name: "jinzhu", Role: "admin"})
GORM 将不同数据库的实现进行了分拆,在使用 gorm.Open(dialector, &gorm.Config{})
初始化 gorm.DB
时,需要将实现了 gorm.Dialector
的数据库 Driver 传入
在初始化时,需要数据库的 driver 给 *gorm.DB
注册,修改,删除相应的 Callbacks
, 具体包含 create
, query
, update
, delete
, row
, raw
等类型, 他们的方法类型为 func(db *gorm.DB)
在所注册的 callbacks 中需要其中某个 callback
读取当前操作的 Statement
中定义的 Clauses
并生成相应的操作并执行,其它的 callbacks 可以为当前的 statement 添加、修改 Clause,或者判断错误等.
详情请参考默认 Callbacks
实现 github.com/go-gorm/gorm/tree/master/callbacks