Go ORM框架——Gorm基础教程| 青训营笔记

3,069 阅读8分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天

前言

本文主要内容为Go语言ORM框架Gorm的使用。

GormGo语言目前比较热门的数据库ORM操作库,对开发者也比较友好,使用非常简单,使用上主要就是把struct类型和数据库表记录进行映射,操作数据库的时候不需要直接手写SQL代码。

迄今为止,Gorm是一个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。

Gorm目前支持数据库有MySQLSQLServerPostgreSQLSQLite

基础使用

下面将以使用Gorm连接MySQL为例,看下Gorm的使用。

Gorm约定

Gorm在使用时有以下约定:

  • Gorm使用名为ID的字段作为主键
  • 如果没有TableName函数,使用结构体的蛇形复数作为表名
  • 字段名的蛇形作为列表
  • 使用CreatedAtUpdatedAt字段作为创建更新时间

安装依赖

需要两个依赖:MySQLGorm

// 安装MySQL依赖
go get -u gorm.io/driver/mysql
// 安装Gorm依赖
go get -u gorm.io/gorm

在实际执行的时候,安装MySQL后,会自动安装Gorm

使用方法

Gorm操作MySQL的步骤如下:

  1. 先用struct定义一个模型,字段与数据库的字段一致
  2. 使用Gorm连接数据库
  3. 使用Gorm操作数据库

既然是操作数据库,那么就需要有数据库表,下面以product表为例,先看下表结构

CREATE TABLE `product` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(50) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

导入连接MySQL的依赖包

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

创建模型

创建一个Product类型的结构体,结构体里的字段和MySQLproduct表字段一一对应。

type Product struct {
    ID unit
    Code  string
    Price uint
}

struct的字段和MySQL表里的字段是一样对应的,包括大小写,如果MySQL的字段是小写的话,比如code字段不一致(如数据中叫product_code),那么在结构体里需要加上关系对应,对应的写法为:

Code  string `gorm:"column:product_code"`

如果是主键的话可以写成:

ID  string `gorm:"primarykey"`

通过使用default标签为字段定义默认值:

Name  string `gorm:"default:galeone"`

创建表名对应关系,刚才创建的结构体ProductMySQL里的表名并不一致,所以要创建一个对应关系,创建方式为为Product结构体增加一个TableName方法

func (p Product) TableName() string {
    return "product"
}

连接数据库

打开数据库连接,通过mysql.Open函数打开连接,该函数需要传递一个dsndsn是连接数据库的一个连接串,相当于Java中的JDBC连接串。

dsn的格式为:{username}:{password}@tcp({host}:{port})/{dbname}?charset=utf8&parseTime=True&loc=Local

具体创建数据库连接的代码为:

dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect database, error=" + err.Error())
}

创建数据

通过Create函数创建数据,先初始化一个结构体,然后调用Create函数。

p := Product{Code: "D42", Price: 100}
if err := db.Create(&p).Error; err != nil {
    fmt.Println("插入失败", err)
}

Create函数还支持创建多条数据,如果是多条的话,需要传一个数组参数。

products := []*Product{{Code: "D42", Price: 100},{Code: "D43", Price: 200}}
if err := db.Create(&products).Error; err != nil {
    fmt.Println("插入失败", err)
}

如果插入数据的时候,出现了主键冲突怎么办?可以使用OnConflict处理冲突。

p := Product{Code: "D42", ID: 1}
if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p).Error; err != nil {
    fmt.Println("插入失败", err)
}

DoNothingtrue代表如果冲突的话,什么也不做。

查询数据

Gorm通过First函数来进行数据的查询,如果First函数的第二个参数是个整形的话,会默认按主键进行查询,如果是字符串,则会直接拼接在where后面。

var product Product
db.First(&product, 1)

这段代码生成的SQL如下:

SELECT * FROM `product` WHERE `product`.`code` = 1 ORDER BY `product`.`code` LIMIT 1

如果要按照指定的条件查询的话,则需要传条件参数:

var product Product
db.First(&product, "Code = ?", "D42")

或者使用Where函数:

db.Where("Code = ?", "D42").First(&product)

这两种方法的功能是一样的,生成的SQL如下:

SELECT * FROM `product` WHERE Code = 'D42' ORDER BY `product`.`code` LIMIT 1

First函数只能查询一条记录,如果要查多条的话,需要使用Find函数。

products := make([]Product, 0)
db.Find(&products, "price = ?", 2)

使用IN查询:

products := make([]*Product, 0)
db.Where("price IN ?", []uint{100, 2}).Find(&products)

使用like查询:

products := make([]*Product, 0)
db.Where("code like ?", "%D%").Find(&products)

使用结构体查询:

db.Where(&Product{Code: "D43", Price: 0}).Find(&products)

生成的SQL为:

SELECT * FROM `product` WHERE `product`.`code` = 'D43'

