GORM从v1升级到v2的破坏性更新
今年GORM发布了新的版本V2,但是从V1到V2发生了一些破坏性的更新。在官网看的话,大部分关于V2特性的介绍都是有中文版本的,唯独对于破坏性的更新没有予以翻译,本文是直接从官网翻译过来的,有不妥之处还请指正。
原文链接:https://gorm.io/zh_CN/docs/v2_release_note.html#%E7%A0%B4%E5%9D%8F%E6%80%A7%E5%8F%98%E6%9B%B4
破坏性变更
我们试图列出大的破坏性改变或那些不能被编译器捕获的改变,如果你发现任何未列出的破坏性改变,请在这里创建一个问题或提交PR。
标签
GROM V2 更喜欢使用驼峰(camelCase)风格来写标签名称,而不再使用下划线风格(snake_case)。比如:auto_increment, unique_index, polymorphic_value, embedded_prefix,点击查看。
指定外键的标签已更改为foreignKey、引用、签出关联标记。详情请点击查看。
表名
TableName 不再允许动态表名,其结果将被缓存以备将来访问。
func (User) TableName() string {
return "t_user"
}动态表名请使用 Scopes 方法来定义,比如:
func UserTable(u *User) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Table("user_" + u.Role)
}
}
db.Scopes(UserTable(&user)).Create(&user)方法链和协程安全
为了减少GC分配,GORM V2将在使用方法链时共享语句,并且只为新初始化的 gorm.DB 或在新的会话方法之后创建新的语句实例。在重用 gorm.DB对象时,需要确保它是在一个新的会话方法后被调用。比如:
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// Safe for new initialized *gorm.DB
for i := 0; i < 100; i++ {
go db.Where(...).First(&user)
}
tx := db.Where("name = ?", "jinzhu")
// NOT Safe as reusing Statement
for i := 0; i < 100; i++ {
go tx.Where(...).First(&user)
}
ctxDB := db.WithContext(ctx)
// Safe after a `New Session Method`
for i := 0; i < 100; i++ {
go ctxDB.Where(...).First(&user)
}
ctxDB := db.Where("name = ?", "jinzhu").WithContext(ctx)
// Safe after a `New Session Method`
for i := 0; i < 100; i++ {
go ctxDB.Where(...).First(&user) // `name = 'jinzhu'` will apply to the query
}
tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
// Safe after a `New Session Method`
for i := 0; i < 100; i++ {
go tx.Where(...).First(&user) // `name = 'jinzhu'` will apply to the query
}点击查看方法链的详情。
默认值
GORM V2不会在创建后自动重新加载由数据库函数创建的默认值,点击查看默认值的详情。
软删除
如果模型有一个名为DeletedAt的字段,GORM V1将启用软删除;在V2中,只要字段类型被定义为GORM.DeletedAt,就可以使用该特性。例如:
type User struct {
ID uint
DeletedAt gorm.DeletedAt
}
type User struct {
ID uint
// field with different name
Deleted gorm.DeletedAt
}注意: gorm.Model已经使用了 gorm.DeletedAt, 如果你直接使用它,就不用做任何改动了。
BlockGlobalUpdate
GORM V2默认启用了BlockGlobalUpdate(更新一个表的所有数据记录)模式,要触发全局更新/删除,你必须使用一些条件或使用原始SQL或启用AllowGlobalUpdate模式,例如:
db.Where("1 = 1").Delete(&User{})
db.Raw("delete from users")
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})ErrRecordNotFound
当你使用First,Last,Take方法期望有结果返回时,GORM V2只会返回 ErrRecordNotFound。另外,RecordNotFound方法也被溢出了,使用 errors.Is 来检查对应的错误。例如:
err := db.First(&user).Error
errors.Is(err, gorm.ErrRecordNotFound)Hook 方法
在V2中,Before/After Create/Update/Save/Find/Delete 这些方法都必须定义为 func(tx *gorm.DB) error 的类型,它像插件回调一样有统一的接口,如果定义为其他类型,将打印一个警告日志,且不会生效,详细信息请查看钩子。
func (user *User) BeforeCreate(tx *gorm.DB) error {
// Modify current operation through tx.Statement, e.g:
tx.Statement.Select("Name", "Age")
tx.Statement.AddClause(clause.OnConflict{DoNothing: true})
// Operations based on tx will runs inside same transaction without clauses of current one
var role Role
err := tx.First(&role, "name = ?", user.Role).Error
// SELECT * FROM roles WHERE name = "admin"
return err
}Update Hook 支持 Changed
当用Update、Updates进行更新时,你可以在 BeforeUpdate,BeforeSave钩子中使用Changed方法检查字段是否被更改。
func (user *User) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Name", "Admin") { // if Name or Admin changed
tx.Statement.SetColumn("Age", 18)
}
if tx.Statement.Changed() { // if any fields changed
tx.Statement.SetColumn("Age", 18)
}
return nil
}
db.Model(&user).Update("Name", "Jinzhu") // update field `Name` to `Jinzhu`
db.Model(&user).Updates(map[string]interface{}{"name": "Jinzhu", "admin": false}) // update field `Name` to `Jinzhu`, `Admin` to false
db.Model(&user).Updates(User{Name: "Jinzhu", Admin: false}) // Update none zero fields when using struct as argument, will only update `Name` to `Jinzhu`
db.Model(&user).Select("Name", "Admin").Updates(User{Name: "Jinzhu"}) // update selected fields `Name`, `Admin`,`Admin` will be updated to zero value (false)
db.Model(&user).Select("Name", "Admin").Updates(map[string]interface{}{"Name": "Jinzhu"}) // update selected fields exists in the map, will only update field `Name` to `Jinzhu`
// Attention: `Changed` will only check the field value of `Update` / `Updates` equals `Model`'s field value, it returns true if not equal and the field will be saved
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"}) // Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"}) // Changed("Name") => false, `Name` not changed
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{"name": "jinzhu2", "admin": false}) // Changed("Name") => false, `Name` not selected to update
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"}) // Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"}) // Changed("Name") => false, `Name` not changed
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"}) // Changed("Name") => false, `Name` not selected to update插件
插件回调也需要被定义为func(tx *gorm.DB) error的类型,点击查看写插件的详情。
使用 struct 更新
当使用struct更新时,GORM V2允许使用Select来选择零值字段来更新它们,例如:
db.Model(&user).Select("Role", "Age").Update(User{Name: "jinzhu", Role: "", Age: 0})关联
GORM V1允许使用一些设置来跳过创建/更新关联,在V2中,你可以使用Select来做这个工作,例如:
db.Omit(clause.Associations).Create(&user)
db.Omit(clause.Associations).Save(&user)
db.Select("Company").Save(&user)与此同时,GORM V2不再允许使用Set("gorm:auto_preload", true)方法进行预加载,你可以使用Preload和clause.Associations来完成这项功能。例如:
// preload all associations
db.Preload(clause.Associations).Find(&users)此外,检查字段权限,可以用来跳过创建/更新的全局关联。
GORM V2在创建/更新记录时会使用 upsert 来保存关联,不会再保存完整的关联数据来保护你的数据不保存未完成的数据,例如:
user := User{
Name: "jinzhu",
BillingAddress: Address{Address1: "Billing Address - Address 1"},
ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
Emails: []Email{
{Email: "jinzhu@example.com"},
{Email: "jinzhu-2@example.com"},
},
Languages: []Language{
{Name: "ZH"},
{Name: "EN"},
},
}
db.Create(&user)
// BEGIN TRANSACTION;
// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1"), ("Shipping Address - Address 1") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "languages" ("name") VALUES ('ZH'), ('EN') ON DUPLICATE KEY DO NOTHING;
// INSERT INTO "user_languages" ("user_id","language_id") VALUES (111, 1), (111, 2) ON DUPLICATE KEY DO NOTHING;
// COMMIT;Join Table
在GORM V2中,一个 JoinTable 可以是一个全功能的模型,具有软删除、钩子和定义其他字段等特性,例如:
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 must defined all required foreign keys, or it will raise error
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})之后,可以使用普通的GORM方法来操作连接表数据,例如:
var results []PersonAddress
db.Where("person_id = ?", person.ID).Find(&results)
db.Where("address_id = ?", address.ID).Delete(&PersonAddress{})
db.Create(&PersonAddress{PersonID: person.ID, AddressID: address.ID})Count
Count只接受*int64作为参数。
事务
一些事务方法被删除,如RollbackUnlessCommitted,更推荐使用Transaction方法来使用事务功能。
db.Transaction(func(tx *gorm.DB) error {
// do some database operations in the transaction (use 'tx' from this point, not 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// return any error will rollback
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// return nil will commit the whole transaction
return nil
})点击查看事务的详情。
迁移工具(Migrator)
- 默认情况下,Migrator将创建数据库外键
Migrator更加独立,许多API都被重命名,以便通过统一的API接口为每个数据库提供更好的支持 - 如果列的大小、精度和Nullable值发生了变化,AutoMigrate将改变列的类型
- 支持通过标签进行检查
- 增强了索引的标记设置