GORM从v1升级到v2的破坏性更新 作者: 灯小笼 时间: 2020-11-23 分类: 开发 今年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 ## 破坏性变更 我们试图列出大的破坏性改变或那些不能被编译器捕获的改变,如果你发现任何未列出的破坏性改变,请在[这里](https://github.com/go-gorm/gorm.io)创建一个问题或提交PR。 ### 标签 GROM V2 更喜欢使用驼峰(camelCase)风格来写标签名称,而不再使用下划线风格(snake_case)。比如:auto_increment, unique_index, polymorphic_value, embedded_prefix,[点击查看](https://gorm.io/zh_CN/docs/models.html#tags)。 指定外键的标签已更改为foreignKey、引用、签出关联标记。详情请[点击查看](https://gorm.io/zh_CN/docs/associations.html#tags)。 ### 表名 `TableName` 不再允许动态表名,其结果将被缓存以备将来访问。 ```go func (User) TableName() string { return "t_user" } ``` 动态表名请使用 Scopes 方法来定义,比如: ```go 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对象时,需要确保它是在一个新的会话方法后被调用。比如: ```go 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 } ``` 点击查看[方法链](https://gorm.io/zh_CN/docs/method_chaining.html)的详情。 ### 默认值 GORM V2不会在创建后自动重新加载由数据库函数创建的默认值,点击查看[默认值](https://gorm.io/zh_CN/docs/create.html#default_values)的详情。 ### 软删除 如果模型有一个名为DeletedAt的字段,GORM V1将启用软删除;在V2中,只要字段类型被定义为GORM.DeletedAt,就可以使用该特性。例如: ```go 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模式,例如: ```go 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 来检查对应的错误。例如: ```go err := db.First(&user).Error errors.Is(err, gorm.ErrRecordNotFound) ``` ### Hook 方法 在V2中,Before/After Create/Update/Save/Find/Delete 这些方法都必须定义为 func(tx *gorm.DB) error 的类型,它像插件回调一样有统一的接口,如果定义为其他类型,将打印一个警告日志,且不会生效,详细信息请查看[钩子](https://gorm.io/zh_CN/docs/hooks.html)。 ```go 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方法检查字段是否被更改。 ```go 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的类型,点击查看[写插件](https://gorm.io/zh_CN/docs/write_plugins.html)的详情。 ### 使用 struct 更新 当使用struct更新时,GORM V2允许使用Select来选择零值字段来更新它们,例如: ```go db.Model(&user).Select("Role", "Age").Update(User{Name: "jinzhu", Role: "", Age: 0}) ``` ### 关联 GORM V1允许使用一些设置来跳过创建/更新关联,在V2中,你可以使用Select来做这个工作,例如: ```go 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`来完成这项功能。例如: ```go // preload all associations db.Preload(clause.Associations).Find(&users) ``` 此外,检查字段权限,可以用来跳过创建/更新的全局关联。 GORM V2在创建/更新记录时会使用 upsert 来保存关联,不会再保存完整的关联数据来保护你的数据不保存未完成的数据,例如: ```go 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 可以是一个全功能的模型,具有软删除、钩子和定义其他字段等特性,例如: ```go 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方法来操作连接表数据,例如: ```go 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`方法来使用事务功能。 ```go 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 }) ``` 点击查看[事务](https://gorm.io/zh_CN/docs/transactions.html)的详情。 ### 迁移工具(Migrator) - 默认情况下,Migrator将创建数据库外键 `Migrator`更加独立,许多API都被重命名,以便通过统一的API接口为每个数据库提供更好的支持 - 如果列的大小、精度和Nullable值发生了变化,AutoMigrate将改变列的类型 - 支持通过标签进行检查 - 增强了索引的标记设置 标签: go, gorm