ORM,即 Object Relational Mapping ,全称“对象关系映射”。
程序中当需要对数据库进行操作时,势必需要通过连接数据库、调用sql语句,执行sql语句等操作,ORM将数据库中的表、字段、行于我们面向对象编程的类及其方法、属性等一一对应,即将该部分操作封装起来,使得不需要很懂得sql语句的情况下,也能完成对数据库数据的操作。
GORM是go开发时操作DB的一大利器,其官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server。此文主要就gorm操作mysql作简单介绍。
GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt
GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。如果您定义了他们,GORM 在创建/更新时会自动填充 当前时间 至这些字段
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
可以将其嵌入到我们的自定义结构体中,以包含这几个字段
type User struct {
gorm.Model
Name string
}
// 上面结构体等效于下面
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
Name string
}
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
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{})
}
注意:
想要 Gorm 正确的处理 time.Time ,您需要带上 parseTime 参数。
想要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4。
MySQL驱动还提供了一些高级配置可供初始化过程中使用,如:
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据版本自动配置
}), &gorm.Config{})
连接池
gorm使用 database/sql 来维护连接池
sqlDB, err := db.DB()
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIgleConns(10)
// SetMaxOpenConns 设置打开数据库连接的最大数量
sqlDB.SetMaxOpenConns(100)
// SetConnMaxLifetime 设置了连接可复用的最大时间
sqlDB.SetConnMaxLifetime(time.Hour)
须知:下面是后续的演示介绍中用到的基本模型定义
type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
新增一条记录:
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
// 执行成功后,可通过下面语句获取一些想要的信息
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
选用指定的字段来新增:
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")
排除指定的字段来新增:
db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")
GORM 允许 BeforeSave, BeforeCreate, AfterSave, AfterCreate 等钩子,创建记录时会调用这些方法, 详情请参阅 钩子
批量插入
将切片数据传递给 Create 方法,GORM 将生成一个单一的 SQL 语句来插入所有数据,并回填主键的值:
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
DB.Create(&users)
for _, user := range users {
user.ID // 1,2,3
}
默认值
可通过tag标签中 default 为字段定义默认值:
type User struct {
ID int64
Name string `gorm:"default:'galeone'"`
Age int64 `gorm:"default:18"`
uuid.UUID UUID `gorm:"type:uuid;default:gen_random_uuid()"` // db 函数
}
删除一条记录:
// 删除一条已有的记录(email 的主键值为 10)
db.Delete(&email)
// DELETE from emails where id=10;
// 带上其它条件
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE FROM emails WHERE id=10 AND name = 'jinzhu'
对于删除操作,GORM 支持 BeforeDelete
、AfterDelete
钩子,在删除记录时会调用这些方法。
func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
if u.Role == "admin" {
return errors.New("admin user not allowed to delete")
}
return
}
批量删除
如果没有指定带有主键值的记录,GORM 将执行批量删除,删除所有匹配的记录:
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
// DELETE from emails where email LIKE "%jinzhu%";
db.Delete(Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";
全局删除
如果在没有任何条件的情况下执行批量删除,GORM 不会执行该操作,并返回ErrMissingWhereClause错误;此时可以使用 ‘=’ 之类的条件来强制全局删除
db.Delete(&User{}).Error // gorm.ErrMissingWhereClause
db.Where("1 = 1").Delete(&User{})
// DELETE `users` WHERE 1=1
Save 会保存所有字段,即使该字段是零值
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
使用Update、Updates更新选定的字段,Update用于单列,Updates用于多列:
// 更新单个字段
// the user of `Model(&user)` needs to have primary key value, it is `111` in this example
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据条件更新单个字段
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
// 通过 `struct` 更新多个字段,不会更新零值字段
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// 通过 `map` 更新多个字段,零值字段也会更新
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
注意:
当通过 struct 更新时,GORM 只会更新非零字段。 如果想确保指定字段被更新,你应该使用 Select 更新选定字段,或使用 map 来完成更新操作。
// Select 与 Map
// the user of `Model(&user)` needs to have primary key value, it is `111` in this example
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// UPDATE users SET name='hello' WHERE id=111;
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// UPDATE users SET age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// Select 与 Struct
DB.Model(&result).Select("Name", "Age").Updates(User{Name: "new_name"})
// UPDATE users SET name='new_name', age=0 WHERE id=111;
批量更新
如果尚未通过 Model 指定记录的主键,则 GORM 会执行批量更新
// 通过 struct 只能更新非零值,若要更新零值,可以使用 map[string]interface{}
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin;
db.Table("users").Where("id IN (?)", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
全局更新
如果在没有任何条件的情况下执行批量更新,GORM 不会执行该操作,并返回ErrMissingWhereClause错误;此时可以使用 ‘=’ 之类的条件来强制全局更新
db.Model(&User{}).Update("name", "jinzhu").Error // gorm.ErrMissingWhereClause
db.Model(&User{}).Where("1 = 1").Update("name", "jinzhu")
// UPDATE users SET `name` = "jinzhu" WHERE 1=1
获取更新的记录数
// 通过 `RowsAffected` 得到更新的记录数
result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin;
result.RowsAffected // 更新的记录数
result.Error // 更新的错误
不使用钩子和时间追踪
如果想在更新时跳过钩子方法和自动更新时间追踪,可以使用 UpdateColumn、UpdateColumns
// 更新单列,用法类似于 `Update`
db.Model(&user).UpdateColumn("name", "hello")
// UPDATE users SET name='hello' WHERE id = 111;
// 更新多列,用法类似于 `Updates`
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE id = 111;
// 配合 Select 更新多列,用法类似于 `Updates`
db.Model(&user).Select("name", "age").UpdateColumns(User{Name: "hello"})
// UPDATE users SET name='hello', age=0 WHERE id = 111;
查询单条数据
GORM 提供 First, Take, Last 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件。当没有找到记录时,它会返回错误 ErrRecordNotFound。
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
查询多条记录
// 获取全部记录
result := db.Find(&users)
// SELECT * FROM users;
result.RowsAffected // 返回找到的记录数,相当于 `len(users)`
result.Error // returns error
条件查询
// 获取第一条匹配的记录
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
// 获取全部匹配的记录
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name in ('jinzhu','jinzhu 2');
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// 主键切片条件
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
注意
当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果字段值为 0、‘’、false 或其他零值,该字段不会被用于构建查询条件:
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";
此时可以使用 map 来构建查询条件,强制指定零值字段也作为条件去参与查询:
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
内联条件查询
// 根据主键获取记录(仅适用于整型主键)
db.First(&user, 23)
// SELECT * FROM users WHERE id = 23;
// 根据主键获取记录,如果是非整型主键
db.First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';
// 单个查询条件
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";
// 多个查询条件
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;
// Struct
db.Find(&users, User{Age: 20})
// SELECT * FROM users WHERE age = 20;
// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;
Not条件查询
// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;
// 不在主键切片中的记录
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
Or条件查询
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
查询指定字段
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&users)
// SELECT name, age FROM users;
rows, err := db.Table("users").Select("COALESCE(age,?)", 42).Rows()
// SELECT COALESCE(age,'42') FROM users;
defer rows.Close()
for rows.Next() {
var user User
// ScanRows 将一行记录扫描至 user
db.ScanRows(rows, &user)
// 业务逻辑...
}
Order设置排序规则查询
db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
Limit 与Offset
Limit 指定获取记录的最大数量
Offset 指定在开始返回记录之前要跳过的记录数量
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;
db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;
db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;
查询拓展tips
gorm允许通过 Select 方法选择特定的字段,如果在程序中经常使用此功能,则可以考虑定义一个较小的结构体,以实现自动选择特定的字段。
type User struct {
ID uint
Name string
Age int
Gender string
// 假设后面还有几百个字段...
}
type APIUser struct {
ID uint
Name string
}
// 查询时会自动选择 `id`, `name` 字段
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10
查询结果集至map
GORM 允许扫描结果至 map[string]interface{} 或 []map[string]interface{},此时别忘了指定 Model 或 Table
var result map[string]interface{}
DB.Model(&User{}).First(&result, "id = ?", 1)
var results []map[string]interface{}
DB.Table("users").Find(&results)
查询钩子
对于查询操作,GORM 支持 AfterFind 钩子,查询记录后会调用它。
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.Role == "" {
u.Role = "user"
}
return
}
原生sql查询
type Result struct {
ID int
Name string
Age int
}
var result Result
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
var age int
DB.Raw("select sum(age) from users where role = ?", "admin").Scan(&age)
执行原生sql
db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at=? WHERE id IN ?", time.Now(), []int64{1,2,3})
Row & Rows
获取 *sql.Row 结果
// 使用 GORM API 构建 SQL
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()
row.Scan(&name, &age)
// 使用原生 SQL
row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row()
row.Scan(&name, &age, &email)
获取 *sql.Rows 结果
// 使用 GORM API 构建 SQL
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// 业务逻辑...
}
// 使用原生 SQL
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)
// 业务逻辑...
}
事务(自动)
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
// or
db.Transaction(func(tx *gorm.DB) error {
sql := "this is DML sql"
if err := tx.Exec(sql).Error; nil != err {
return err
}
sql = "this is DML sql"
if err := tx.Exec(sql).Error; nil != err {
return err
}
return nil
})
事物(手动)
func TransactionSample1(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
}
// 类举xorm的事物
func TransactionSample2(en *xorm.Engine) (err error) {
sess := en.NewSession()
err = sess.Begin()
defer func() {
if nil != err {
sess.Rollback()
} else {
sess.Commit()
}
sess.Close()
}()
if nil != err {
return err
}
sql := "this is DML sql"
res, err := sess.Exec(sql)
if nil != err {
return err
}
sql2 := "this is DML sql"
_, err = sess.Exec(sql2)
if nil != err {
return err
}
return nil
}
Gorm 有一个 默认 logger 实现,默认情况下,它会打印慢 SQL 和错误。
Logger 接受的选项不多,您可以在初始化时自定义它。
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Silent, // Log level
Colorful: false, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(
mysql.New(
mysql.Config{
...
},
),
&gorm.Config{
Logger: newLogger,
},
)
概述 · GORM V2 中文文档 · 看云
本文简单介绍了golang中的数据库操作利器GORM,通过一系列代码实例生动的演示了如何使用gorm引擎去操作底层数据,相信通过本文的学习能很快的上手gorm,提升开发中的编码效率。