这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记。
《DATABASE/SQL 与 GORM 设计与实践》课程由张金柱老师讲授,根据张老师讲解的课程内容,我总结梳理出了如下笔记内容。
见微知著——课程重点一览
步步为营——知识点详细剖析
理解database / sql
对于目前的服务开发来说,数据库已然成为一个不可获取的一部分。而在众多的数据库当中,关系型数据库通常作为技术选型中的首要选择。为了能够实现通过一个统一的接口来访问不同的数据库的目标,database数据包应运而生。
基本用法
首先可以先通过一段代码来了解一下database数据包的用法。这段代码交代了database数据包使用的四个步骤:“如何建立一个连接”、“如何使用连接去查询数据”、“如何将数据Scan到对象中去”以及“如何去处理其中的一些错误”。
// database数据包的基本用法
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 {
// ...
}
}
如何建立一个连接
我们可以通过import导入driver,并通过driver+DSN的方式来实现数据库连接的初始化操作。
// 建立连接
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")
如何使用连接去查询数据
我们在执行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进行关闭时也会发生异常。因此,上面的代码需要在关闭连接时对异常进行捕获操作。
// 数据Scan到对象
var users []User
for rows.Next() {
var user User
err := rows.Scan(&user.ID, &user.Name)
if err != nil {
// ...
}
users = append(users, user)
}
// 关闭时异常获取
defer func(){
err = rows.Close()
}
怎么去处理其中的一些错误
最后可以统一地对数据查询过程中的错误进行处理。
// 异常处理
if rows.Err() != nil {
// ...
}
设计原理
下面看一下dabase数据包是怎么设计的。
database数据包采用极简接口设计原则:它对上层应用程序提供一个标准的api操作接口,并对下层驱动暴露简单的驱动接口。在database数据包的内部实现了连接池的管理。这意味着对于不同的数据库,只需实现一个相同的驱动连接接口、操作接口即可实现对不同数据库连接的支持。
连接池
数据库的连接池使用池化技术,它将昂贵费时的资源放到特定的池子里,同时维护最大连接数、最小连接数、阻塞队列等参数,方便管理整个池子,便于连接的复用。此外,它提供探活机制和监控功能,以提高查询性能。
database数据包提供了两类方法来管理连接池:
- 连接池配置:配置连接池参数
// 连接池配置
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)
- 连接池状态:管理连接池状态
// 连接池状态
func (db *DB) Stats() DBStats
下面一段代码展示了database数据包连接数据库的操作过程的伪实现,从该实现中可以发现数据库连接有两种策略:
- 尽量复用的策略:从连接池中获取连接并使用
- 新建新的连接:通过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{})
}
这个连接接口的设计看起来十分合理,但实则有几处小问题:
- DSN为字符串形式,不便于理解和相关处理
- 在使用时,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的操作接口主要在两个方面上进行设计:连接类型和处理返回数据方式。
-
在连接类型上分成了三种:直接连接(Conn)、预编译连接(Stmt)和事务(Tx)。其中,值得解释的是预编译连接方式,在该方式下,driver会先生成预编译语句,然后通过reference id来进行查询。采用这样的方式可以减少网络传输和解析sql语句的时间,从而提升系统的整体性能。
-
在数据处理方法上也划分为三个方法:
- Exec / ExecContext -> Result方法:只关心是否成功,返回成功失败信息
- Query / QueryContext -> Rows方法:通过行的形式返回数据,需要手动close
- QueryRow / QueryRowContext -> Row方法:简化版Rows方法
GORM使用简介
背景知识
GORM是“设计简洁、功能强大、可自由扩展的全功能ORM”。它本着“API精简”、“测试优先”、“最小惊讶”、“灵活扩展”、“无依赖”、“可信赖”的设计原则,提供了如下多种完善的功能:
- 关联:一对一、一对多、单表自关联、多态;Preload、Joins预加载、级联删除;关联模式;自定义关联表
- 事务:事务代码块、嵌套事务、Save Point
- 多数据库、读写分离、命名参数、Map、子查询、分组条件、代码共享、SQL表达式(查询、创建、更新)、自动选字段、查询优化器
- 字段权限、软删除、批量数据处理、Prepared Stmt、自定义类型、命名策略、虚拟字段、自动track时间、SQL Builder、Logger
- 代码生成、复合主键、Constraint、Prometheus、Auto Migration、真·跨数据库兼容
- 多模式灵活自由扩展
- 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
- 创建场景
// 创建场景
// 创建
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
}
- 查询场景
//查询场景
// 读取
var product Product
db.First(&product, 1) // 查询id为1的product
db.First(&product, "code = ?", "L1212") // 查询code为L1212的product
- 更新场景
//更新场景
// 更新某个字段
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})
- 删除场景
//删除场景
//删除
db.Delete(&product)
模型定义
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
DeletedAt gorm.DeletedAt `gorm:"index"`
}
- 嵌套结构
// 嵌套结构
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中做了如下约定:
- 表名为struct name的snake_cases复数格式
- 字段名为field name的snake_cases单数格式
- ID/id字段为主键,如果为数字,则为自增主键
- CreatedAt字段,创建时,保存当前时间
- UpdatedAt字段,创建、更新时,保存当前时间
- 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主要关注“如何保存关联”、“如何利用关联模式管理数据”和“如何支持批量操作”:
- 如何保存关联
// 如何保存关联
// 保存用户及其关联
db.Save(&User{
Name: "jinzhu",
Languages: []Language{{Name: "zh-CN"}, {Name: "en-US"}},
})
- 利用关联模式管理数据
// 利用关联模式管理数据
//关联模式
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)
GORM设计原理
在讲解GORM设计原理之前,我们先抛出一个问题:“如何通过一行配置迅速提升服务的性能?”关于这个问题,在学习完GORM设计原理之后,便可以给出清晰的解答。
GORM其实相当于是在database数据包之上再加了一层,它负责与应用程序进行交互。下面我们就来看一下GORM具体是如何进行工作的。
SQL是怎么生成的
既然GORM介于用用程序和database数据包之间,那么它就要做好二者的“沟通”。所以,GORM要负责生成正确的SQL来给到对应的数据库,让数据库进行查询。下面就先来看一下GORM是怎么生成SQL语句的。
一条SQL语句由多条子句组成,一部分子句又由多个表达式构成。SQL语句和子句、表达式呈现着下图所示的结构关系:
对于一条GORM语句来说,它由多条Chain Method和一条Finisher Methord组成:
- 在Chain Method中可以添加gorm子句,用于生成最终的SQL语句
- Finisher Methord决定着数据最终的类型并且去执行语句
GORM语句参考着SQL语句进行仿生设计,同时对SQL进行了更好的扩展。
// GORM API方法添加Clauses至GORM Statement
// Where add conditions
func (db *DB) Where(query interface{}, args ... interface{}) (tx *DB){
tx = db.getInstance()
if conds := tx.Statement.BuildCondition(query, args ...); len(conds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: conds})
}
return
}
//Limit specify the number of records to be retrieved
func (db *DB) Limit(limit int) (tx *DB) {
tx = db.getInstance()
tx.Statement.AddClause(clause.Limit{Limit: limit})
return
}
// GORM Finisher 方法执行 GORM Statement
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.getInstance()
tx.Statement.Dest = dest
return tx.callbacks.Query().Execute(tx)
}
//callbacks.go
func (p *processor) Execute(db *DB) *DB {
stmt := db.Statement
stmt.BuildClauses = []string{"SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "LIMIT"}
//callbacks/query.go
stmt.Build(stmt.BuildClause...)
stmt.ConnPool.Exec(stmt.SQL.String(), stmt.Vars...)
}
下面我们来具体分析一下,GORM为什么要这么设计。这种设计方式主要有三个方面原因:
- 方便自定义Clause Builder
由于不同数据库和相同数据库的不同版本所支持的SQL不同不尽相同,为了兼容所有的SQL语句,就有必要将这些不同点给隐藏起来,实现一套统一的GORM代码。这样就使得代码可以灵活扩展,便于对不同数据库提供支持。
// 方便自定义Clause Builder
// SELECT * FROM `users` LOCK IN SHARE MODE // MySQL < 8 , MariaDB
// SELECT * FROM `users` FOR SHARE OF `users` // MySQL 8
db.Clauses(clause.Locking{
Strength: "SHARE",
Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
- 方便扩展子句
在编译生成子句的时候,GORM可以提供前缀或后缀扩展性接口,只需注册一个必要的接口就可以实现。
// 方便扩展子句
import "gorm.io/hints"
//扩展SELECT Clause 后
db.Clauses(hints.New("MRR(idx1)")).Find(&User{})
//SELECT /*+ MRR(idx1) */ * FROM `users`
//扩展FROM Clause 后
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
//SELECT * FROM `users` USE INDEX (`idx_user_name`)
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
//SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`, `idx_user_id`)
//自由扩展Clause前中后
db.Clauses(hints.Comment("select", "master")).Find(&User{})
//SELECT /* master */ * FROM `users`;
db.Clauses(hints.CommentBefore("insert", "node2")).Create(&user)
// /* node2 */ INSTER INTO `users` ...;
db.Clauses(hints.CommentAfter("where", "hint")).Find(&User{}, "id = ?", 1)
//SELECT * FROM `users` WHERE id = ? /* hint */
我们在select后加入评论,通过评论可以注册一个查询优化器,生成一个带有查询优化器的SQL,这样就可以进行SQL的查询优化操作。
我们也可以扩展from操作,通过指定索引来加速查询操作。
此外,GORM还可以自由地扩展子句的前中后操作。
- 自由选择子句
GORM的最终目的是:隐藏掉不同数据库间的差异,在不改变代码的前提下去灵活扩展,支持各种数据库,减少迁移成本,只需写一回代码就足够了。
此外,GORM通过自由选择子句的模式在功能上添加一些支持,使得不同数据库实现driver的时候允许它自定义一些不同子句的类型,在后面生成不一样的SQL语句,再配合前面所说的必要的形式,便可以生成出任意的SQL语句。
这样就实现了在不改变任何代码的情况下,可以轻松地兼容所有的数据库,并且不需要承担使用新的技术所带来的成本和义务。
插件是怎么工作的
Finisher Method在生成SQL语句的过程中会经历GORM中的插件系统,下面就来看一下GORM中的插件系统是如何工作的。
Finisher Method首先会去检查Statement代表的类型,根据对应的类型取出Statement对应了哪些Callback。之后,会根据这些Callback生成对应的SQL语句,最后再把它传递给数据库进行最终的执行,待数据库返回数据再递交给应用程序返回给系统。
GORM中的Callback形式一共有6种:
- Create:默认定制了7个Callback
// 预定义CREATE CALLBACKS
//开启事务
db.Callback().Create().Register("gorm:begin_transaction", BeginTransaction)
//取出create前有哪些hooks方法,然后把这些方法进行执行,返回结果
//若有错误回滚事务,保存前置关联
db.Callback().Create().Register("gorm:before_create", BeforeCreate)
db.Callback().Create().Register("gorm:save_before_associations",SaveBeforeAssociations)
db.Callback().Create().Register("gorm:create", Create)
//后置关联,外键依赖前面返回值
db.Callback().Create().Register("gorm:after", AfterCreate)
db.Callback().Create().Register("gorm:save_after_associations", SaveAfterAssociations)
//判断有没有错误,commit和rollback
db.Callback().Create().Register("gorm:commit_or_rollback_transaction",CommitOrRollback)
// 执行Create的过程,依次调用注册的Create Callbacks
// 创建对象
db.Create(&Product{Code: "L1212", Price: 1000})
//GORM找出Create所注册的所有方法,并一一调用
func Create(data interface{}) error {
// ...伪代码
for _, f := range db.callbacks.creates {
f()
}
}
//注册新Callback
db.Callback().Create().Register("myplugin", func(*gorm.DB) {})
//删除Callback
db.Callback().Create().Remove("gorm:begin_transaction")
//替换Callback
db.Callback().Create().Replace("gorm:before_create", func(*gorm.DB) {})
//查询注册的Callback
db.Callback().Create().Get("gorm:create")
//指定Callback顺序
db.Callback().Create().Before("gorm:create").After("myplugin").Register("myplugin2", func(*gorm.DB) {})
//注册到所有服务之前
db.Callback().Create().Before("*").Register("my_plugin:new_callback", func(*gorm.DB) {})
//注册时检查条件
enableTransaction := func(db *gorm.DB) bool { return !db.SkipDefaultTransaction }
db.Callback().Create().Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
- Query
- Update
- Delete
- Row
- Raw
同样地,下面说一下插件系统为什么这么设计。
之所以这样设计是为了完成“灵活定制和自由扩展”的目标,从而满足“多租户”、“多数据库、读写分离”、“加解密、压力测试、混沌工程”等功能。
- 多租户
我们的系统中会有很多租户,我们不希望把所有的租户信息缓存到一起,希望不同租户的数据存在隔离的关系,在查询的过程中需要加入AppID等过滤的条件。我们期望在查询的过程当中能够自动地加上这些条件,而不是每次调用方法都手工指定,因为一旦手工指定条件,那必然会在某些地方漏掉某些条件,导致数据的混乱。
// 多租户
// 根据TenantID过滤
func setTenantScope(db *gorm.DB) {
if tenantID, err := getTenantID(db.Statement.Context); err != nil {
db.Where("tenant_id = ?", tentantID)
} else {
db.AddError(err)
}
}
db.Callback.Query().Before("gorm:query").Register("set_tenant_scope", setTenantScope)
db.Callback.Delete().Before("gorm:delete").Register("set_tenant_scope", setTenantScope)
db.Callback.Update().Before("gorm:update").Register("set_tenant_scope", setTenantScope)
//设置 TenantID
func setTenantID(db *gorm.DB) {
tenantID, err := getTenantID(db.Statement.Context)
db.Statement.SetColumn("tenant_id", tenantID)
// ...
}
db.Callback.Create().Before("gorm:create").Register("set_tenant_id", setTenantID)
GORM会从当前系统里取出当前租户的ID,通过Where条件把当前租户ID更新到当前的GORM Statement中,这样后续的查询条件都会加上这层过滤条件。
- 多数据库、读写分离
// 多数据库、读写分离
DB.Use(dbresolver.Register(dbresolver.Config{
// `db2`作为sources,`db3`、`db4`作为replicas
Sources: []gorm.Dialector{ mysql.Open("db2_dsn") },
Replicas: []gorm.Dialector{ mysql.Open("db3_dsn"), mysql.Open("db4_dsn") },
//sources / replicas 负载均衡策略
Policy: dbresolver.RandomPolicy{},
}).Register(dbresolver.Config{
// `db1`作为sources(DB的默认连接),对于`User`、`Address`使用`db5`作为replicas
Replicas: []gorm.Dialector{ mysql.Open("db5_dsn") },
}, &User{}, &Address{}).Register(dbresolver.Config{
//`db6`、`db7`作为sources,对于`orders`、`Product`使用`db8`作为replicas
Sources: []gorm.Dialector{ mysql.Open("db6_dsn"), mysql.Open("db7_dsn") },
Replicas: []gorm.Dialector{ mysql.Open("db8_dsn")},
}, "orders", &Product{}, "secondary"))
//使用Write模式,从`sources` db `db1` 读取user
DB.Clause(dbresolver.Write).First(&user)
//指定Resolver,从`secondary` 的 replicas db `db8` 读取user
DB.Clause(dbresolver.Use("secondary")).First(&user)
//指定Resolver和Write模式,从`secondary` 的 sources db `db6` 或 `db7` 读取user
DB.Clause(dbresolver.Use("secondary"), dbresolver.Write).First(&user)
- 加解密、压力测试、混沌工程等
ConnPool是什么
GORM看起来是在和DB Conn进行一个交互,但实际上并不是这样的一个简单的结构。
go语言十分推崇接口,因此这里重新定义了一个ConnPool的interface,并由DB Conn去实现这样一个interface。实际上,GORM是通过ConnPool接口与数据库进行连接,从而实现最终的一个数据间的交流。
基于这样的一个设计,GORM在读写分离上的存在着优势。对于所有的写数据库去做写数据库的接口实现;对于所有的读数据库去做都数据库的接口实现。这样就从真正意义上实现了数据库的读写分离。
另外,GORM还支持事务前选择开启事务的方式,利用相应的开启事务方式来对数据库进行连接。
下面举几个ConnPool应用中的例子来帮助理解一下。
- 预编译例子:
所有的SQL语句都会交给ConnPool执行一个预编译,GORM会将这些预编译的语句缓存起来。当后面再执行一些同类SQL语句时,就会使用这些预编译的SQL语句,根据它们的referenceID加上参数来执行。这样,同样的数据只需要进行一次解析即可,大大提升了整体的执行效率。
预编译模式有两种:一种是全局模式、另一种是会话模式。
- 全局模式:所有DB操作都会预编译并缓存(缓存不含参数部分),全局缓存的语句可被会话使用
//全局模式
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
db.First(&user, 1)
- 会话模式:后续会话的操作都会预编译并缓存
//会话模式
tx := db.Session(&Session{PrepareStmt: true})
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
tx.First(&user, 2)
stmtManger, ok := tx.ConnPool.(*PreparedStmtDB)
//关闭当前会话的预编译语句
stmtManger.Close()
在预编译模式下,SQL语句的执行被简化为了三个步骤:
- 查找缓存的预编译SQL
- 未找到,将收到的SQL和Vars预编译
- 使用缓存的预编译SQL执行
- 数据安全的例子
某海外国家重视数据安全,所有数据都不能直接存储到国外的数据中心。借助ConnPool的可扩展能力可以写一个多访问的业务插件,通过插件来实现将数据先发送到该国的数据库,最后再把数据同步到原来的数据库,这样对原来的业务代码是透明的,只需配置相关参数即可完成业务需求。这是一个典型的抽象数据操作逻辑和对数据的执行逻辑来实现业务需求的例子。
- API查询的替换
此外,我们还可以利用GORM来构建interface实现一个缓存插件,从而实现API查询的替换。
现在,我们可以尝试着回答一开始我们所提出的那个问题了。我们在讲解GORM原理之前,提出了一个问题:“如何通过一行配置迅速提升服务的性能?”。在这里,我们给出答案,就是在配置参数中添加interpolateParams=false的配置。
interpolateParams参数的存在原本是为了解决数据注入问题的,但该参数仅在多编码场景下才生效。当interpolateParams参数配置为true时,在连接数据库时,会先后经过“执行前预编译SQL”、“调用预编译的SQL”和“关闭预编译的SQL”三个操作。由于凭空多了三个操作,自然会使得查询效率降低。然而,目前软件开发在编码上基本已经做到了统一,没有必要再使用interpolateParams参数来控制多编码场景下的数据注入问题。因此,只需将该参数配置为false即可大大提升服务的性能。
//bytedgorm
import(
"code.byted.org/gorm/bytedgorm"
"gorm.io/gorm"
)
//初始化
DB, err := gorm.Open(
//psm 的格式为 p.s.m 无需 _write, _read 等后缀,dbname为数据库名
bytedgorm.MySQL("p.s.m"/*数据库PSM*/,"dbname"/*数据库名*/).WithReadReplicas(),
bytedgorm.WithDefaults(),
)
为了统一最优配置,而不是将精力放在调参上,字节提供了bytedgorm,可将用户不需要了解的逻辑封装起来来提供一个最佳实现
Dialector
最后简单介绍一下什么时Dialector。Dialector定义了GORM如何去定义一个利用数据来查询后端的一个接口。它提供了多种定制功能:
- 定制SQL生成
- 定制GORM插件
- 定制ConnPool
- 定制企业特性逻辑
// Dialector
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
import "gorm.io/driver/mysql"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
import "gorm.io/driver/postgres"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
import "gorm.io/driver/clickhouse"
db, err := gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
import "xxx.io/caches"
db, err := gorm.Open(caches.New(caches.Config{
Fallback: mysql.Open(dsn),
Store: lru.New(lru.Config{}),
}), &gorm.Config{})
此外,Dialector还提供了另一个接口——Option。Option内提供了两部分方法,一部分是初始化db之前调用的Apply方法,主要用于修改配置文件;另一部分是在初始化后期调用的AfterInitialize方法,主要用于初始化后期调用db所有的API。
//Option
type Option interface {
Apply(*Config) error
AferInitialize(*DB) error
}
GORM最佳实践
数据序列化与SQL表达式
不同数据库的表达式间的统一性兼容的不是很好,各种SQL语句方法又有很多不一致的地方。为了支持所有情况,GORM提供了统一的表达式支持。
- 更新创建
//更新创建
//方法1:通过gorm.Expr使用SQL表达式
db.Model(User{}).Create(map[string]interface{}{
"Name": "jinzhu",
"Location": gorm.Expr("ST_PointFromText(?)", "POINT(100 100)"),
})
//INSERT INTO "user with points" ("name", "location") VALUES ("jinzhu", ST_PointFormText("POINT(100 100)"));
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
//方法2:使用GORMValues使用SQL表达式 / SubQuery
type Location struct {
X, Y int
}
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return gorm.Expr("ST_PointFormText(?)", fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y))
}
db.Create(User{Name: "jinzhu", Location: Location{X: 100, Y: 100}})
db.Model(&User{ID: 1}).Updates(User{Name: "jinzhu", Location: Location{X: 100, Y: 100}})
//方法3:通过 *gorm.DB 使用 SubQuery
subQuery := db.Model(&Company[]).Select("name").Where("companies.id = users.company_id")
db.Model(&user).Updates(map[string]interface{}{"company_name": subQuery})
//UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE companies.id = users.company_id);
- 表达式查询
//表达式查询
//方法1:使用gorm.Expr
db.Where("location = ?", gorm.Expr("ST_PointFromText(?)", "POINT(100 100)")).First(&user)
//SELECT * FROM `users` WHERE `location` = ST_PointFromText("POINT(100 100)");
//方法2:Struct 定义 GormValuer
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return gorm.Expr("ST_PointFromText(?)", fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y))
}
db.Where("location = ?", Location{X: 100, Y:100}).First(&user)
//SELECT * FROM `users` WHERE `location` = ST_PointFromText("POINT(100 100)");
//方法3:自定义查询SQL实现接口clause.Expression
type Expression interface {
Build(builder Builder)
}
db.Find(&user, datatypes.JSONQuery("attributes").HashKey("role"))
db.Clauses(datatypes.JSONQuery("attributes").HasKey("org", "name")).Find(&user)
//方法4:SubQuery
db.Where("name in (?)", db.Model(&User{}).Select("name").Where("id > 10")).Find(&user)
- 数据序列化
// 数据序列化
type User struct {
Name []byte `gorm:"serializer:json"`
Roles Roles `gorm:"serializer:json"`
Contracts map[string]interface{} `gorm:"serializer:gob"`
JobInfo Job `gorm:"type:bytes;serializer:gob"`
CreatedTime int64 `gorm:"serializer:unixtime;type:time"`
Password Password
Attributes datatypes.JSON
}
//自定义数据格式实现接口 Scanner, Value
func (j JSON) Value() (driver.Value, error) { /** **/ }
func (j *JSON) Scan(value, interface{}) error { /** **/ }
//自定义数据格式实现 Serializer 接口
type Password string
type SerializerInterface interface {
Scan(context.Context, f *schema.Field, dst reflect.Value, dbValue interface{}) errror
Value(context.Context, f *schema.Field, dst reflect.Value, fieldV interface{}) (interface{}, errror)
}
批量数据操作
- 批量创建与查询
//批量创建、查询
//批量创建
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
db.CreateInBatches(users, 100)
for _, user := range users {
user.ID // 1,2,3
}
//批量查询
rows, err := db.Model(&User{}).Where("role = ?", "admin").Rows()
for rows.Next(){
// 方法1:sql.Rows.Scan
rows.Scan(&name, &age)
// 方法2:gorm.ScanRows
db.ScanRows(rows, &user)
// xxx
}
DB.Where("role = ?", "admin").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error{})
从上面的代码可以看出,批量创建有两种方式:
- 一种是直接传入数组进行创建:db.Create(&users)
- 另一种是指定一次创建的条目数:db.CreateInBatch(users, 100),这代表每100条创建一次
另外,批量查询也给出了两种操作方法,其中采用第二种方法可以有效避免OOM,也可以每100条进行一次查询,从而防止OOM。
- 批量更新
//批量更新
//忽略数据冲突
db.Clauses(clauses.OnConflict{DoNothing: true}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY DO NOTHING; // PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `id`=`id`; //MySQL
db.Clauses(clauses.Insert{Modifier: "IGNORE"}).Create(&users)
//INSERT IGNORE INTO `users` ***; //MySQL
//数据冲突时忽略某些字段
db.Clauses(clauses.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"deleted_at": nil}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `deleted_at`=NULL;
db.Clauses(clauses.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users) //使用SQL表达式更新
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`= GREATEST(count, VALUES(count));
//数据冲突时更新某些字段为新值
db.Clauses(clauses.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments([]string{"name", "age"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`= VALUES(name), `age`= VALUES(age);
//数据冲突时更新全部字段(除主键)为新值
db.Clauses(clauses.OnConflict{UpdateAll: true}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`= VALUES(name), `age`= VALUES(age), ...;
- 批量数据加速操作
//批量数据加速操作
//方法1:关闭默认事务
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
db.Create(&user)
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.Create(&user)
//方法2:默认批量导入会调用Hooks方法,使用`SkipHooks`跳过
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(&users, 1000)
//方法3:使用Prepared Statement
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
db.Create(&users)
//方法4:混合使用
tx := db.Session(&Session{
PrepareStmt: true , SkipDefaultTransaction: true, SkipHooks: true, CreateBatchSize : 1000,
})
tx.Create(&user)
代码复用、分库分表、Sharding
- 代码复用
// 代码复用
func Paginate(r *http.Request) func(db *gorm.DB) *gorm.DB {
return func (db *gorm.DB) *gorm.DB {
page, _ := strconv.Atoi(r.Query("page"))
if page == 0 {
page = 1
}
pageSize, _ := strconvAtoi(r.Query("page_size"))
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
//共享代码
db.Scopes(Paginate(r)).Find(&users)
db.Scopes(Paginate(r)).Find(&articles)
- 分库分表
// 分库分表
//使用传入数据进行分表
func TableOfYear(user *User, year int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
tableName := user.TableName() + strconv.Itoa(year)
return db.Table(tableName)
}
}
DB.Scopes(TableOfYear(user, 2019)).Find(&users)
//SELECT * FROM users_2019;
//使用传入数据进行分库(同一个连接)
func TableOfOrg(user *User, dbName string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
tableName := dbName + "." + user.TableName()
return db.Table(tableName)
}
}
DB.Scopes(TableOfOrg(user, "org1")).Find(&users)
//SELECT * FROM org1.users;
//使用对象信息获取表名 / interface
func TableOf User(user *User) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
year := getYearInfoFromUserID(user.ID)
return db.Table(user.TableName() + strconv.Itoa(year))
}
}
- Sharding
//Sharding
import "gorm.io/sharding"
db.Use(sharding.Register(sharding.Config{
ShardingKey: "user_id",
NumberOfShards: 64,
PrimaryKeyGenerator: sharding.PKSnowflake,
}, "orders").Register(sharding.Config{
ShardingKey: "user_id",
NumberOfShards: 256,
PrimaryKeyGenerator: sharding.PKSnowflake,
}, Notification{}, AuditLog{}))
db.Create(&Order{UserID: 2})
//INSERT INTO orders_2 ...
db.Exec("INSERT INTO orders(user_id) VALUES(?)", int64(3))
//INSERT INTO orders_3 ...
混沌工程/压测
- 人为创建错误场景检查系统状态
// 混沌工程
import (
"example.com/gorm/sqlchaos"
"gorm.io/gorm"
)
db, err := gorm.Open(
mysql.Open(dsn),
&gorm.Config{},
sqlchaos.WithChaos(sqlchaos.Config{
PSM: "service name"
DBName: "dbname"
EnvList: []string{"ppe", "boe"}, //演练环境
}),
)
db.Create(&User{ID: 1024, Name: "rick", Result: 10})
// INSERT INTO `table` (`id`, `user`, `result`) VALUES (1024, rick, 10)
Sqlchaos 篡改为
// INSERT INTO `table` (`id`, `user`, `result`) VALUES (1024, morty, 100)
- 性能压测:设置context
// 压测
DB.WithContext(ctx).Where(User{Name: "jinzhu"}).Update("age", 20)
// UPDATE `users` SET `age`=20 WHERE `users`.`name` = `jinzhu` //正常
// UPDATE `users_stress` SET `age`=20 WHERE `users_stress `.`name` = `jinzhu` //压测
func SetDBMiddleware(next http.Handler) http.Handle {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
timeoutContext, _ := context.WithTimeout(context.Background(), time.Second)
ctx := context.WithValue(r.Context(), "DB", db.WithContext(timeoutContext))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
r := chi.NewRouter()
r.Use(SetDBMiddleware)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
db, ok := ctx.Value("DB").(*gorm.DB)
// ...
})
Logger/Trace
链路追踪与扩展打印日志内容,其中包括全局模式和会话模式。
// Logger / Trace
// Interface logger interface
type Interface interface {
LogMode(LogLevel) Interface
Info(context.Context, string, ...interface{})
Warn(context.Context, string, ...interface{})
Error(context.Context, string, ...interface{})
Trace(ctx context.Context, begin timr.Time, fc func() (sql string, rowsAffected int64), err error)
}
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, //Slow SQL threshold
LogLevel: logger.Silent, //Log level
IgnoreRecordNotFoundError: true, //Ignore ErrRecordNotFound error for logger
Colorful: false, //Disable color
},
)
//全局模式
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{Logger: newLogger})
//会话模式
tx := db.Session(&Session{Logger: newLogger})
Migrator
- 自动迁移数据库(不建议使用)
//自动迁移数据库
db.AutoMigrate(&User{})
- 版本管理数据库
//版本管理数据库
import "github.com/go-gormigrate/gormigrate/v2"
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration){
{
ID: "201608301400",
Migrate: func(tx *gorm.DB) error {
type User struct {
Name string
}
return tx.AddColumn(&User{}, "Name")
},
Rollback: func(tx *gorm.DB) error {
return tx.DropColumn(&User{}, "Name")
},
},
})
m.Migrate()
Gen代码生成 / Raw SQL
- 原生SQL
// 原生SQL
db.Raw("SELECT id, name, age FROM users WHERE name = ?", "jinzhu").Scan(&result)
db.Raw("SELECT id, name, age FROM users WHERE name = @name", "jinzhu").Scan(&result)
db.Exec("UPDATE orders SET shipped_at=? WHERE id IN ?", time.Now(), []int64{1,2,3})
//Row & Rows
//使用GORM API构建SQL
row := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()
//SELECT name, age FROM users WHERE name = "jinzhu"
row.Scan(&name, &age)
//使用Named Argument作为Where条件
DB.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
DB.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"})).Find(&user)
DB.Where("name1 = @name OR name2 = @name", User{Name: "jinzhu"}).Find(&user)
//SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu"
//命名参数和原生SQL
DB.Raw(
"SELECT * FROM users WHERE name1 = @name OR name2 = @name2 OR name3 = @name", sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")
).Find(&user)
//SELECT * FROM users WHERE name1 = "jinzhu1" OR name2 = "jinzhu2" OR name3 = "jinzhu1"
- 代码生成——防止出现错误,生成安全代码
// Gen
type Query interface {
// SELECT * FROM @@table
// {{where}}
// {{if id > 0}} id = @id {{end}}
// {{if name != ""}} AND name= @name {{end}}
// {{if age > 18}} age > @age {{end}}
// {{end}}
FindUser(id int32, name string, age int)(gen.T err)
}
//生成自定义SQL静态代码
g := gen.NewGenerator(gen.Config{
OutPath: "../dal/query",
})
g.ApplyBasic(model.User{}) //生成对应的CRUD API
g.ApplyInterface(func(query method.Query) {}, model.User{}) // 给User 生成 Query Interface方法
g.Execute()
//查询数据
user, err := query.User.FindUser(10, "jinzhu", 16)
//SELECT * FROM users WHERE id = 10 AND name = "jinzhu"
安全问题
- 安全--以参数传入
//以参数传入
db.Create(User{Name: userInput})
db.Model(user).Update("name", userInput)
db.Where(User{Name: userInput}).First(&user)
db.Where("name = ?", userInput}).First(&user)
- 危险--直接拼接 SQL注入
//SQL注入
sql := fmt.Sprintf("name = %v", userInput)
db.Where(sql).First(&user)
db.Select("name;drop table users;").First(&user)
db.Distinct("name;drop table users;").First(&user)
db.Model(&user).Pluck("name;drop table users;", &names)
db.Group("name;drop table users;").First(&user)
db.Group("name").Having("1 = 1;drop table users;").First(&user)
db.Row("select name from users; drop table users;").First(&user)
db.Exec("select name from users; drop table users;")
小试牛刀——课后实践
作业描述
首先实现一个脚本工具:包含一个 User struct, 只包含 UUID string, Name string,Age int,Version int 四个字段,在脚本中使用 gorm + mysql 初始化 DB, 并使用初始化后的 DB 的 AutoMigrate 迁移数据表。
然后完成一个 Gen (github.com/go-gorm/gen/) 项目,基于上面创建的数据库及表名,通过 Gen 的自动同步库表功能生成 struct People,并给该 struct 生成基本 CRUD 方法,基于 OnConflict Upsert 功能实现 100 个随机用户的创建,其中需要包含重复的 UUID 用户的 Upsert, 在 Upsert 时,如果遇到重复 UUID 中,需要将 Version 更新为 Version + 1。最后再通过一条自定义的Raw SQL 实现,将数据按 Version 分组,并取出 Version 最高的一组的用户总数的功能,该 Raw SQL 需要通过自定义查询方法的形式实现,需要给 People 生成相应的方法名: GetMaxVersionCount。完成后提交代码到 github
温故知新——总结与感悟
尽管课程的过程中出现了一些小插曲,但丝毫没有影响到张金柱老师精彩的分享,整堂课下来我收获满满!
金柱老师先为我们介绍了传统的database包的使用方式,并指出了其中的一些使用上存在的一些问题。同时,在讲课的过程当中不断地引导着我们进行思考,引发着我们的学习兴趣。之后,金柱老师由浅入深地介绍了GORM的使用方法以及设计原理,让我们对GORM有了一个全方位的认识。最后,金柱老师为我们提供了一些GORM的实践上的例子,帮助我们进行理解。
通过学习这门课,我对GORM有了一定的了解,但仍然需要多加练习才能够熟练地使用GORM来提升开发的效率。之后,我将通过抖音项目的开发来熟悉GORM的使用,争取早日掌握GORM。