ORM(Object Relational Mapping),中文名为对象关系映射。
使用 ORM 组件,可以让开发者通过操作对象的方式完成对数据库的操作(读写),避免手动书写 SQL 和完成数据到对象的转换,让我们更方便地操作数据库。
理论上 ORM 可以让我们脱离 SQL,但实际上还是需要懂 SQL 才能更好地使用 ORM。
GORM 是一个流行的 Golang ORM 库。
类似于 Java 生态里大家听到过的 Mybatis、Hibernate、SpringData 等。
GORM 由国人开发,中文文档齐全,对开发者友好,支持主流关系型数据库。
GORM 功能丰富齐全:
GORM 最新源码地址:go-gorm/gorm。
GORM V1 版本地址:jinzhu/gorm。
GORM 中文文档地址:这里。
本文将讲解 GORM 中常用的功能,帮助你快速上手。
当然除了 GORM,你还有其他选择,比如 facebook-ent、sqlx 和 sqlc 等。
基于 Go Module 开发,import 最新包然后 go get 即可。
go get -u gorm.io/gorm
// 不同 DB 对应的驱动
go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/mysql
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlserver
驱动包按照自己实际使用的 DB 选择即可。
本文将以 MySQL 为例,讲解 GORM 的使用。
以 MySQL 为例,建立数据库连接。
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// MySQLConn GORM MySQL 连接。
var MySQLConn *gorm.DB
// Init gorm mysql connnection.
// 依赖服务配置初始化完成。
func InitMySQLConn() error {
// data source name.
dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", Conf.Mysql.User, Conf.Mysql.Passwd, Conf.Mysql.IP, Conf.Mysql.Port, Conf.Mysql.Dbname)
var err error
MySQLConn, err = gorm.Open(mysql.Open(dsn))
return err
}
填入 DB 对应的正确的用户名、密码、地址、端口、数据库名称等信息后,便可建立对应数据源的连接。相关配置一般在服务启动时,事先从配置文件中加载。
在进行增查改删(CRUD)之前,需要先创建一个数据表。
GORM 中一个 struct 对应一张数据库表,对应的 struct 被称为模型。
假如我们要创建一张商品(goods)表,那么模型可定义为:
// Good 商品。
type Good struct {
gorm.Model
Name string `gorm:"type:string;size:256;not null"`
Price int `gorm:"type:int;not null"`
}
其中 gorm.Model 时 GORM 预先定义的一些基础字段,我们可以嵌入直接拿来用。
// Model a basic GoLang struct which includes the following fields: ID, CreatedAt, UpdatedAt, DeletedAt
// It may be embedded into your model or you may build your own model without it
//
// type User struct {
// gorm.Model
// }
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
字段后的 tag 用来定义字段在 DB 中的相关属性,如 primarykey 表示主键,index 表示索引,type 表示字段类型。
除此以外,还有更加丰富的标签定义参见官方文档:字段标签。
一般在服务启动时创建数据表,如建立 DB 连接后只执行一次来完成数据表的创建。
db.AutoMigrate(&User{})
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 创建表时添加后缀。
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
比如创建我们上面的商品表。
// 自动创建表,如果表已经存在不会有任何动作。
err := MySQLConn.AutoMigrate(&Good{})
创建好后的数据表名为 struct 名称命名方式是 snake_case(下划线命名法)的复数形式,字段名为 struct 字段的 sanke_case 形式。
如果想更改表名,可以通过在模型结构体上添加 TableName() 方法来自定义表名称。
func (Good) TableName() string {
return "tb_good"
}
如果想更改表字段名,可在模型结构体上通过 gorm tag 的 column 标签指定。
type User struct {
gorm.Model
Name string `gorm:"column:user_name"`
}
通过如下方式选择要操作的表。
DB.Model(&ModelName{})
DB.Table("table_name")
DB.Table("table_name alias_name")
DB.Table("table_name AS alias_name")
使用 Table() 方法为指定要操作的表时,如果表名太长,可以使用 AS(可省略)设置一个短别名来引用表。
// 插入商品。
task := &Good{
Name: name,
Price: price,
}
err := MySQLConn.Create(task).Error
// 或
// err := MySQLConn.Save(task).Error
主键 ID 会自增,此外 GORM 还会自动维护 created_at、updated_ad 和 deleted_at 三个字段。
其中 Save 方法在保存记录时,如果主键 ID 非空则执行更新操作,零值也会更新到 DB。如果主键 ID 为空,则执行插入操作。
我们还可以使用 Create() 创建多项记录。
users := []*User{
User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(users) // pass a slice to insert multiple row
result.Error // returns error
result.RowsAffected // returns inserted records count
同样地,Save() 也可以创建多项记录。
result := db.Save(users) // pass a slice to insert multiple row when value does not contain primary key
result.Error // returns error
result.RowsAffected // returns inserted records count
GORM 提供了 Upsert 的能力,记录存在(根据主键判断)则更新,不存在则增加。
func UpsertYourModel(m *YourModel) error {
return Db.Save(m).Error
}
或者在键冲突时决定要更新的列。
import "gorm.io/gorm/clause"
// Do nothing on conflict
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// Update columns to new value on `id` conflict
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// Update all columns to new value on conflict except primary keys and those columns having default values from sql func
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// Update all columns to new value on composite unique index conflict.
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "student_no"}, {Name: "course_no"}},
DoUpdates: clause.AssignmentColumns([]string{"status", "updated_at"}),
}).Create(&courseSelection)
db.First(&user, 10)
// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")
// SELECT * FROM users WHERE id = 10;
如果查不到,将报 gorm.ErrRecordNotFound 错误。
当目标对象有一个主键值时,将使用主键构建查询条件,例如:
var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;
var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;
比如按照多个主键查询。
db.Find(&goods, []int{1,2,3})
或者通过内联条件。查询条件可以类似于 Where 的方式内联到 First 和 Find 等方法中。
db.Find(&goods, "id IN ?", []int{1,2,3})
或者通过 Where 指定 IN 条件。
db.Where("id IN ?", []int{1,2,3}).Find(&goods)
再如按照其他字段进行 and 查询。
多次调用 Where 方法可指定多个条件,条件关系为 AND。
// getGoodsByInfo 根据商品信息分页拉取。
func getGoodsByInfo(name string, price int, lastID uint) ([]Good, error) {
db := internal.MySQLConn
if name != "" {
db.Where("name = ?", name)
}
db.Where("price >= ?", price)
// 按照每页大小 50 拉取商品。
db.Where("id > ?", lastID).Order("id ASC").Limit(50)
var goods []Good
result := db.Find(&goods)
return goods, result.Error
}
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);
使用 Group 条件可以更轻松地编写复杂 SQL。
db.Where(
db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{})
// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")
db.Where(&Good{Name: "衣服", Price: 10}).Find(&goods)
// SELECT * FROM goods WHERE name = "衣服" AND price = 10;
db.Where(&Good{Name: "衣服", Price: 10}, "Name").Find(&goods)
// SELECT * FROM goods WHERE name = "衣服";
// getGoodNumber 获取符合条件的商品数量。
func getGoodNumber(price int) (int, error) {
var c int
err := internal.MySQLConn.Model(&Good{}).Where("price >= ?", price).Count(&c).Error
return c, err
}
在 GORM 中,可以使用 Count 方法来判断一个查询是否返回了记录。
func GoodExist(name string) (bool, error) {
var c int64
err := MySQLConn.Model(&Good{}).Where("name = ?", name).Count(&count).Error
return count > 0, err
}
当然,你也可以使用 First 方法,并结合 ErrRecordNotFound 错误来判断记录是否存在。
func GoodExist(name string) (bool, error) {
var good Good
err := MySQLConn.Where("name = ?", name).First(&good).Error
// 不存在。
if err == gorm.ErrRecordNotFound {
return false, nil
}
// 查询发生错误。
if err != nil {
return false, err
}
// 存在。
return true, nil
}
使用 Count 方法可以在不加载实际记录的情况下检查是否存在记录。这种方法比使用 First 方法更高效,尤其是在需要检查大量记录是否存在的情况下。因为 Count 方法只计算匹配条件的记录数,而不需要加载和返回记录的实际内容。但是,它可能会有一些微小的开销,因为它需要向数据库发送一个额外的 COUNT(*) 查询来计算记录数。
所以,如果你只是需要检查记录是否存在,推荐使用 Count 方法。
使用 Pluck 方法可以查询指定字段的所有值。如下面的代码查询 users 表中所有用户的姓名。
var names []string
db.Model(&User{}).Pluck("name", &names)
如果是一条记录的某个字段,可以使用单个变量而非切片接收查询结果。
var name string
db.Model(&User{}).Where("id = ?", 1).Pluck("name", &name)
如果您想要查询多列,您应该使用 Select 和 Scan 或 Find。
// 超过一列的查询,应该使用 Scan 或 Find
db.Select("name", "age").Scan(&users)
db.Select("name", "age").Find(&users)
Scan 和 Find 的作用是类似的,关于二者的区别可参考 GORM Issue #4218。下面是大佬 jinzhu 的回答。
Scan, Find using different callbacks, Scan won’t call hooks methods.
从模型中选择不同的值。
db.Distinct("name", "age").Order("name, age desc").Find(&results)
Distinct 也可与 Pluck 和 Count 配合使用。
可以使用 Limit & Offset 实现分页查询。
Limit 指定要检索的最大记录数, Offset 指定在开始返回记录之前要跳过的记录数。
db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;
// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)
db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;
db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;
// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
利用 Limit & Offset 实现分页查询时,如果需要同时查询符合条件的记录总数,则需要先查询记录数,再查询记录。
db.Model(&User{}).Where("age >= ?", 18)
// 先查询记录数。
var count int64
db.Count(&count)
// 再查询记录。
db.Limit(10).Offset(10).Find(&users)
// 或 Count 在前。
var count int64
db.Count(&count).Limit(10).Offset(10).Find(&users)
从数据库检索记录时指定顺序。
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;
db.Clauses(clause.OrderBy{
Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
}).Find(&User{})
// SELECT * FROM users ORDER BY FIELD(id,1,2,3)
GORM 允许您在 Table 方法中通过 FROM 子句使用子查询。
db.Table("(?) AS u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) AS u WHERE `age` = 18
subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) AS u, (?) AS p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p
当然也可以用于 JOIN。
subQuery := db.Model(&User{}).Select("id", "name").Where("age = ?", 18)
db.Model(&Pet{}).Select("pets.name", "users.name").Joins("LEFT JOIN (?) users ON pets.owner_id = users.id", subQuery)
还可以用于 IN 条件。
subQuery := db.Model(&User{}).Select("id").Where("age = ?", 18)
db.Model(&Pet{}).Select("name").Where("owner_id IN (?)", subQuery)
指定联接条件。
type result struct {
Name string
Email string
}
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
// SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.id
rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
...
}
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)
// multiple joins with parameter
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)
还有很多查询方式,比如按照 map 指定查询字段以及 OR 和 NOT 条件等,具体请参考官方文档 GORM 查询。
使用 Save 方法更新所有字段,即使是零值也会更新。
// 先根据 ID 查询。
db.First(&good, 1)
// 再修改值。
good.Name = "小米"
// 最后写回。
db.Save(&user)
注意,当使用 Model 方法且其值具有主键时,主键将用于构建条件。
// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// 注意:user 的 ID 是 111。
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据条件和 model 的值进行更新
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;
Updates 方法支持 struct
和 map[string]interface{}
参数。当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段。
// 注意:user 的 ID 是 111。
// 根据 `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, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
删除一条记录时,删除对象需要指定主键,否则会触发批量 Delete,例如:
// Email 的 ID 是 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 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字,也可以使用字符串。
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;
db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;
db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);
指定的值没有主键值,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%";
要有效删除大量记录,请将带有主键的切片传递给 Delete 方法。
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// DELETE FROM users WHERE id IN (1,2,3);
db.Delete(&users, "name LIKE ?", "%jinzhu%")
// DELETE FROM users WHERE name LIKE "%jinzhu%" AND id IN (1,2,3);
如果您的模型包含了一个 gorm.DeletedAt 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!
如果您不想引入 gorm.Model,您也可以这样启用软删除特性:
type User struct {
ID int
Deleted gorm.DeletedAt
Name string
}
拥有软删除能力的模型调用 Delete 时,记录不会被数据库。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。
使用 Unscoped 方法查找被软删除的数据。
db.Unscoped().Where("user_name = gry").Find(&users)
要想物理删除,使用 Unscoped 方法永久删除数据。
user.ID = 14
db.Unscoped().Delete(&user)
GORM 允许使用 Preload 通过多个 SQL 中来直接加载关系。
type User struct {
gorm.Model
Username string
Orders []Order
}
type Order struct {
gorm.Model
UserID uint
Price float64
}
// 查找 user 时预加载相关 Order
db.Preload("Orders").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4);
db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // has many
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // has one
// SELECT * FROM roles WHERE id IN (4,5,6); // belongs to
其中一个用户可以拥有多个订单(has many),但是一个用户只能拥有一份用户资料(has one),并每个用户属于(belong to)某一个用户角色。
预加载时,需要在模型的定义中体现这种关系,比如上面示例中 User 定义中有一个订单的切片,预加载时指定切片名称。
GORM 允许预加载时使用条件,其工作原理类似于内联条件。
// Preload Orders with conditions
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled');
db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users WHERE state = 'active';
// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');
您可以通过传入func(db * gorm.DB)* gorm.DB来自定义预加载SQL,例如控制预加载内容的排序规则。
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("orders.amount DESC")
}).Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC;
预加载还有很多方式,比如 Joins 预加载、预加载全部和嵌套预加载等,详情请见官方文档。
本文简单介绍了 ORM、GORM、以及 GORM 连接数据库,创建数据表和 CRUD 的简单操作,帮忙新手快速上手。
更多用法,请参见官方文档 GORM 指南,这里有你想要的一切。
如果您喜欢这篇文章,欢迎关注我的微信公众号“恋喵大鲤鱼”了解最新精彩内容。
GORM 指南| GORM - GORM
GORM 极速入门- 卢振千的博客
19-Gorm入门到精通- 刘清政 - 博客园
Go组件学习——gorm四步带你搞定DB增删改查 - 掘金