Skip to content

GORM V2 Release Note Draft CN

Jinzhu edited this page Jun 9, 2020 · 27 revisions

GORM 2.0 Release Note (Draft)

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)
}

主要更新介绍

Context 支持

  • 所有 SQL 操作都提供了 Context 提供了支持
// 设置该请求的 Context
DB.WithContext(ctx).Find(&users)

// 在 Context 方法设置 context 后,后续的请求都将使用该 context
// 方便将 context 放在统一入口处理设置,例如:
tx := DB.WithContext(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 Statment 模式

  • 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)

DryRun 模式

根据当前的 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}

Join Preload 模式

  • 之前可通过 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})

Find To Map

将查询结果查询到 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做为主键
}

替换Join Table功能

可以更简单的来设置 Many2ManyJoinTable,替换后的 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{})

Association 模式

  • 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})

SQL Builder

  • 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 *** WHEN MATCHED THEN UPDATE SET ***; Sql Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

DB.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; Sql Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL

Locking

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`

插入时间、更新时间可同时支持多字段,并加入 unix (nano) second 支持 (设置为int类型)

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

  • 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'"`
}

统一的 NamingStrategy

可以在初始化数据库时设置统一的命名逻辑接口 (字段名,表名,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

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 里更新对象时可以直接对原对象进行修改

不再需要必须在 Hooks 里必须执行 SetColumn 才能修改插入到数据库的值,可以直接修改对象的值 (注意方法的接受值必须为指针)

func (user *User) BeforeUpdate(tx *gorm.DB) error {
  user.Age = 18
  return nil
}

Hooks 方法

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 方法

TableName 方法将再支持动态的表名生成,避免遇到一系列容易出错的问题. (批量插入数据包含不同表名, 空对象查询时预期使用了某字段动态生成表名...),会在第一次执行时缓存该值给后续该类对象使用

func (User) TableName() string {
  return "t_user"
}

更好的自定义数据类型支持 (JSON 为例)

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"))

Omit, Select 不强制要求传入slice, 可传入多个参数

// 创建对象时
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"})

全新插件API

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

Happy Hacking!

Clone this wiki locally