今年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将改变列的类型
  • 支持通过标签进行检查
  • 增强了索引的标记设置

标签: go, gorm

添加新评论