GORM | 青训营笔记

286 阅读11分钟

基本用法

首先可以先通过一段代码来了解一下database数据包的用法。这段代码交代了database数据包使用的四个步骤:“如何建立一个连接”、“如何使用连接去查询数据”、“如何将数据Scan到对象中去”以及“如何去处理其中的一些错误”。

package main

// database数据包的基本用法

import (
	"database/sql"
	_ "gitub.com/go-sql-driver/mysql"
)

func main() {
	// 如何建立一个连接:通过import导入driver,并通过driver+DSN的方式来实现数据库连接的初始化操作。
	db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
	// 如何使用连接去查询数据:在执行SQL语句后,通过rows来返回数据。当数据处理完毕后,需要将连接释放,防止资源的泄漏。
	rows, err := db.Query("select id, name from users where id = ?", 1)
	if err != nil {
		//xxx
	}
	defer rows.Close()
	// 怎么把数据Scan到对象中去:我们获取到的数据会通过rows来进行返回。
	// 这里的rows是一个游标,它通过Next()来不断获取数据。
	// 然而,使用Next()来关闭连接是会导致信息丢失的,并且正常地使用rows进行关闭时也会发生异常。
	// 因此,上面的代码需要在关闭连接时对异常进行捕获操作。
	var users []User
	for rows.Next() {
		var user User
		err := rows.Scan(&user.ID, &user.Name)

		if err != nil {
			// ...
		}
		users = append(users, user)
	}
	// 怎么去处理其中的一些错误:
	if rows.Err() != nil {
		// ...
	}
}

设计原理

database数据包采用极简接口设计原则:它对上层应用程序提供一个标准的api操作接口,并对下层驱动暴露简单的驱动接口。在database数据包的内部实现了连接池的管理。这意味着对于不同的数据库,只需实现一个相同的驱动连接接口、操作接口即可实现对不同数据库连接的支持。 在这里插入图片描述

连接池

数据库的连接池使用池化技术,它将昂贵费时的资源放到特定的池子里,同时维护最大连接数、最小连接数、阻塞队列等参数,方便管理整个池子,便于连接的复用。此外,它提供探活机制和监控功能,以提高查询性能。database数据包提供了两类方法来管理连接池: 1、连接池配置:配置连接池参数

// 连接池配置
func (db *DB) SetConnMaxIdleTime(d time.Duration)
func (db *DB) SetConnMaxLifeTime(d time.Duration)
func (db *DB) SetMaxIdleConns(n int)
func (db *DB) SetMaxOpenConns(n int)

2、连接池状态:管理连接池状态

// 连接池状态
func (db *DB) Stats() DBStats

下面一段代码展示了database数据包连接数据库的操作过程的伪实现,从该实现中可以发现数据库连接有两种策略: 1、尽量复用的策略:从连接池中获取连接并使用 2、新建新的连接:通过driver新建立一个连接

// 操作过程伪实现
for i := 0; i < maxBadConnRetries; i++ {
    //从连接池获取连接或通过 driver 新建连接
    dc, err := db.conn(ctx, strategy)
        // 有空闲连接 -> reuse -> mas life time
        // 新建连接 -> max open...
    
    //将连接放回连接池
    defer dc.db.putConn(dc, err, true)
        // validateConnection 有无错误
        // max life time, max idle conns 检查
    //连接实现 driver.Queryer, driver.Execer 等 interface
    if err == nil {
    err = dc.ci.Query(sql, args...)
    }
    
    isBadConn = errors.Is(err, driver.ErrBadConn)
    if !isBadConn{
        break
    }
}

连接接口

driver的连接接口是通过实现Register方法来注册的。在使用时,先通过import driver来导入相应的数据库driver;之后,在main方法中建立连接;最后,基于这个连接进行查询操作。而driver的注册操作是通过init方法来进行调用的。

// 业务代码
import _ "github.com/go-sql-driver/mysql"

func main(){
    db, err := sql.Open("mysql", "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local")
}

//github.com/go-sql-driver/mysql/driver.go
//注册 Driver
func init() {
    sql.Register("mysql", &MySQLDriver{})
}

