GORM 核心功能的实现原理

GORM 是 Golang 在数据库操作上经常使用到的 ORM 库,相较于同类其他 ORM 库,该库支持的功能更丰富,更新也较为活跃。

本文是基于 GORM V2(版本号: v1.23.5),对核心功能的源码进行探究。

初始化

官方推荐的连接数据库方法是 gorm.Open,该方法接收至少两个参数。

1
2
3
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
// ...
}

Dialector 是一个接口类型,GORM 会调用这些接口方法,来构建对应数据库能运行的 SQL 语句:

1
2
3
4
5
6
7
8
9
10
type Dialector interface {
Name() string
Initialize(*DB) error
Migrator(db *DB) Migrator
DataTypeOf(*schema.Field) string
DefaultValueOf(*schema.Field) clause.Expression
BindVarTo(writer clause.Writer, stmt *Statement, v interface{})
QuoteTo(clause.Writer, string)
Explain(sql string, vars ...interface{}) string
}

由于不同的数据库都有自己的方言(Dialects),例如分页的语句,在 MySQL 中是 LIMIT 10,而 Oracle 则是 FETCH NEXT 10 ROWS ONLY 。因此不同的数据库需要实现自己的方法,通过相同的接口方法去构建出对应的 SQL 语句。这也是 ORM 库的主要功能之一,可以通过抽象代码兼容不同的数据库。

GORM 官方支持 SQLiteMySQLPostgresSQLServer,其他数据库可以自己实现,或使用其他开发者的开源库。

Dialector 主要部分是实现 Initialize 方法:

1
2
3
4
5
6
7
8
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {

callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{})
db.Callback().Create().Replace("gorm:create", Create)
db.Callback().Update().Replace("gorm:update", Update)

// ...
}

通常在该方法中,需要注册回调函数,GORM 在执行增删改查等操作过程中或过程前后,会调用这些回调函数,因此你也可以实现自己的方法去替换它们。

生成 SQL 语句

前面提到的 gorm.Open 方法会返回一个 gorm.DB 结构体指针:

1
2
3
4
5
6
7
type DB struct {
*Config
Error error
RowsAffected int64
Statement *Statement
clone int
}

这个结构体的实例经常用到,Table Select Where 等一系列链式方法都是这个结构体指针的方法。

实际上链式方法构成的 SQL 片段和参数,会被存储到 Statement 这个字段中,这个字段的结构体将贯穿整个链式调用的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Statement struct {
*DB
TableExpr *clause.Expr
Table string
Model interface{}
Unscoped bool
Dest interface{}
ReflectValue reflect.Value
Clauses map[string]clause.Clause
BuildClauses []string
Distinct bool
Selects []string // selected columns
Omits []string // omit columns
Joins []join
Preloads map[string][]interface{}
Settings sync.Map
ConnPool ConnPool
Schema *schema.Schema
Context context.Context
RaiseErrorOnNotFound bool
SkipHooks bool
SQL strings.Builder
Vars []interface{}
CurDestIndex int
attrs []interface{}
assigns []interface{}
scopes []func(*DB) *DB
}

可以看到里面包含了诸如 Table Selects Joins 等属性,例如 Select 方法中就是把查询的字段名存入 Selects 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB) {
tx = db.getInstance()

switch v := query.(type) {
case []string:
tx.Statement.Selects = v

for _, arg := range args {
switch arg := arg.(type) {
case string:
tx.Statement.Selects = append(tx.Statement.Selects, arg)
case []string:
tx.Statement.Selects = append(tx.Statement.Selects, arg...)
default:
tx.AddError(fmt.Errorf("unsupported select args %v %v", query, args))
return
}
}

if clause, ok := tx.Statement.Clauses["SELECT"]; ok {
clause.Expression = nil
tx.Statement.Clauses["SELECT"] = clause
}
// ...
}

return
}

而当调用 First Find Scan Create Update 等这些 Finisher 方法时,执行过程中就会把 Statement 存储的内容读取出来,构建出最终的 SQL 语句并被执行。

例如调用 Find 方法:

1
2
3
4
5
6
7
8
9
10
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
tx = db.getInstance()
if len(conds) > 0 {
if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: exprs})
}
}
tx.Statement.Dest = dest
return tx.callbacks.Query().Execute(tx)
}

