这是我参与「第三届青训营 -后端场」笔记创作活动的第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数据库作为演示:
- 新建一个db_gorm数据库
- 连接数据库
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)
}
上面只是最基本的数据库连接,并没有对数据库连接进行其他设置,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{})
- 使用连接池连接数据库
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数据库:
- 定义配置文件
# 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
- 定义与配置文件相对应的结构体
结构体需要与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"`
}
- 读取配置文件,将配置文件信息映射到对应的结构体中
解析yml文件需要引入yml库:gopkg.in/yaml.v2
3. Gorm基础
3.1. Gorm模型
- 模型定义
模型是标准的结构体,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成。简单来说,模型就是与数据库的表结构相对应的一个结构体,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 字段作为表的主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用CreatedAt、UpdatedAt字段追踪记录的创建、更新时间等。
遵循 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 约定使用 CreatedAt、UpdatedAt 追踪记录的创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间,如果要使用不同名称的字段追踪记录的创建/更新时间,可以配置 autoCreateTime、autoUpdateTime 标签。
- 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())
- 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` 会修改为当前时间
- 时间戳格式
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"`
}
- 对于匿名字段,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
}
- 对于正常的结构体字段,可以通过标签
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"`
}
使用该模型创建表,表结构如下:
4. 建表
在数据库连接成功之后,我们可以直接使用GORM创建表,进行增删改查等操作。这些操作都以GORM定义的模型(对象)为基础,与数据库的表结构相对应。
建表会使用到的方法如下:
func (db *DB) AutoMigrate(dst ...interface{}) errorfunc (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:重命名表