GORM基础 | 青训营笔记

517 阅读12分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第6篇笔记

1. GORM简介

GORM是一个使用Go语言编写的ORM框架。它文档齐全,对开发者友好,支持主流数据库。

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

2. Gorm连接数据库

2.1. 安装Gorm

  • go get -u gorm.io/gorm
  • go get -u gorm.io/driver/mysql

2.2. 连接数据库

GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server,本笔记使用Mysql数据库作为演示:

  1. 新建一个db_gorm数据库

  1. 连接数据库
package main

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

func main() {
	// root为数据库登录用户名,123456为登录密码,db_gorm为要连接的数据库
	dsn := "root:123456@tcp(127.0.0.1:3306)/db_gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    // 获取通用数据库对象sql.DB,然后可以使用它提供的方法对连接池进行设置
    sqlDB, err := db.DB()
    // SetMaxIdleConns 设置连接池中空闲连接的最大数量
    sqlDB.SetMaxIdleConns(10)
    // SetMaxOpenConns 设置打开数据库连接的最大数量
    sqlDB.SetMaxOpenConns(100)
    // SetConnMaxLifetime 设置连接可复用的最大时间
    sqlDB.SetConnMaxLifetime(time.Hour)
	fmt.Println(db, err)
}

image.png

上面只是最基本的数据库连接,并没有对数据库连接进行其他设置,MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:

db, err := gorm.Open(mysql.New(mysql.Config{
    DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
    DefaultStringSize: 256, // string 类型字段的默认长度
    DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
    DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
    DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
    SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{})
  1. 使用连接池连接数据库
package main

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

func main() {
	// root为数据库登录用户名,123456为登录密码,db_gorm为要连接的数据库
	dsn := "root:123456@tcp(127.0.0.1:3306)/db_gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    // 获取通用数据库对象sql.DB,然后可以使用它提供的方法对连接池进行设置
    sqlDB, err := db.DB()
    // SetMaxIdleConns 设置连接池中空闲连接的最大数量
    sqlDB.SetMaxIdleConns(10)
    // SetMaxOpenConns 设置打开数据库连接的最大数量
    sqlDB.SetMaxOpenConns(100)
    // SetConnMaxLifetime 设置连接可复用的最大时间
    sqlDB.SetConnMaxLifetime(time.Hour)
	fmt.Println(db, err)
}

2.3. Gorm配置

gorm.Config结构体中,GORM 提供的配置可以在初始化时使用,Config源码如下:

// Config GORM config
type Config struct {
	// GORM perform single create, update, delete operations in transactions by default to ensure database data integrity
	// You can disable it by setting `SkipDefaultTransaction` to true
	SkipDefaultTransaction bool
	// NamingStrategy tables, columns naming strategy
	NamingStrategy schema.Namer
	// FullSaveAssociations full save associations
	FullSaveAssociations bool
	// Logger
	Logger logger.Interface
	// NowFunc the function to be used when creating a new timestamp
	NowFunc func() time.Time
	// DryRun generate sql without execute
	DryRun bool
	// PrepareStmt executes the given query in cached statement
	PrepareStmt bool
	// DisableAutomaticPing
	DisableAutomaticPing bool
	// DisableForeignKeyConstraintWhenMigrating
	DisableForeignKeyConstraintWhenMigrating bool
	// DisableNestedTransaction disable nested transaction
	DisableNestedTransaction bool
	// AllowGlobalUpdate allow global update
	AllowGlobalUpdate bool
	// QueryFields executes the SQL query with all fields of the table
	QueryFields bool
	// CreateBatchSize default create batch size
	CreateBatchSize int

	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool
	ConnPool ConnPool
	// Dialector database dialector
	Dialector
	// Plugins registered plugins
	Plugins map[string]Plugin

	callbacks  *callbacks
	cacheStore *sync.Map
}

其中,重要配置为:

  • 跳过默认事务

GORM 默认会将单个的 create, update,delete操作封装在事务内进行处理,以确保数据的完整性。如无必要,可以关闭默认的事务,获得更大的性能提升, 事务的全局性或者临时关闭,即使在关闭默认事务,仍然可以通过方法 Begin方法开启事务。SkipDefaultTransaction设置为true则关闭默认事务:

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

如需使用事务,可以手动开启事务:

// 手动开启事务
tx := db.Begin()

// 在事务中做一些数据库操作
tx.Create(...)

// ...

// 发生错误时回滚事务
tx.Rollback()

// 提交事务
tx.Commit()
  • 命名策略

GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定,这需要实现Namer接口:

type Namer interface {
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(table string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
}

默认 NamingStrategy 也提供了几个选项,如:

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // table name prefix, table for `User` would be `t_users`
    SingularTable: true, // use singular table name, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name
  },
})
  • 日志