注意

  • 使用First查询时,如果查询不到数据会返回ErrRecordNotFound
  • 使用Find查询时,查询不到数据不会返回错误
  • 使用结构体作为查询条件时,Gorm只会查询非零值字段,也就是0''false或其他零值字段将被忽略,可以使用MapSelect来替换

更新数据

Gorm更新数据是通过Update函数操作的,Update函数需要传入要更新的字段和对应的值。

需要通过Model函数来传入要更新的模型,主要是用来确定表名,也可以使用Table函数来确定表名。

db.Model(&Product{}).Update("Price", 200)

如果要更新多个字段的话,可以使用Updates函数,该函数需要传入一个结构体或map

db.Model(&Product{}).Updates(Product{Code: "D43", Price: 1})

如果使用结构体作为参数的话,可以省略Model这一部分,因为Gorm也会从参数的结构体中查询相关表名。

注意在使用结构体时,不会更新零值,如果要更新的话,需要使用map

db.Model(&Product{}).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

如果是按条件更新的话,可以通过Where函数传入条件:

db.Model(&Product{}).Where("Code = ?", "D42").Update("Price", 200)

更新选定字段,只会更新Select函数里的字段,其他字段会被忽略:

db.Model(&Product{}).Select("price").Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

表达式更新,如果要实现update product set price = price * 2 * 100的话,实现方式如下:

db.Model(&Product{}).Update("price", gorm.Expr("price * ? * ?", 2, 100))

删除数据

删除数据又分为物理删除和逻辑删除。

物理删除

删除数据使用的是Delete函数,Delete函数需要传入一个结构体以及参数,如果参数是整形的话,会按主键进行删除。

db.Delete(&Product{}, 1)
db.Delete(&Product{}, "Code = ?", "F42")
db.Delete(&Product{}, []int{1,2,3})
db.Where("code like ?", "%F%").Delete(Product{})

如果想按条件删除的话,需要使用Where函数:

db.Where("Code = ?", "F42").Delete(&Product{})
软删除

Gorm提供了软删除的能力,需要在结构体中定义一个Deleted字段,此时再调用Delete删除函数,则会生成update语句,并将deleted字段赋值为当前删除时间。

疑问:下面代码是实际操作的代码,生成的SQL的删除字段是deleted,但是从网上查的很多资料都是deleted_at,不知道是不是我哪里写错了,如果有知道的小伙伴,帮忙指正一下吧,多谢。

type Product struct {
    ID      uint
    Code    string
    Price   uint
    Deleted gorm.DeletedAt
}

再次执行删除操作:

db.Delete(&Product{}, 1)

生成的sql为:

UPDATE `product` SET `deleted`='2023-01-30 22:22:22.202' WHERE `product`.`id` = 1 AND `product`.`deleted` IS NULL

查询被软删除的操作,要使用Unscoped函数:

db.Unscoped().First(&product, 2)

有了DeleteAt字段后,删除操作已经变成了更新操作,那么想要物理删除怎么办?也是使用Unscoped函数:

db.Unscoped().Delete(&Product{}, 1)

Gorm事务

Gorm提供了BeginCommitRollback方法用于使用事务。

// 开启事务
tx := db.Begin()
if err := tx.Create(&Product{Code: "D32", Price: 100}).Error; err != nil {
    // 出现错误回滚
    tx.Rollback()
    return
}
if err := tx.Create(&Product{Code: "D33", Price: 100}).Error; err != nil {
    // 出现错误回滚
    tx.Rollback()
    return
}
// 提交事务
tx.Commit()

注意:在开启事务后,调用增删改操作是应该使用开启事务返回的tx而不是db

Gorm还提供了Transaction函数用于自定提交事务,避免用户漏写CommitRollback

if err = db.Transaction(func(tx *gorm.DB) error {
    if err := db.Create(&Product{Code: "D55", Price: 100}).Error; err != nil {
        return err
    }
    if err := db.Create(&Product{Code: "D56", Price: 100}).Error; err != nil {
        return err
    }
    return nil
}); err != nil {
    return
}

这种写法,当出现错误时会自动进行Rollback,当正常执行时会自动Commit

Hook

当我们想在执行增删改查操作前后做一些额外的操作时,可以使用Gorm提供的Hook能力。

Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数,如果任何Hook返回错误,Gorm将停止后续的操作并回滚事务。

Hook会开启默认事务,所以会带来了一些性能损失。

性能提高

对于写操作(创建、更新、删除),为了确保数据的完整性,Gorm会将他们封装在事务内运行,但是这样会降低性能,可以使用SkipDefaultTransaction关闭默认事务。

使用PrepareStmt缓存预编译语句可以提高后续调用的速度。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
  PrepareStmt: true,
})

Gorm生态

Gorm拥有非常丰富的扩展生态,下面列举一些常用的扩展。

引用

字节内部视频——Go框架三件套详解(Web/RPC/ORM)

Gorm快速入门教程