这个连接接口的设计看起来十分合理,但实则有几处小问题: 1、DSN为字符串形式,不便于理解和相关处理 2、在使用时,driver会赋给_,这使得import语句没有编译检查,导致容易忘记导入driver从而引发运行时异常。 为了解决上面提到的两个小问题,database数据包提供了新的接口。新的接口中支持用户传入一个interface,这样就可以通过定义一个结构体来实现使用时的连接操作。

// 改进的连接接口
type Connector interface {
    Connect(context.Context) (Conn, error)
    Driver() Driver
}

func OpenDB(c driver.Connector) *DB {
    //...
}

有了这个接口定义,在连接数据库的时候就支持用户定义一个清晰的结构体,从这个结构体中我们可以很清楚地看到我们数据库连接的各个参数情况。

// 业务代码
import "github.com/go-sql-driver/mysql"

func main(){
    connector, err := mysql.NewConnector(&mysql.Config{
        User:        "gorm",
        Passwd:      "gorm",
        Net:         "tcp",
        Addr:        "127.0.0.1:3306",
        DBName:      "gorm",
        ParseTime:   true,
    })
    db := sql.OpenDB(connector)
}

操作接口

driver的操作接口主要在两个方面上进行设计:连接类型和处理返回数据方式。

1、在连接类型上分成了三种:直接连接(Conn)、预编译连接(Stmt)和事务(Tx)。其中,值得解释的是预编译连接方式,在该方式下,driver会先生成预编译语句,然后通过reference id来进行查询。采用这样的方式可以减少网络传输和解析sql语句的时间,从而提升系统的整体性能。

2、在数据处理方法上也划分为三个方法: Exec / ExecContext -> Result方法:只关心是否成功,返回成功失败信息 Query / QueryContext -> Rows方法:通过行的形式返回数据,需要手动close QueryRow / QueryRowContext -> Row方法:简化版Rows方法

GORM使用简介

背景知识

 GORM是“设计简洁、功能强大、可自由扩展的全功能ORM”。它本着“API精简”、“测试优先”、“最小惊讶”、“灵活扩展”、“无依赖”、“可信赖”的设计原则,提供了如下多种完善的功能: 1、关联:一对一、一对多、单表自关联、多态;Preload、Joins预加载、级联删除;关联模式;自定义关联表 2、事务:事务代码块、嵌套事务、Save Point 3、多数据库、读写分离、命名参数、Map、子查询、分组条件、代码共享、SQL表达式(查询、创建、更新)、自动选字段、查询优化器 4、字段权限、软删除、批量数据处理、Prepared Stmt、自定义类型、命名策略、虚拟字段、自动track时间、SQL Builder、Logger 5、代码生成、复合主键、Constraint、Prometheus、Auto Migration、真·跨数据库兼容 6、多模式灵活自由扩展 7、Developer Friendly

基本用法

我们先来对比一下使用GORM和使用SQL语句进行查询操作的代码实现:

// 使用GORM实现查询
import (
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)

func main(){
    db, err := gorm.Open(
        mysql.Open("user:password@tcp(127.0.0.1:3306)/hello")
    )
    var user []User
    Err = db.Select("id", "name").Find(&users, 1).Error
}
// 使用SQL实现查询
import(
    "database/sql"
    _ "gitub.com/go-sql-driver/mysql"
)

func main(){
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
    
    rows, err := db.Query("select id, name from users where id = ?", 1)
    if err != nil {
        //xxx
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var user User
        err := rows.Scan(&user.ID, &user.Name)
        
        if err != nil {
            // ...
        }
        users = append(users, user)
    }
    
    if rows.Err() != nil {
        // ...
    }
}

相比于SQL代码来说,GORM使用起来更加简洁,而简洁代码可以让系统的健壮性和可维护性更高。 下面我们来看一下在CRUD各个场景下,GORM代码应该如何编写:

// 操作数据库
db.AutoMigrate(&Product{})
db.Migrator().CreateTable(&Product{})
//https://grom.io/docs/migration.html
//版本管理 - https://github.com/go-gormigrate/gormigrate