GORM 允许通过覆盖此选项更改 GORM 的默认logger

Logger:logger.Default.LogMode(logger.Info),     // 设置日志级别,控制台会打印SQL语句的执行信息

也可以在执行操作时手动加上Debug方法打印日志信息:

db.Debug().Create()
  • 前移时禁用外键约束

AutoMigrate(自动迁移) 或 CreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true(一般禁用,物理外键会降低数据库性能,如今更多地是主张使用逻辑外键而不是物理外键):

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

2.4. 自定义配置连接数据库

package main

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

func main() {
	// 自定义配置连接
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN:"root:123456@tcp(127.0.0.1:3306)/db_gorm?charset=utf8mb4&parseTime=True&loc=Local",
		DefaultStringSize: 191, // utf8mb4的字符串长度应该设置为191字节,
		// 参考https://cloud.tencent.com/developer/article/1917039
	}), &gorm.Config{
		SkipDefaultTransaction: true,
		NamingStrategy: schema.NamingStrategy{
			TablePrefix:   "t_", // 表明前缀,`User`的表名为`t_users`
			SingularTable: true, // 使用单数表名,启用该项,`User`的表名为`t_user`
		},
		DisableForeignKeyConstraintWhenMigrating: true, // 禁用外键约束
		Logger:logger.Default.LogMode(logger.Info),     // 设置日志级别,控制台会打印SQL语句的执行信息
	})
}

以上的代码全都放在main函数下,实际开发中,会封装成一个函数单独完成某个功能,比如可以把数据库连接封装成一个函数,以供其他包或者函数调用。

package gorm_mysql

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"time"
)

// 建立数据库连接
func ConnectDb() (*gorm.DB, error) {
	// 自定义配置连接
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN:"root:123456@tcp(127.0.0.1:3306)/db_gorm?charset=utf8mb4&parseTime=True&loc=Local",
		DefaultStringSize: 191, // utf8mb4的字符串长度应该设置为191字节,
		// 参考https://cloud.tencent.com/developer/article/1917039
	}), &gorm.Config{
		SkipDefaultTransaction: true,
		NamingStrategy: schema.NamingStrategy{
			TablePrefix:   "t_", // 表明前缀,`User`的表名为`t_users`
			SingularTable: true, // 使用单数表名,启用该项,`User`的表名为`t_user`
		},
		DisableForeignKeyConstraintWhenMigrating: true, // 禁用外键约束
		Logger:logger.Default.LogMode(logger.Info), // 设置日志级别,控制台会打印SQL语句的执行信息
	})
	if err != nil {
		panic("failed to connect database")
	}
	// 获取通用数据库对象sql.DB,然后可以使用它提供的方法对连接池进行设置
	sqlDB, err := db.DB()
	// SetMaxIdleConns 设置连接池中空闲连接的最大数量
	sqlDB.SetMaxIdleConns(10)
	// SetMaxOpenConns 设置打开数据库连接的最大数量
	sqlDB.SetMaxOpenConns(100)
	// SetConnMaxLifetime 设置连接可复用的最大时间
	sqlDB.SetConnMaxLifetime(time.Hour)
	fmt.Println(db, err) // 打印连接信息

	// 返回数据库连接
	return db, err
}

2.5. 使用配置文件连接数据库

在日常开发中常用配置文件有以下几种:Json格式字符串、K=V键值对、xml文件、yml文件、toml文件。

虽然说配置文件各种各样,但是总体处理步骤都大致相同:

  • 定义配置文件
  • 定义与配置文件相对应的结构体
  • 读取配置文件并且加载到相对应的结构体当中

这里使用yml配置文件连接Mysql数据库:

  1. 定义配置文件
# Mysql连接配置
database:
  type: mysql
  host: localhost
  port: 3306
  username: root
  password: 123456
  dbname: db_gorm
  max_idle_conn: 10
  max_open_conn: 100
  conn_max_lifetime: 300
  1. 定义与配置文件相对应的结构体

