这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
今天和大家分享gorm的概念和常见操作。
ORM
ORM(Object Relation Mapping)通过实例化对象完成对关系型数据库的操作。
连接数据库
连接一个mysql数据库:
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3308)/gin_learn?charset=utf8mb4&parseTime=True&loc=Local"
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
fmt.Println("连接成功...")
}
解释以下其中的dsn字符串:
用户名:密码@tcp(ip地址:端口)/数据库名称?charset=utf8mb4&parseTime=True&loc=Local
logger
Logger | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
gorm 设置表名
通过给结构体定义TableName方法,可以指定结构体的表名:
// Product 声明一个商品模型
type Product struct {
gorm.Model
Code string
Price uint
}
// 指定商品模型的表名
func (p Product) TableName() string {
return "product"
}
gorm 模型迁移
使用AutoMigrate函数可以将做好的模型迁移到数据库,如果当前数据库不具有该模型,则生成一个表。
// Product 声明一个商品模型
type Product struct {
gorm.Model
Code string
Price uint
}
err = db.AutoMigrate(&model.Product{})
这里根据模型生成的表结构中,列都是小写下划线形式的,例如code,create_at
gorm 增删改查
我们将连接数据库生成的db作为包级别的首先声明:
var db *gorm.DB
然后定义init函数来初始化数据库:
func init() {
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 彩色打印
},
)
dsn := "root:123456@tcp(127.0.0.1:3308)/gin_learn?charset=utf8mb4&parseTime=True&loc=Local"
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
fmt.Println("连接成功...")
}
init函数go语言会自动执行,我们不需要引用它。
在main函数中,我们执行增删改查的动作:
func main() {
// 增
//db.Create(&model.Product{})
var p model.Product
// 查单个
//db.Find(&p, "code = ?", "D42")
//fmt.Println(p.Price)
// 查多个
//var products []model.Product
//result := db.Find(&products, "code = ?", "D42")
//fmt.Println(result.RowsAffected)
//fmt.Println(products[0].CreatedAt)
//fmt.Println(products[1].CreatedAt)
// 更新一列
//db.Model(&p).Where("id = ?", "1").Update("price", 300)
// 更新多列
//db.Model(&p).Where("id = ?", "1").Updates(model.Product{
// Price: 400,
// Code: "D43",
//})
// 更新零值 由于gorm处于保护机制,在更新多列时并不会更新零值,我们需要使用map去更新0值
//db.Model(&p).Where("id = ?", "1").Updates(map[string]interface{}{
// "Code": "",
// "Price": 0,
//})
// 删除
// 软删除 当模型具有deleted_at字段时 gorm会通过设置deleted_at字段的方法来进行软删除删除
// db.Delete(&p, 1) // 进行删除后gorm后,其实执行的是 UPDATE `products` SET `deleted_at`='2022-12-30 20:14:35.437' 操作
db.Find(&p, "id = ?", "2")
fmt.Println(p.Code)
}
gorm 更新零值的第二种方式
在上面的代码中,我们可以使用map[string]interface{}{...}更新零值,现在我们也可以使用sql.NullString这样的方式去更新零值或空值:
空字符串:sql.NullString
数字0:sql.NullInt32
例子:
// 修改模型 Product
type Product struct {
gorm.Model
Code sql.NullString
Price uint
}
// 增加记录:
func main() {
p := &model.Product{
Code: sql.NullString{
Valid: false,
},
}
db.Create(p)
}
这样增加的记录,Code是null。(并非是空字符串而是直接null)
我们更新记录使得该列不再null,而是空字符串:
func main() {
var p model.Product
db.Model(&p).Where("id = ?", 4).Update("code", sql.NullString{
Valid: true,
String: "",
})
fmt.Println(p.Code.String) // ""
}
gorm 模型定义
我们使用gorm时,尽量遵守其模型定义规范,这样使用gorm会方便很多:
模型定义 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
例子:
// Food 声明一个食物模型
type Food struct {
FoodId uint `gorm:"primaryKey"`
Name string `gorm:"column:food_name;type:varchar(64);index:idx_food_name,unique"`
DeletedAt gorm.DeletedAt
}
其中的FoodId作为主键;Name作为索引,在数据库中的列名为food_name,unique代表将列定义为唯一键;DeleteAt为gorm.DeletedAt类型可以用作软删除。
gorm 批量添加
方案1:
创建一个model切片,使用db.Create添加:
foods := []model.Food{
{
Name: "feed",
},
{
Name: "banana",
},
{
Name: "orange",
},
}
res := db.Create(foods) // 注意这里不使用指针,因为切片本身就是引用类型
方案2:
// 批量添加 每次添加2条
db.CreateInBatches(foods, 2)
这里的批量添加会每次向数据库中添加两条记录,如果一共有3条记录,那么会添加两次:
INSERT INTO `foods` (`food_name`,`deleted_at`) VALUES ('feed',NULL),('banana',NULL)
INSERT INTO `foods` (`food_name`,`deleted_at`) VALUES ('orange',NULL)
方案3:
// 使用map添加
db.Model(&model.Food{}).Create([]map[string]interface{}{
{
"Name": "apple",
},
{
"Name": "orange",
},
{
"Name": "grape",
},
})
使用map这种方式批量添加只会添加定义的列和主键,其他的列不会使用默认值而是直接为NULL
gorm 局部更新 (+更新列零值的第三种方式)
gorm在使用Updates方法时,gorm会自动忽略没有赋值或者赋值为零值的属性。当我们想只局部更新某些属性时,可以使用Select方法选择部分属性进行更新:
func main() {
p := &model.Product{}
db.First(&p, "code = ?", "D42").Select("price").Updates(model.Product{
Code: "xxx",
Price: 200,
})
}
当我们确定好了更新范围时,即使后面的更新函数中写明了要更新其他列,sql语句中也只会更新范围内的列:
UPDATE `products` SET `updated_at`='2023-01-01 15:34:57.693',`price`=200 WHERE code = 'D42' AND `products`.`deleted_at` IS NULL AND `id` = 2 ORDER BY `products`.`id` LIMIT 1
除了使用Select选择之外,还可以使用Omit进行忽略操作。
gorm 添加依赖的数据
当两个模型之间存在依赖的时候,我们可以利用gorm来建立其之间的依赖:
例如我们建立员工表和公司表,假设员工和公司都是一一对应的,有一个员工就有一个公司(极端情况,勿带入实际):
// 模型 这里的每一个Employer都对应一个确定的公司
type Employer struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
gorm.Model
Name string
}
// 插入数据时
company1 := model.Company{
Name: "xx公司",
}
employ1 := model.Employer{
Name: "小王",
Company: company1, // 这里我们直接拷贝company1
}
db.Create(&employ1) // 在插入employ1记录时,会先自动添加一个Company记录,根据company1填写相关字段
最终效果:
// 公司表
id
2 2023-01-14 01:24:11.441 2023-01-14 01:24:11.441 xx公司
// 人员表
id company_id
1 2023-01-14 01:24:11.446 2023-01-14 01:24:11.446 小王 2
一对多的关联添加
继续上面的例子,我们继续建立信用卡表,员工和信用卡的关系是一对多的关系:
// model
type Employer struct {
gorm.Model
Name string
CompanyID int
Company Company
CreditCard []CreditCard `gorm:"foreignKey:employer_id;references:id"` // 使用切片来表示一对多的关系
}
type CreditCard struct {
gorm.Model
Number string
EmployerId uint
}
我们添加的时候可以这样去添加:
employ := model.Employer{}
db.First(&employ, 1) //找到id为1的员工记录
for i := 0; i < 5; i++ {
id := i + 1
c := model.CreditCard{
Number: "信用卡" + strconv.Itoa(id+1),
EmployerId: employ.ID,
}
db.Create(&c)
}
这样我们对一对多的员工记录进行查询时,可以预加载其中的所有信用卡记录:
employ := model.Employer{}
db.Preload("CreditCards").First(&employ, 1)
fmt.Println(employ.ID)
fmt.Println(employ.CreditCards) // 因为预加载,虽然我们查询的是员工表,但实际上有两条查询语句,最终可以查询到所有信用卡记录
并且gorm在处理这样一对一多的场景,都会在创建表时添加上外键。详情:Belongs To | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
gorm 预加载
预加载 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
gorm 解决冲突
使用clause.OnConflict来解决添加冲突:
p := &Product{ID: 1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p) // 如果添加过程中出现重复主键等冲突情况,由于DoNothing: true,出现冲突后gorm会什么都不做并且中断操作。
gorm tag
`gorm: "primarykey"` 主键
`gorm: "column: code"` 列名
`gorm: "default: 18"` 默认值
`gorm: "type: varchar(8)"` 数据在数据库中的类型
`gorm: "foreignKey:identity;references:user_identity"` 将该属性锁对应的结构体中的identity作为外键,和当前整体对象的user_identity属性做映射
gorm 非链式调用
gorm是支持非链式调用的,并且能保证达到链式调用的效果:
tx = Db.Model(&SubmitBasic{})
if pid != "" {
tx.Where("problem_identity = ?", pid)
}
if uid != "" {
tx.Where("user_identity = ?", uid)
}
if status != 0 {
tx.Where("status = ?", status)
}
return tx
这样写,每个分支中的查询都会影响tx,但是不用写成链式调用。