日常开发中单表增删改查是十分常用的,相比于直接编写SQL,使用GORM能够极大简化这些增删改查操作的代码编写。这篇笔记我们学习如何使用GORM实现单表增删改查。
ORM框架需要数据模型来映射数据库表。因此在具体使用GORM进行数据库操作前,我们需要先定义数据模型。
这里我们有2个数据库表用于演示,其中t_category
是产品分类表,t_product
是产品表。
具体的数据模型代码定义如下。
model/product.go
package model
import "time"
type Product struct {
ProductId int64 `gorm:"column:product_id;primaryKey;autoIncrement"`
Sku string `gorm:"column:sku"`
CategoryId int64 `gorm:"column:category_id"`
ProductName string `gorm:"column:product_name"`
Price string `gorm:"column:price"`
Store int32 `gorm:"column:store"`
CreateTime time.Time `gorm:"column:create_time"`
}
func (*Product) TableName() string {
return "t_product"
}
model/category.go
package model
type Category struct {
CategoryId int64 `gorm:"column:category_id;primaryKey;autoIncrement"`
CategoryName string `gorm:"column:category_name"`
}
func (*Category) TableName() string {
return "t_category"
}
Product结构体中,我们按照数据表的类型定义了结构体的类型。注意Price
字段,这里由于Go语言没有decimal
类型,因此这里使用的是string
来映射的,而CreateTime
数据库表中为datetime
类型,这里使用time.Time
类型来映射。Category结构体比较简单,只有2个字段。有关其它数据库类型和Go语言类型的映射关系具体参考文档即可,这里就不多介绍了。
上面代码中,数据模型中使用了Tag指定字段的数据库字段名、是否为主键、是否为自增长等信息,常用的Tag如下表:
标签名 | 说明 |
---|---|
column | 字段列名 |
type | 列类型 |
size | 列数据长度 |
primaryKey | 表明列为主键 |
unique | 列具有唯一约束 |
default | 指定列的默认值 |
not null | 指定列不为空 |
autoincrement | 指定列为自增长 |
comment | 指定列的注释 |
还有一些其它常用配置具体参考文档即可。
这里有同学可能有疑问了,数据库表都已经建好了,ORM模型中的type
、size
、not null
这些Tag有什么用呢?其实ORM框架的使用有两种方式(这里借用C#的一个ORM框架Entity Framework的概念):Code First和Database First,前者的工作流程是先写数据模型,由框架根据数据模型生成数据迁移(Data Migration)然后根据数据迁移生成数据库表;后者的工作流程是先建数据库表,然后手写数据模型或是使用代码生成插件根据表结构生成代码,同时由程序员保证两者之间的映射关系不会出错。
虽然Code First模式也有很多拥簇着,但实际开发中较大型的项目还是倾向于使用Database First。有关两种方式哪个更好这里不做讨论,这里我们要知道GORM也支持Code First的开发模式,如果要使用Code First就要把GORM模型的Tag写全;如果使用Database First就不必把仅在建表阶段起作用的Tag写出来了,写出运行时必要的Tag即可。
这里我们介绍一些单表查询的写法,关联查询将在后续章节介绍。
下面代码我们使用主键作为条件查询数据。
package main
import (
"errors"
"fmt"
"github.com/gacfox/demoorm/conf"
"github.com/gacfox/demoorm/model"
"gorm.io/gorm"
)
func main() {
conf.InitDb()
var product model.Product
result := conf.Db.First(&product, 1)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 未查到数据
fmt.Println("未查询到数据")
} else if result.Error != nil {
// 其他错误
panic(result.Error)
} else {
// 查到了数据
fmt.Println(product)
}
}
这里First()
方法接收两个参数,第1个参数是要映射数据的结构体,第2个参数是主键。它会在没有数据时产生错误gorm.ErrRecordNotFound
,我们可以根据result.Error
来判断是否查询到了结果。如果没有报错,数据记录会被映射到product
结构体。此外First()
方法本质上是类似LIMIT 1
的操作,这里如果有多条记录,也只会取第一条。
此外,主键也可以传多个,此时我们需要用数组来接收结果。
package main
import (
"fmt"
"github.com/gacfox/demoorm/conf"
"github.com/gacfox/demoorm/model"
)
func main() {
conf.InitDb()
var products []model.Product
result := conf.Db.Find(&products, []int{4, 5, 6})
if result.Error != nil {
panic(result.Error)
}
fmt.Printf("共%v条记录\n", result.RowsAffected)
for _, product := range products {
fmt.Println(product)
}
}
上面代码我们查询了主键为4
、5
、6
的数据记录,并打印了查询结果。这里和之前不同,如果查询不到结果将返回空数组,而不是抛出ErrRecordNotFound
错误。
如果要查询全部数据,还是使用Find()
方法,不传任何体条件即可,返回值还是使用数组来接收。
var products []model.Product
result := conf.Db.Find(&products)
GORM支持很多种风格的API实现条件查询,这里我们使用其中一种类似SQL风格的查询条件写法,下面是一些例子。
实现精确的条件查询,这里查询sku
为230410002
的数据:
var products []model.Product
sku := "230410002"
result := conf.Db.Where("sku=?", sku).Find(&products)
实现IN查询:
var products []model.Product
skus := []string{"230410001", "230410002"}
result := conf.Db.Where("sku in ?", skus).Find(&products)
实现LIKE查询:
var products []model.Product
pattern := "%主板%"
result := conf.Db.Where("product_name like ?", pattern).Find(&products)
使用AND连接两个查询条件:
var products []model.Product
skus := []string{"230410002", "230410003", "230410004"}
pattern := "%主板%"
result := conf.Db.Where("product_name like ? and sku in ?", pattern, skus).Find(&products)
GORM没有对分页进行直接的支持,但提供了Limit()
和Offset()
这两个方法对应SQL中的LIMIT
语句,我们可以基于这两个方法实现分页。下面例子中,我们联用了Where()
方法和分页方法。
var products []model.Product
pattern := "%主板%"
pageSize := 10
pageNum := 1
result := conf.Db.Where("product_name like ?", pattern).Limit(pageSize).Offset(pageSize * (pageNum - 1)).Find(&products)
对于一些复杂的查询,我们也可以基于代码逻辑实现复杂的动态查询。
GORM中我们可以使用Create()
方法创建数据,其参数是填好数据的数据模型指针。下面是一个例子。
package main
import (
"fmt"
"github.com/gacfox/demoorm/conf"
"github.com/gacfox/demoorm/model"
"time"
)
func main() {
conf.InitDb()
product := model.Product{
Sku: "230410004",
CategoryId: 1,
ProductName: "Asus H610 主板",
Price: "199.00",
Store: 30,
CreateTime: time.Now(),
}
result := conf.Db.Create(&product)
if result.Error != nil {
// 插入报错
panic(result.Error)
} else {
// 新插入数据的主键
fmt.Println(product.ProductId)
// SQL影响数据记录数
fmt.Println(result.RowsAffected)
}
}
这里Create()
函数也支持传入数组用于批量插入,我们批量插入时我们需要控制好Batch Size,以免超过最大限制而报错。
删除数据时,我们可以指定主键来删除,也可以指定一个条件删除多条数据。
下面例子中,我们的product
对象中包含了主键,调用Delete()
方法时会自动根据其主键进行删除。这种方式常用于先查询,再根据查询结果删除的场景。
conf.Db.Delete(&product)
当然,我们也可以直接指定主键来删除数据。下面例子中,我们删除了主键为1
的数据记录。
conf.Db.Delete(1)
下面例子中,我们使用了Where()
方法指定了一个条件实现删除多条数据记录。
pattern := "%主板%"
conf.Db.Where("product_name like ?", pattern).Delete(&model.Product{})
GORM中更新使用Save()
方法。更新数据一个比较常用的场景就是先查询数据,然后修改某些字段后再保存,此时可以使用Save()
方法实现更新。
// 查询数据
product := model.Product{}
conf.Db.First(&product, 1)
// 更新数据
product.Store = 300
result := conf.Db.Save(&product)
if result.Error != nil {
panic(result.Error)
} else {
fmt.Println(result.RowsAffected)
}
代码中,我们先查询主键为1
的数据记录,然后更新了一个字段后调用了Save()
进行保存。
注意:Save()
方法有一个坑,它同时具有插入和更新的功能,它的内部逻辑是先执行UPDATE
,如果RowsAffected
为0
就会自动执行INSERT
。如果我们的数据模型查询出来后没有做任何更改又调用了Save()
方法,就会报错主键冲突,因为没有更改时RowsAffected
也是0
,GORM内部就会执行插入,此时就会报错。这个问题大概也是GORM设计上的一个失误,至今也没有解决,我们实际开发时要注意以下,没有更改就不要调用Save()
了。