结构体需要与yml的层级关系相对应,并且加入yaml:"server"方便系统识别

var Database *database

type database struct {
    Type            string `yaml:"type"`
    Host            string `yaml:"host"`
    Port            string `yaml:"port"`
    UserName        string `yaml:"username"`
    Password        string `yaml:"password"`
    DbName          string `yaml:"dbname"`
    MaxIdleConn     int    `yaml:"max_idle_conn"`
    MaxOpenConn     int    `yaml:"max_open_conn"`
    ConnMaxLifetime int    `yaml:"conn_max_lifetime"`
}
  1. 读取配置文件,将配置文件信息映射到对应的结构体中

解析yml文件需要引入yml库:gopkg.in/yaml.v2

3. Gorm基础

3.1. Gorm模型

  1. 模型定义

模型是标准的结构体,由 Go 的基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成。简单来说,模型就是与数据库的表结构相对应的一个结构体,GORM中使用模型来实现与数据库表的映射关系,例如:

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	
}

模型是Gorm进行各种操作的基础,Gorm使用模型实现和数据库表的映射关系。建立模型和建表类似,模型建立的好坏对于数据库操作来说至关重要,模型的建立需要遵循Gorm的约定 。

3.2. Gorm约定

GORM 倾向于约定大于配置。默认情况下,GORM 使用 ID 字段作为表的主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用CreatedAtUpdatedAt字段追踪记录的创建、更新时间等。

遵循 GORM 已有的约定,可以减少您的配置和代码量。如果约定不符合您的需求,GORM 运行您自定义配置它们。

约定1:GORM 默认使用 ID 作为表的主键

type User struct {
  ID   string // 默认情况下,名为 `ID` 的字段会作为表的主键
  Name string	
}

如果不使用ID作为主键,可以通过标签 primaryKey 将其它字段设为主键。

// 将 `UUID` 设为主键
type Animal struct {
    ID     int64
    UUID   string `gorm:"primaryKey"`
    Name   string
    Age    int64
}

还可以将多个字段设为主键,以创建复合主键:

type Product struct {
    ID           string `gorm:"primaryKey"`
    LanguageCode string `gorm:"primaryKey"`
    Code         string
    Name         string
}

注意:默认情况下,整型 PrioritizedPrimaryField 启用了 AutoIncrement,要禁用它,您需要为整型字段关闭 autoIncrement

type Product struct {
    CategoryID uint64 `gorm:"primaryKey;autoIncrement:false"`
    TypeID     uint64 `gorm:"primaryKey;autoIncrement:false"`
}

约定2:复数表名

GORM 使用结构体名的 蛇形命名法 作为表名。对于结构体 User,根据约定,其表名为 users。

蛇形命名法:即用下划线将单词连接起来,例如:file_name