其中最后的 tx.callbacks.Query() 是返回对应的查询处理器(query processor),而处理器的 Execute 方法中会对 Statement 进行最后的整理,如对参数进行预处理和校验。p.fns 是个列表,包含了一系列回调函数,即是之前初始化中通过 callbacks.RegisterDefaultCallbacks 方法注册的回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (p *processor) Execute(db *DB) *DB {
// ...
for _, f := range p.fns {
f(db)
}
// ...
}

// 默认注册的查询回调
func Query(db *gorm.DB) {
if db.Error == nil {
BuildQuerySQL(db)

if !db.DryRun && db.Error == nil {
rows, err := db.Statement.ConnPool.QueryContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
if err != nil {
db.AddError(err)
return
}
gorm.Scan(rows, db, 0)
db.AddError(rows.Close())
}
}
}

其中会通过 BuildQuerySQL 构建出查询 SQL 语句,然后通过连接池在数据库进行执行,并将结果通过 gorm.Scan 填充到前面 Find(dest) 中。

关联

GORM 支持了常见的数据表关联关系:一对一、一对多、多对一、多对多,这些关系会保存在如下的结构体实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Relationships struct {
HasOne []*Relationship
BelongsTo []*Relationship
HasMany []*Relationship
Many2Many []*Relationship
Relations map[string]*Relationship
}

type Relationship struct {
Name string
Type RelationshipType
Field *Field
Polymorphic *Polymorphic
References []*Reference
Schema *Schema
FieldSchema *Schema
JoinTable *Schema
foreignKeys, primaryKeys []string
}

GORM 在操作中会扫描 Modal 的嵌套结构体,以及 foreignkeyreferences 这些 Tag 去解析它们之间的关系,并构成 Relationship 进行保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (schema *Schema) parseRelation(field *Field) *Relationship {
// ...

if schema.err == nil {
schema.Relationships.Relations[relation.Name] = relation
switch relation.Type {
case HasOne:
schema.Relationships.HasOne = append(schema.Relationships.HasOne, relation)
case HasMany:
schema.Relationships.HasMany = append(schema.Relationships.HasMany, relation)
case BelongsTo:
schema.Relationships.BelongsTo = append(schema.Relationships.BelongsTo, relation)
case Many2Many:
schema.Relationships.Many2Many = append(schema.Relationships.Many2Many, relation)
}
}

// ...
}

在增删改查的回调中,都包含了对关联关系的处理,下面通过关联创建来举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
user := User{
Name: "jinzhu",
Emails: []Email{
{Email: "jinzhu@example.com"},
{Email: "jinzhu-2@example.com"},
},
}

db.Create(&user)
// BEGIN TRANSACTION;
// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com"), (111, "jinzhu-2@example.com") ON DUPLICATE KEY DO NOTHING;
// COMMIT;

该例中通过创建 User 会关联创建内嵌的 Email,可以看出这是个一对多的关系。其中 INSERT INTO "emails" 的 SQL 语句是通过 gorm:save_after_associations 注册的回调函数执行出来的,默认的回调会遍历 Relationships 所有的成员列表,然后将对应关联的 Model 对象进行保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func SaveAfterAssociations(create bool) func(db *gorm.DB) {
return func(db *gorm.DB) {
// ...

for _, rel := range db.Statement.Schema.Relationships.HasMany {
// ...

if elems.Len() > 0 {
assignmentColumns := make([]string, 0, len(rel.References))
for _, ref := range rel.References {
assignmentColumns = append(assignmentColumns, ref.ForeignKey.DBName)
}

saveAssociations(db, rel, elems, selectColumns, restricted, assignmentColumns)
}

// ...
}

// ...
}
}

func saveAssociations(db *gorm.DB, rel *schema.Relationship, rValues reflect.Value, selectColumns map[string]bool, restricted bool, defaultUpdatingColumns []string) error {
// ...
return db.AddError(tx.Create(values).Error)
}

事务

