gorm学习笔记 | 青训营笔记

258 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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,但是不用写成链式调用。