约定3:使用时间戳追踪( 纳秒、毫秒、秒、Time

GORM 约定使用 CreatedAtUpdatedAt 追踪记录的创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间,如果要使用不同名称的字段追踪记录的创建/更新时间,可以配置 autoCreateTimeautoUpdateTime 标签。

  1. CreateAt

对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间(秒)。

db.Create(&user) // 将 `CreatedAt` 设为当前时间

user2 := User{Name: "jinzhu", CreatedAt: time.Now()}
db.Create(&user2) // user2 的 `CreatedAt` 不会被修改

// 想要修改该值,您可以使用 `Update`
db.Model(&user).Update("CreatedAt", time.Now())
  1. UpdateAt

对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间(秒)。

db.Save(&user) // 将 `UpdatedAt` 设为当前时间

db.Model(&user).Update("name", "jinzhu") // 会将 `UpdatedAt` 设为当前时间

db.Model(&user).UpdateColumn("name", "jinzhu") // `UpdatedAt` 不会被修改

user2 := User{Name: "jinzhu", UpdatedAt: time.Now()}
db.Create(&user2) // 创建记录时,user2 的 `UpdatedAt` 不会被修改

user3 := User{Name: "jinzhu", UpdatedAt: time.Now()}
db.Save(&user3) // 更新世,user3 的 `UpdatedAt` 会修改为当前时间
  1. 时间戳格式

GORM 支持多种类型的时间追踪字段,如果您想要保存 UNIX(毫/纳)秒的时间戳,而不是 time,只需简单地将 time.Time 修改为 int 即可。

type User struct {
    CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
    UpdatedAt int       // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
    Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳填纳秒数充更新时间
    Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
    Created   int64 `gorm:"autoCreateTime"`      // 使用时间戳秒数填充创建时间
}

约定3:gorm.Model

gorm.Model是GORM中定义的结构体,其包括四个字段: ID、CreatedAt、UpdatedAt、DeletedAt,通常选择将Model对象嵌入到自己定义的结构体中,以包含这些字段。

// gorm.Model 的定义
type Model struct {
    ID        uint           `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}
  1. 对于匿名字段,GORM 会将其字段包含在父结构体中。
type User struct {
    gorm.Model  // 嵌入匿名结构体
    Name string
}
// 等效于
type User struct {
    ID        uint           `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
    Name string
}
  1. 对于正常的结构体字段,可以通过标签 embedded 将其嵌入。
type Author struct {
    Name  string
    Email string
}

type Blog struct {
    ID      int
    Author  Author `gorm:"embedded"`
    Upvotes int32
}
// 等效于
type Blog struct {
    ID    int64
    Name  string
    Email string
    Upvotes  int32
}

并且,可以使用标签 embeddedPrefix 来为数据库中的字段名添加前缀。

3.3. 字段标签

在声明模型时,可以为字段添加标签(相当于给表添加约束),GORM 支持以下标签: 标签名大小写不敏感,但建议使用驼峰风格。

标签名说明
column指定字段名
type指定字段类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes
size指定字段大小,如:size:256
primaryKey指定字段作为主键
unique指定字段唯一
default指定字段的默认值
precision指定字段的精度
scale指定字段大小
not null指定字段不能为空
autoIncrement指定字段自增
autoIncrementIncrement自动步长,控制连续记录之间的间隔
embedded嵌入字段
embeddedPrefix给嵌入字段添加前缀名
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪秒级时间戳,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪秒级时间戳,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex与 index 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略这个字段,-没有读/写权限,-:migration没有迁移权限,-: 所有没有读/写/迁移权限
comment迁移时为字段添加注释

标签使用示例如下:

type Student struct {
	Model     gorm.Model `gorm:"embedded;embeddedPrefix:model_"`
	Name      string     `gorm:"unique;not null"`
	Email     string     `gorm:"default:null"`
	Age       uint8      `gorm:"not null"`
	CreatedAt time.Time  `gorm:"comment:只允许创建;<-:create"`
	UpdatedAt time.Time  `gorm:"comment:只允许更新;<-:update"`
}

使用该模型创建表,表结构如下:

image.png

4. 建表

在数据库连接成功之后,我们可以直接使用GORM创建表,进行增删改查等操作。这些操作都以GORM定义的模型(对象)为基础,与数据库的表结构相对应。

建表会使用到的方法如下:

  • func (db *DB) AutoMigrate(dst ...interface{}) error
  • func (db *DB) Migrator() Migrator

4.1. 自动迁移建表

create_table.go

package gorm_mysql

import (
	"fmt"
	"gorm.io/gorm"
)

// 定义一个User结构体
type User struct {
	gorm.Model // 嵌入匿名结构体
	Name string
	Age  int
}

func CreateTable(db *gorm.DB) error {
	// 自动迁移键表(将结构体映射到数据库)
	err := db.AutoMigrate(&User{})
	if err != nil {
		fmt.Println("Create table failed:", err)
	}
	return err
}

main.go

package main

import (
	"fmt"
	"gorm_demo/src/gorm_mysql"
)

func main() {
	// 连接数据库
	db, err := gorm_mysql.ConnectDb()
	if err != nil {
		fmt.Println("Connect database failed:", err)
	}
	// 建表
	gorm_mysql.CreateTable(db)
}

运行main函数,刷新数据库:

数据库新增了一个与User结构体相对应的t_user表,这就是GORM的自动迁移建表。

4.2. Migrator接口

还可以通过Migrator接口手动进行建表,Migrator接口提供了很多方法,可以在建表时实现更多的操作。Migrator接口提供的方法如下:

  • CreateTable(&模型):创建表
  • HasTable:判断表是否存在
  • DropTable:删除表
  • RenameTable:重命名表

gorm.io/zh_CN/docs/…