1、创建场景

// 创建场景

// 创建
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // pass pointer of data to Create

user.ID              //返回主键 last insert id
result.Error         //返回 error
result.RowAffected   //返回影响的行数

//批量创建
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
db.CreateInBatches(user, 100)

for _, user := range users {
    user.ID //1, 2, 3
}

2、查询场景

//查询场景
// 读取
var product Product
db.First(&product, 1)                   // 查询id为1的product
db.First(&product, "code = ?", "L1212") // 查询code为L1212的product

3、更新场景

//更新场景
// 更新某个字段
db.Model(&product).Update("Price", 2000)
db.Model(&product).UpdateColumn("Price", 2000)
//更新多个字段
db.Model(&product).Updates(Product{Price: 2000, Code: "L1212"})
db.Model(&product).Updates(map[string]interface{}{"Price": 2000, "Code": "L1212"})
//批量更新
db.Model(&Product{}).Where("price < ?", 2000).Updates(map[string]interface{}{"Price": 2000})

4、删除场景

//删除场景
//删除
db.Delete(&product)

模型定义

GORM框架不仅支持基本类型、还支持结构的嵌套: 1、简单结构

//简单结构
type User struct {
    ID               uint
    Name             string
    Email            *string
    Age              uint8
    Birthday         *time.Time
    MemberNumber     sql.NullString
    ActivatedAt      sql.NullTime
    CreatedAt        time.Time
    UpdatedAt        time.Time
    DeletedAt        gorm.DeletedAt `gorm:"index"`
}

2、嵌套结构

// 嵌套结构
type User struct {
    gorm.Model
    ID               uint
    Name             string
    Email            *string
    Age              uint8
    Birthday         *time.Time
    MemberNumber     sql.NullString
    ActivatedAt      sql.NullTime
}
// gorm.io/gorm
Type Model struct {
    ID          uint            `gorm:"primaryKey"`
    CreatedAt   time.Time
    UpdatedAt   time.Time
    DeletedAt   gorm.DeletedAt  `gorm:"index"`
}

对于GORM来说,它也服从“约定优于配置”的设计范式,这已经成为软件设计中约定俗成的惯例。 在GORM中做了如下约定: 1、表名为struct name的snake_cases复数格式 2、字段名为field name的snake_cases单数格式 3、ID/id字段为主键,如果为数字,则为自增主键 4、CreatedAt字段,创建时,保存当前时间 5、UpdatedAt字段,创建、更新时,保存当前时间 6、gorm.DeletedAt字段,默认开启soft delete模式 在GORM中,一切皆可配置。

关联介绍

在GORM中,O是object的缩写,代表模型定义;R是relation的缩写,代表关联。在GORM中提供了多种类型的关联,下面利用一段描述,给出GORM中支持的几大类关联类型: User拥有一个Account(has one),拥有多个Pets(has many),多个Toys(多态 has many)。属于某Company(belongs to),属于某manager(单表 belongs to),管理 Team(单表 has many)。会多门Language(many to many),拥有很多Friends(单表 many to many),并且他的Pet也有一个Toy(多态 has one)。

// 模型定义

type User struct {
    gorm.Model
    Name         string
    Account      Account
    Pets         []*Pet
    Toys         []Toy        `gorm:"polymorphic:Owner"`
    CompanyID    *int
    Company      Company
    ManagerID    *uint
    Manager      *User
    Team         []User       `gorm:"foreignkey:ManagerID"`
    Languages    []Language   `gorm:"many2many:UserSpeak;"`
    Friends      []*User      `gorm:"many2many:user_friends;"`
}

type Pet struct {
    gorm.Model
    UserID   *uint
    Toy      Toy     `gorm:"polymorphic:Owner;"`
}

type Toy struct {
    ID          uint
    Name        string
    OwnerID     string
    OwnerType   string
    CreatedAt   time.Time
}

关联操作