在事务方面常用的 Transaction 方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) {
panicked := true

if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {
// nested transaction
if !db.DisableNestedTransaction {
err = db.SavePoint(fmt.Sprintf("sp%p", fc)).Error
if err != nil {
return
}

defer func() {
// Make sure to rollback when panic, Block error or Commit error
if panicked || err != nil {
db.RollbackTo(fmt.Sprintf("sp%p", fc))
}
}()
}
err = fc(db.Session(&Session{NewDB: db.clone == 1}))
} else {
tx := db.Begin(opts...)
if tx.Error != nil {
return tx.Error
}

defer func() {
// Make sure to rollback when panic, Block error or Commit error
if panicked || err != nil {
tx.Rollback()
}
}()

if err = fc(tx); err == nil {
panicked = false
return tx.Commit().Error
}
}

panicked = false
return
}

可以看到实际就是封装了 BeginCommit 等方法,并且会先判断是否为内嵌事务对象,如果是则通过 SavePoint 来阶段性提交。

字段权限

GORM 内置了一些 Tag 来控制字段级别的权限,例如只读、只写、只创建、只更新或者被忽略:

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
Name string `gorm:"<-:create"` // allow read and create
Name string `gorm:"<-:update"` // allow read and update
Name string `gorm:"<-"` // allow read and write (create and update)
Name string `gorm:"<-:false"` // allow read, disable write permission
Name string `gorm:"->"` // readonly (disable write permission unless it configured)
Name string `gorm:"->;<-:create"` // allow read and create
Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
Name string `gorm:"-"` // ignore this field when write and read with struct
Name string `gorm:"-:all"` // ignore this field when write, read and migrate with struct
Name string `gorm:"-:migration"` // ignore this field when migrate with struct
}

这些 Tag 会在 Model 解析时跟随字段就一起被解析,信息被保存在 Schema 结构体内嵌的 Field 结构体中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type Schema struct {
Name string
ModelType reflect.Type
Table string
PrioritizedPrimaryField *Field
DBNames []string
PrimaryFields []*Field
PrimaryFieldDBNames []string
Fields []*Field
FieldsByName map[string]*Field
FieldsByDBName map[string]*Field
FieldsWithDefaultDBValue []*Field
Relationships Relationships
// ...
}

type Field struct {
Name string
DBName string
PrimaryKey bool
AutoIncrement bool
AutoIncrementIncrement int64
Creatable bool
Updatable bool
Readable bool
IgnoreMigration bool
HasDefaultValue bool
DefaultValue string
DefaultValueInterface interface{}
NotNull bool
Unique bool
Tag reflect.StructTag
TagSettings map[string]string
// ...
}

可以看到 Field 包含了 Creatable Updatable Readable IgnoreMigration 这些布尔类型的属性,它们会在解析过程中根据 Tag 被赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field {
// ...

if val, ok := field.TagSettings["-"]; ok {
val = strings.ToLower(strings.TrimSpace(val))
switch val {
case "-":
field.Creatable = false
field.Updatable = false
field.Readable = false
field.DataType = ""
case "all":
field.Creatable = false
field.Updatable = false
field.Readable = false
field.DataType = ""
field.IgnoreMigration = true
case "migration":
field.IgnoreMigration = true
}
}

if v, ok := field.TagSettings["->"]; ok {
field.Creatable = false
field.Updatable = false
if strings.ToLower(v) == "false" {
field.Readable = false
} else {
field.Readable = true
}
}

if v, ok := field.TagSettings["<-"]; ok {
field.Creatable = true
field.Updatable = true

if v != "<-" {
if !strings.Contains(v, "create") {
field.Creatable = false
}

if !strings.Contains(v, "update") {
field.Updatable = false
}
}
}

// ...
}

然后创建、更新等操作中会判断这些属性,达到控制字段权限的目的。例如 AutoMigrate 方法中会有建表的调用,如果某字段的 IgnoreMigrationtrue,则会跳过这个字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (m Migrator) CreateTable(values ...interface{}) error {
// ...

for _, dbName := range stmt.Schema.DBNames {
field := stmt.Schema.FieldsByDBName[dbName]
if !field.IgnoreMigration {
createTableSQL += "? ?"
hasPrimaryKeyInDataType = hasPrimaryKeyInDataType || strings.Contains(strings.ToUpper(string(field.DataType)), "PRIMARY KEY")
values = append(values, clause.Column{Name: dbName}, m.DB.Migrator().FullDataTypeOf(field))
createTableSQL += ","
}
}

// ...
}

GORM 核心功能的实现原理
https://zkqiang.cn/posts/aad5a67b/
作者
张凯强
发布于
2022年4月9日
许可协议