Go gorm
这篇文章主要先简单总结一下gorm的crud,
什么是orm
在学习gorm之前,先了解一下什么是orm
在后端开发上,通常都要与资料库做操作(新增、修改、删除、查找),后端会撰写 SQL 语句,并且透过一些工具或套件(例如:pymysql)向 SQL 资料库来做沟通。而撰写原生 SQL 的缺点为:
- 不可维护性:代码难阅读且不易维护。
- 不可重用性:通常不容易被重用,每个 SQL 语句都需要独立编写并维护。
- 容易犯错:容易犯错,容易缺少引号、忘记加条件等。
- 容易被攻击:容易遭到 SQL Injection 攻击。
- 资料库迁移问题:针对 MySQL 开发的 SQL 语句就没办法直接应用到 Oracle 上的资料库。
为了解决上述问题,ORM 是再往上进行一层封装,而无需去编写原生的 SQL 语句,取而代之的是基于物件导向的思想去编写 Class、Object、Method 等。而 ORM 会再生成 SQL 语句再往下去执行
说大白话就是用类似orm.create来替代原有相对复杂的sql语句去对数据库进行操作。
ORM 优缺点
优点
提高开发效率
缺点
牺牲性能
牺牲灵活性
安装
接下来回到这篇文章的主体gorm上,这里先进行安装
go get -u gorm.io/gorm | |
go get -u gorm.io/driver/sqlite |
演示
package main | |
| |
import ( | |
"gorm.io/gorm" | |
"gorm.io/driver/sqlite" | |
) | |
| |
type Product struct { | |
gorm.Model | |
Code string | |
Price uint | |
} | |
| |
func main() { | |
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{}) | |
if err != nil { | |
panic("failed to connect database") | |
} | |
| |
// 迁移 schema | |
db.AutoMigrate(&Product{}) | |
| |
// Create | |
db.Create(&Product{Code: "D42", Price: 100}) | |
| |
// Read | |
var product Product | |
db.First(&product, 1) // 根据整型主键查找 | |
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录 | |
| |
// Update - 将 product 的 price 更新为 200 | |
db.Model(&product).Update("Price", 200) | |
// Update - 更新多个字段 | |
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段 | |
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) | |
| |
// Delete - 删除 product | |
db.Delete(&product, 1) | |
} |
模型声明
user模型的示例
type User struct { | |
ID uint // Standard field for the primary key | |
Name string // 一个常规字符串字段 | |
Email *string // 一个指向字符串的指针, allowing for null values | |
Age uint8 // 一个未签名的8位整数 | |
Birthday *time.Time // A pointer to time.Time, can be null | |
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings | |
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields | |
CreatedAt time.Time // 创建时间(由GORM自动管理) | |
UpdatedAt time.Time // 最后一次更新时间(由GORM自动管理) | |
} |
约定
主键:GORM 使用一个名为ID
的字段作为每个模型的默认主键。
表名:默认情况下,GORM 将结构体名称转换为 snake_case
并为表名加上复数形式。 例如,一个 User
结构体在数据库中的表名变为 users
。
列名:GORM 自动将结构体字段名称转换为 snake_case
作为数据库中的列名。
时间戳字段:GORM使用字段 CreatedAt
和 UpdatedAt
来自动跟踪记录的创建和更新时间。
连接数据库
目前GORM 官方支持的数据库类型有:MySQL, PostgreSQL, SQLite, SQL Server 和 TiDB,基本把常用数据库都涵盖了。
mysql
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{}) | |
} |
PostgreSQL
import ( | |
"gorm.io/driver/postgres" | |
"gorm.io/gorm" | |
) | |
| |
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai" | |
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) |
SQLite
import ( | |
"gorm.io/driver/sqlite" // Sqlite driver based on CGO | |
// "github.com/glebarez/sqlite" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details | |
"gorm.io/gorm" | |
) | |
| |
// github.com/mattn/go-sqlite3 | |
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{}) |
SQL Server
import ( | |
"gorm.io/driver/sqlserver" | |
"gorm.io/gorm" | |
) | |
| |
// github.com/denisenkom/go-mssqldb | |
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" | |
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{}) |
TiDB
TiDB 兼容 MySQL 协议。 因此你可以按照 MySQL一节来创建与 TiDB 的连接。
import ( | |
"fmt" | |
"gorm.io/driver/mysql" | |
"gorm.io/gorm" | |
) | |
| |
type Product struct { | |
ID uint `gorm:"primaryKey;default:auto_random()"` | |
Code string | |
Price uint | |
} | |
| |
func main() { | |
db, err := gorm.Open(mysql.Open("root:@tcp(127.0.0.1:4000)/test"), &gorm.Config{}) | |
if err != nil { | |
panic("failed to connect database") | |
} | |
| |
db.AutoMigrate(&Product{}) | |
| |
insertProduct := &Product{Code: "D42", Price: 100} | |
| |
db.Create(insertProduct) | |
fmt.Printf("insert ID: %d, Code: %s, Price: %d\n", | |
insertProduct.ID, insertProduct.Code, insertProduct.Price) | |
| |
readProduct := &Product{} | |
db.First(&readProduct, "code = ?", "D42") // find product with code D42 | |
| |
fmt.Printf("read ID: %d, Code: %s, Price: %d\n", | |
readProduct.ID, readProduct.Code, readProduct.Price) | |
} |
创建
创建记录
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()} | |
result := db.Create(&user) // 通过数据的指针来创建 | |
user.ID // 返回插入数据的主键 | |
result.Error // 返回 error | |
result.RowsAffected // 返回插入记录的条数 |
创建多项纪录
users := []*User{ | |
{Name: "Jinzhu", Age: 18, Birthday: time.Now()}, | |
{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 |
你无法向 ‘create’ 传递结构体,所以你应该传入数据的指针.
用指定的字段创建记录
创建记录并为指定字段赋值
db.Select("Name", "Age", "CreatedAt").Create(&user) | |
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18 |
创建记录并忽略传递给 ‘Omit’ 的字段值
db.Omit("Name", "Age", "CreatedAt").Create(&user) | |
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 |
批量插入
要高效地插入大量记录,可以将切片传递给Create
方法。 GORM 将生成一条 SQL 来插入所有数据,以返回所有主键值,并触发 Hook
方法。 当这些记录可以被分割成多个批次时,GORM会开启一个事务</0>来处理它们。
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}} | |
db.Create(&users) | |
for _, user := range users { | |
user.ID // 1,2,3 | |
} |
查询
查询单个对象
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 or nil | |
// 检查 ErrRecordNotFound 错误 | |
errors.Is(result.Error, gorm.ErrRecordNotFound) |
如果你想避免ErrRecordNotFound
错误,你可以使用Find
,比如db.Limit(1).Find(&user)
,Find
方法可以接受struct和slice的数据。
对单个对象使用Find
而不带limit,db.Find(&user)
将会查询整个表并且只返回第一个对象,只是性能不高并且不确定的。
First
and Last
方法会按主键排序找到第一条记录和最后一条记录 (分别)。 只有在目标 struct 是指针或者通过 db.Model()
指定 model 时,该方法才有效。 此外,如果相关 model 没有定义主键,那么将按 model 的第一个字段进行排序。 例如:
var user User | |
var users []User | |
// works because destination struct is passed in | |
db.First(&user) | |
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1 | |
// works because model is specified using `db.Model()` | |
result := map[string]interface{}{} | |
db.Model(&User{}).First(&result) | |
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1 | |
// doesn't work | |
result := map[string]interface{}{} | |
db.Table("users").First(&result) | |
// works with Take | |
result := map[string]interface{}{} | |
db.Table("users").Take(&result) | |
// no primary key defined, results will be ordered by first field (i.e., `Code`) | |
type Language struct { | |
Code string | |
Name string | |
} | |
db.First(&Language{}) | |
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1 |
根据主键检索
如果主键是数字类型,您可以使用 内联条件 来检索对象。 当使用字符串时,需要额外的注意来避免SQL注入;查看 Security 部分来了解详情。
db.First(&user, 10) | |
// SELECT * FROM users WHERE id = 10; | |
db.First(&user, "10") | |
// SELECT * FROM users WHERE id = 10; | |
db.Find(&users, []int{1,2,3}) | |
// SELECT * FROM users WHERE id IN (1,2,3); |
如果主键是字符串(例如像uuid),查询将被写成如下:
db.First(&user, "id = ?", "1b74413f-f3b8-409f-ac47-e8c062e3472a") | |
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a"; |
当目标对象有一个主键值时,将使用主键构建查询条件,例如:
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; |
NOTE: 如果您使用 gorm 的特定字段类型(例如 gorm.DeletedAt
),它将运行不同的查询来检索对象。
type User struct { | |
ID string `gorm:"primarykey;size:16"` | |
Name string `gorm:"size:24"` | |
DeletedAt gorm.DeletedAt `gorm:"index"` | |
} | |
var user = User{ID: 15} | |
db.First(&user) | |
// SELECT * FROM `users` WHERE `users`.`id` = '15' AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT 1 |
检索全部对象
// Get all records | |
result := db.Find(&users) | |
// SELECT * FROM users; | |
result.RowsAffected // returns found records count, equals `len(users)` | |
result.Error // returns error |
String 条件
// Get first matched record | |
db.Where("name = ?", "jinzhu").First(&user) | |
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1; | |
// Get all matched records | |
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'; |
如果对象设置了主键,条件查询将不会覆盖主键的值,而是用 And 连接条件。 例如: var user = User{ID: 10} db.Where("id = ?", 20).First(&user) // SELECT * FROM users WHERE id = 10 and id = 20 ORDER BY id ASC LIMIT 1 这个查询将会给出record not found
错误 所以,在你想要使用例如user
这样的变量从数据库中获取新值前,需要将例如id
这样的主键设置为nil。
更新
保存所有字段
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; |
保存
是一个组合函数。 如果保存值不包含主键,它将执行 Create
,否则它将执行 Update
(包含所有字段)。
db.Save(&User{Name: "jinzhu", Age: 100}) | |
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00") | |
db.Save(&User{ID: 1, Name: "jinzhu", Age: 100}) | |
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1 |
NOTE不要将Save
和Model
一同使用, 这是 未定义的行为。
更新单个列
当使用 Update
更新单列时,需要有一些条件,否则将会引起ErrMissingWhereClause
错误,查看 阻止全局更新 了解详情。 当使用 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 只会更新非零值的字段
// 根据 `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; |
注意 使用 struct 更新时, GORM 将只更新非零值字段。 你可能想用map
来更新属性,或者使用Select
声明字段来更新
更新选定字段
如果您想要在更新时选择、忽略某些字段,您可以使用 Select
、Omit
// 选择 Map 的字段 | |
// User 的 ID 是 `111`: | |
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) | |
// UPDATE users SET name='hello' WHERE id=111; | |
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) | |
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111; | |
// 选择 Struct 的字段(会选中零值的字段) | |
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0}) | |
// UPDATE users SET name='new_name', age=0 WHERE id=111; | |
// 选择所有字段(选择包括零值字段的所有字段) | |
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0}) | |
// 选择除 Role 外的所有字段(包括零值字段的所有字段) | |
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0}) |
删除
删除一条记录
删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如:
// 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 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字(如以下例子。也可以使用字符串——译者注)。查看 查询-内联条件(Query Inline Conditions) 了解详情。
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 支持 BeforeDelete
、AfterDelete
Hook,在删除记录时会调用这些方法,查看 Hook 获取详情
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%"; |
可以将一个主键切片传递给Delete
方法,以便更高效的删除数据量大的记录
govar 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); |
到这里就总结的差不多了,这是国人做的项目,所以中文文档很齐全,非常非常推荐去看看官方文档。