从上面一段话中不难看出GORM支持的关联类型十分丰富,针对这些关联类型,GORM也提供了多种关联操作。 CRUD:关于关联操作的CRUD主要关注“如何保存关联”、“如何利用关联模式管理数据”和“如何支持批量操作”: 1、如何保存关联

// 如何保存关联

// 保存用户及其关联
db.Save(&User{
    Name: "jinzhu",
    Languages: []Language{{Name: "zh-CN"}, {Name: "en-US"}},
})

2、利用关联模式管理数据

// 利用关联模式管理数据

//关联模式
langAssociation := db.Model(&user).Association("Languages")

//查询关联
langAssociation.Find(&languages)

//将汉语,英语添加到用户掌握的语言中
langAssociation.Append([]Language{languageZH, languageEN})

//把用户掌握的语言替换为汉语、德语
langAssociation.Replace([]Language{languageZH, languageDE})

//删除用户掌握的两个语言
langAssociation.Delete(languageZH, languageEN)

//删除用户所有掌握的语言
langAssociation.Clear()

//返回用户所掌握的语言的数量
langAssociation.Count()

支持批量操作

//支持批量操作

//批量模式 Append, Repalce
var users = []User{user1, user2, user3}
langAssociation := db.Model(&users).Association("Languages")

//批量模式 Append, Repalce,参数需要与源数据长度相同
//例如:我们有3个user:将userA添加到user1的Team
//将userB添加到user2的Team,将userA、userB、userC添加到user3的Team
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{&userA, &userB, userC})

Preload / Joins 预加载

此外,GORM还提供对预加载的支持。 preload:使用preload进行预加载操作时,它会触发另一条SQL的执行 join:使用join进行预加载操作时,它会将查询转化为一条SQL进行查询

// preload / joins 预加载

type User struct {
    Orders   []order
    Profile  Profile
}

//查询用户的时候找出其订单,个人信息(1 + 1条SQL)
db.Preload("Orders").Preload("Profile").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // 一对多
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // 一对一

//使用Join SQL 加载(单条JOIN SQL)
db.Joins("Company").Joins("Manager").First(&user, 1)
db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users)

//预加载全部关联(只加载一级关联)
db.Preload(clause.Associations).Find(&users)
//多级预加载
db.Preload("Orders.OrderItems.Product").Find(&users)
//多级预加载 + 预加载全部一级关联
db.Preload("Orders.OrderItems.Product").Preload(clause.Associations).Find(&users)

//查询用户的时候找出其未取消的订单
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("orders.amount DESC")
}).Find(&users)

尽管preload在预加载时生成多条SQL语句来进行查询,但优于缓存操作的存在,多条SQL查询的效率不一定要比单条SQL语句查询效率低,甚至查询性能有可能更优。

级联删除

为了确保数据库中没有孤儿数据,所有数据都是有用的,GORM还提供了级联删除的支持。关于级联删除,GORM提供了两个方法。 方法一:数据库约束

// 数据库约束保证级联删除

type User struct {
    ID            uint
    Name          string
    Account       Account       `gorm:"canstraint:OnUpdate:CASCADE,OnDelete:CASCAE;"`
    CreditCards   []CreditCard  `gorm:"canstraint:OnUpdate:CASCADE,OnDelete:CASCAE;"`
    Orders        []Order       `gorm:"canstraint:OnUpdate:CASCADE,OnDelete:CASCAE;"`
}

//需要使用 GORM Migrate 数据库迁移数据库外键才行
db.AutoMigrate(&User{})

//如果启用软删除,在删除 User 时会自动删除其依赖
db.Delete(&User{})

方法二:select实现

// 使用Select实现级联删除,不依赖数据库约束及软删除

//删除 user 时,也删除user的account
db.Select("Account").Delete(&user)

//删除 user 时,也删除user的Orders、CreditCards记录
db.Select("Orders", "CreditCards").Delete(&user)

//删除user时,也删除user的Orders、CreditCards记录,也删除订单中的BillingAddress
db.Select("Orders", "Orders.BillingAddress", "CreditCards").Delete(&user)

//删除user时,也删除用户及其依赖的所有has one/many、many2many记录
db.Select(clause.Associations).Delete(&user)