单表CRUD

日常开发中单表增删改查是十分常用的,相比于直接编写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 指定列的注释

还有一些其它常用配置具体参考文档即可。

Code First vs Database First

这里有同学可能有疑问了,数据库表都已经建好了,ORM模型中的typesizenot 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)
    }
}

上面代码我们查询了主键为456的数据记录,并打印了查询结果。这里和之前不同,如果查询不到结果将返回空数组,而不是抛出ErrRecordNotFound错误。

查询全部数据

如果要查询全部数据,还是使用Find()方法,不传任何体条件即可,返回值还是使用数组来接收。

var products []model.Product
result := conf.Db.Find(&products)

条件查询

GORM支持很多种风格的API实现条件查询,这里我们使用其中一种类似SQL风格的查询条件写法,下面是一些例子。

实现精确的条件查询,这里查询sku230410002的数据:

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,如果RowsAffected0就会自动执行INSERT。如果我们的数据模型查询出来后没有做任何更改又调用了Save()方法,就会报错主键冲突,因为没有更改时RowsAffected也是0,GORM内部就会执行插入,此时就会报错。这个问题大概也是GORM设计上的一个失误,至今也没有解决,我们实际开发时要注意以下,没有更改就不要调用Save()了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap