@
GORM 官网地址 https://gorm.io/ 最新版本v1.25.1
GORM 官网文档地址 https://gorm.io/docs/
GORM 源码地址 https://github.com/go-gorm/gorm
GORM 是Golang语言中一个功能齐全的优秀的ORM 框架,对开发者友好,支持多种数据库,并提供了丰富的功能和 API,可以让开发者更加方便地进行数据库操作。
Preload
、Joins
的预加载模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner和 Valuer接口的自定义类型及其指针或别名组成,示例如:
type User struct { ID uint Name string Email *string Age uint8 Birthday *time.Time MemberNumber sql.NullString ActivatedAt sql.NullTime CreatedAt time.Time UpdatedAt time.Time }
GORM 倾向于约定优于配置 默认情况下,GORM 使用 ID
作为主键,使用结构体名的 蛇形复数
作为表名,字段名的 蛇形
作为列名,并使用 CreatedAt
、UpdatedAt
字段追踪创建、更新时间。如果遵循GORM采用的约定,则只需编写很少的配置/代码。如果约定不符合需求,GORM也允许指定配置。
GORM定义了gorm.Model,其中包括字段ID, CreatedAt, UpdatedAt, DeletedAt,可以将其嵌入到结构中以包含这些字段
可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略
type User struct { Name string `gorm:"<-:create"` // 允许读和创建 Name string `gorm:"<-:update"` // 允许读和更新 Name string `gorm:"<-"` // 允许读和写(创建和更新) Name string `gorm:"<-:false"` // 允许读,禁止写 Name string `gorm:"->"` // 只读(除非有自定义配置,否则禁止写) Name string `gorm:"->;<-:create"` // 允许读和写 Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读) Name string `gorm:"-"` // 通过 struct 读写会忽略该字段 Name string `gorm:"-:all"` // 通过 struct 读写、迁移会忽略该字段 Name string `gorm:"-:migration"` // 通过 struct 迁移会忽略该字段 }
GORM按惯例使用CreatedAt、UpdatedAt来跟踪创建/更新时间,如果定义了字段,GORM将在创建/更新时设置当前时间。要使用具有不同名称的字段,可以使用标签autoCreateTime、autoUpdateTime来配置这些字段。如果希望节省存储不采用时间格式改为采用UNIX(毫/纳)秒,可以简单地更改字段的数据类型。
type User struct { CreatedAt time.Time // 如果创建时为零,则设置为当前时间 UpdatedAt int // 在更新时设置为当前unix秒数,或者在创建时设置为零 Updated int64 `gorm:"autoUpdateTime:nano"` // 使用unix纳秒作为更新时间 Updated int64 `gorm:"autoUpdateTime:milli"`// 使用unix毫秒作为更新时间 Created int64 `gorm:"autoCreateTime"` // 使用unix seconds作为创建时间 }
例如将Go内置gorm.Model结构体嵌入到User结构体里
type User struct { gorm.Model Name string } // 这个定义等价于上面 type User struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` Name string }
但对于普通的struct字段,可以将其嵌入标签,例如:
type Author struct { Name string Email string } type Blog struct { ID int Author Author `gorm:"embedded"` Upvotes int32 } // 这个定义等价于上面Blog内嵌Author type Blog struct { ID int64 Name string Email string Upvotes int32 }
可以使用标签embeddedPrefix为嵌入字段的数据库名称添加前缀,例如:
type Blog struct { ID int Author Author `gorm:"embedded;embeddedPrefix:author_"` Upvotes int32 } // 这个定义等价于上面Blog内嵌Author type Blog struct { ID int64 AuthorName string AuthorEmail string Upvotes int32 }
Tag Name | Description |
---|---|
column | 数据库表列名 |
type | 列数据类型,例如:bool, int, uint, float, string, time, bytes,这适用于所有数据库,并且可以与其他标签一起使用,如' not null ', ' size ', ' autoIncrement '…指定的数据库数据类型,如' varbinary(8) '也支持,当使用指定的数据库数据类型时,它需要是一个完整的数据库数据类型,例如: MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT
|
serializer | 指定如何将数据序列化和反序列化到db的序列化器, e.g: serializer:json/gob/unixtime
|
size | 指定列数据大小/长度, e.g: size:256
|
primaryKey | 指定列为主键 |
unique | 将列指定为唯一约束 |
default | 指定列默认值 |
precision | 指定列精度 |
scale | 指定列的比例 |
not null | 指定列为NOT NULL |
autoIncrement | 指定列可自动递增 |
autoIncrementIncrement | 自动递增步长,控制连续列值之间的间隔 |
embedded | 嵌入字段 |
embeddedPrefix | 嵌入字段的列名前缀 |
autoCreateTime | 创建时跟踪当前时间,对于' int '字段,它将跟踪Unix秒,使用值' nano ' / ' milli '来跟踪Unix纳米/毫秒, e.g: autoCreateTime:nano
|
autoUpdateTime | 在创建/更新时跟踪当前时间,对于' int '字段,它将跟踪Unix秒,使用值' nano ' / ' milli '来跟踪Unix纳米/毫秒, e.g: autoUpdateTime:milli
|
index | 对多个字段使用相同的名称创建复合索引 |
uniqueIndex | 与' index '相同,但创建唯一的索引 |
check | 创建检查约束, eg: check:age > 13
|
<- | 设置字段的写权限,' <-:create ' create-only字段,' <-:update ' update-only字段,' <-:false '无写权限,' <- '创建和更新权限 |
-> | 设置字段的读权限,' ->:false '没有读权限 |
- | 忽略该字段,' - '无读写权限,' -:migration '无迁移权限,' -:all '无读写迁移权限 |
comment | 在迁移时为字段添加注释 |
# 引入gorm go get -u gorm.io/gorm # 引入mySQL驱动 go get -u gorm.io/driver/mysql # 引入sqlite驱动 go get -u gorm.io/driver/sqlite
import ( "gorm.io/driver/mysql" "gorm.io/gorm" ) func main() { dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) }
MySQL Driver提供了一些可以在初始化时使用的高级配置,例如:
db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name DefaultStringSize: 256, // default size for string fields DisableDatetimePrecision: true, // disable datetime precision, which not supported before MySQL 5.6 DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB SkipInitializeWithVersion: false, // auto configure based on currently MySQL version }), &gorm.Config{})
GORM使用数据库/sql维护连接池
sqlDB, err := db.DB()// SetMaxIdleConns设置空闲连接池中的最大连接数。sqlDB.SetMaxIdleConns(10)// SetMaxOpenConns设置数据库的最大打开连接数。sqlDB.SetMaxOpenConns(100)// SetConnMaxLifetime设置连接可能被重用的最大时间。sqlDB.SetConnMaxLifetime(time.Hour)
package main import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) type User struct { gorm.Model Name string Age int8 } func main() { dsn := "root:123456@tcp(mysqlserver8:3306)/test?charset=utf8&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println("data error") } db.AutoMigrate(&User{}) // 单个插入创建 db.Create(&User{Name: "zhangsan", Age: 20}) users := []User{ {Name: "lisi", Age: 25}, {Name: "wangwu", Age: 26}, } // 多个插入并演示返回值 result := db.Create(users) if result != nil { fmt.Println(result.RowsAffected) fmt.Println(result.Error) } var user User // 根据主键ID查询主键值为1的记录并返回数据 db.First(&user, 1) fmt.Println(user) }
MySQL的test数据库及对应表users信息如下:
// 指定批处理大小 var users = []User{{Name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}} db.CreateInBatches(users, 100) // 初始化GORM时使用CreateBatchSize选项,所有INSERT在创建记录和关联时都将遵循此选项 db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{ CreateBatchSize: 1000, }) db := db.Session(&gorm.Session{CreateBatchSize: 1000}) users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...} db.Create(&users)
钩子函数示例
package main import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) type User struct { gorm.Model Name string Age int8 } func (u *User) BeforeCreate(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("before create hint admin") } return } func (u *User) AfterCreate(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("after create hint admin") } return } func (u *User) BeforeSave(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("before save hint admin") } return } func (u *User) AfterSave(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("after save hint admin") } return } func main() { dsn := "root:123456@tcp(mysqlserver8:3306)/test?charset=utf8&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println("data error") } db.Create(&User{Name: "admin", Age: 40}) }
如果你想跳过Hooks方法,可以使用SkipHooks会话模式
DB.Session(&gorm.Session{SkipHooks: true}).Create(&user) DB.Session(&gorm.Session{SkipHooks: true}).Create(&users) DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)
GORM提供了First、Take、Last方法来从数据库中检索单个对象,它在查询数据库时添加了LIMIT 1条件,如果没有找到记录,它将返回错误ErrRecordNotFound。
First和Last方法将按主键顺序分别查找第一个和最后一个记录。只有当指向目标结构的指针作为参数传递给方法时,或者使用db.Model()指定模型时,它们才有效。此外,如果没有为相关模型定义主键,则模型将按第一个字段排序。
package main import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) type User struct { gorm.Model Name string Age int8 } func (u *User) BeforeCreate(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("before create hint admin") } return } func (u *User) AfterCreate(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("after create hint admin") } return } func (u *User) BeforeSave(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("before save hint admin") } return } func (u *User) AfterSave(tx *gorm.DB) (err error) { if u.Name == "admin" { fmt.Println("after save hint admin") } return } func main() { dsn := "root:123456@tcp(192.168.50.95:3306)/test?charset=utf8&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println("data error") } var user, user1, user2, user3 User // 获取一条记录,下面相当于SELECT * FROM users ORDER BY id LIMIT 1; db.First(&user) fmt.Println("user=", user) // 获取一条记录, 下面相当于SELECT * FROM users LIMIT 1; db.Take(&user1) fmt.Println("user1=", user1) // 获取一条记录, 下面相当于SELECT * FROM users ORDER BY id DESC LIMIT 1; db.Last(&user2) fmt.Println(user2) // 指定表获取数据放入map result := map[string]interface{}{} db.Table("users").Take(&result) fmt.Println("result=", result) db.First(&user3, "id = ?", 3) fmt.Println("user3=", user3) var users, users1 []User db.Find(&users, []int{1, 2, 3}) fmt.Println("users=", users) // 查询指定字段和条件 db.Select("name").Where("id > ?", 2).Find(&users1) fmt.Println("users1=", users1) }
其他详细查看官网
// limit ,SELECT * FROM users LIMIT 3; db.Limit(3).Find(&users) // OFFSET ,SELECT * FROM users OFFSET 3; db.Offset(3).Find(&users) // group by ,SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group" db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result) // distinct db.Distinct("name", "age").Order("name, age desc").Find(&results) // join ,SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{}) // scan,将结果扫描到结构体中的工作方式类似于我们使用Find的方式 var result Result db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result) // 原始SQL db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)
type User struct { ID uint Name string Age int Gender string // hundreds of fields } type APIUser struct { ID uint Name string } // 查询时自动选择“id”、“name”,SELECT `id`, `name` FROM `users` LIMIT 10 db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT * FROM `users` FOR UPDATE db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users) // SELECT * FROM `users` FOR SHARE OF `users` db.Clauses(clause.Locking{ Strength: "SHARE", Table: clause.Table{Name: clause.CurrentTable}, }).Find(&users) // SELECT * FROM `users` FOR UPDATE NOWAIT db.Clauses(clause.Locking{ Strength: "UPDATE", Options: "NOWAIT", }).Find(&users)
// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders"); db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders) // SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%") subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users") db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
// SELECT count(1) FROM users WHERE name = 'jinzhu'; (count) db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
db.First(&user) user.Name = "jinzhu 2" user.Age = 100 db.Save(&user)
db.First(&user, "age = ?", 25) db.Model(&user).Updates(User{Name: "lisinew1", Age: 44}) db.Model(&user).Updates(map[string]interface{}{"Name": "lisinew2", "Age": 35}) db.Model(&user).Update("age", 30)
如果您的模型包含一个gorm。DeletedAt字段(包含在gorm.Model中),它将自动获得软删除能力!
// GORM允许使用带有内联条件的主键删除对象 db.Delete(&user, 1) // 假如Email's ID is `10`,DELETE from emails where id = 10; db.Delete(&email) // 带附加条件删除 db.Where("name = ?", "jinzhu").Delete(&email) // 永久删除 db.Unscoped().Delete(&order)
type Result struct { ID int Name string Age int } var result Result db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result) var age int db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age) var users []User db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)
// 执行删除表数据 db.Exec("DROP TABLE users") // 执行带有表达式的更新 db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")
// Session Configuration type Session struct { DryRun bool PrepareStmt bool NewDB bool Initialized bool SkipHooks bool SkipDefaultTransaction bool DisableNestedTransaction bool AllowGlobalUpdate bool FullSaveAssociations bool QueryFields bool Context context.Context Logger logger.Interface NowFunc func() time.Time CreateBatchSize int } // session mode stmt := db.Session(&Session{DryRun: true}).First(&user, 1).Statement println(stmt.SQL.String()) println(stmt.Vars)
// row row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row() row.Scan(&name, &age) row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row() row.Scan(&name, &age, &email) // rows rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows() defer rows.Close() for rows.Next() { rows.Scan(&name, &age, &email) // do something } rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows() defer rows.Close() for rows.Next() { rows.Scan(&name, &age, &email) // do something }
GORM在事务内部执行写(创建/更新/删除)操作以确保数据一致性,如果不需要,可以在初始化时禁用它,之后将获得大约30%以上的性能提升。流程如下:
func CreateAnimals(db *gorm.DB) error { // 注意,在事务中使用tx作为数据库句柄 tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() if err := tx.Error; err != nil { return err } if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { tx.Rollback() return err } if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { tx.Rollback() return err } return tx.Commit().Error }
// 可以使用标记primaryKey将其他字段设置为主键 type Animal struct { ID int64 UUID string `gorm:"primaryKey"` Name string Age int64 }
// 转换表名为my_users func (User) TableName() string { return "my_users" }
type Animal struct { AnimalID int64 `gorm:"column:beast_id"` // 将列名设置为 `beast_id` Birthday time.Time `gorm:"column:day_of_the_beast"` // 将列名设置为 `day_of_the_beast` Age int64 `gorm:"column:age_of_the_beast"` // 将列名设置为 `age_of_the_beast` }
Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充,带来的额外开销极小。对开发者友好、透明,使用上与普通 SQL、Gorm 查询无差别,只需要额外注意一下分表键条件。 为您提供高性能的数据库访问。
功能特点
配置分片中间件,注册想要分片的表
import ( "fmt" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/sharding" ) dsn := "postgres://localhost:5432/sharding-db?sslmode=disable" db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn})) db.Use(sharding.Register(sharding.Config{ ShardingKey: "user_id", NumberOfShards: 64, PrimaryKeyGenerator: sharding.PKSnowflake, }, "orders").Register(sharding.Config{ ShardingKey: "user_id", NumberOfShards: 256, PrimaryKeyGenerator: sharding.PKSnowflake, // 对于show up give notifications, audit_logs表使用相同的分片规则。 }, Notification{}, AuditLog{}))
序列化器是一个可扩展的接口,允许自定义如何使用databasae序列化和反序列化数据。
package main import ( "database/sql/driver" "encoding/json" "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) type Profile struct { Email string `json:"email"` Mobile string `json:"mobile"` } type User struct { gorm.Model Name string `json:"name"` Age int8 `json:"age"` Profile Profile `json:"profile" gorm:"type:json;comment:'个人信息'"` } // 转换表名 func (User) TableName() string { return "new_users" } // Value 存储数据的时候转换为字符串 func (t Profile) Value() (driver.Value, error) { return json.Marshal(t) } // Scan 读取数据的时候转换为json func (t *Profile) Scan(value interface{}) error { return json.Unmarshal(value.([]byte), &t) } func main() { dsn := "root:123456@tcp(mysqlserver8:3306)/test?charset=utf8&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println("data error") } db.AutoMigrate(&User{}) user := User{ Name: "刘海", Age: 23, Profile: Profile{ Email: "test1@qq.com", Mobile: "18822334455", }, } db.Create(&user) var user1 User db.Debug().Where("profile->'$.mobile'=(?)", "18822334455").First(&user1) fmt.Println(user1) }
查看mysql已有新创建的new_users数据库和对应的数据