go-gorm源码分析(一)
October 15, 2024
最近在查找一些golang版本的SQL Builder的选型,刚好了解到了gorm的能力。gorm内嵌的SQL拼接能力其实不太适合作为独立特性来使用,其内部实现与gorm有较深的耦合关系。但是gorm的关系操作能力很强大,猜测跟directus中的实现有些类似。刚好之前阅读directus源码时觉得ts代码有些繁杂晦涩,所以阅读一遍gorm的源码,并记录其分析过程。
源码内容较大,相关分析会分几批来进行。
简介
GORM是golang下的ORM库,主要能力包括:
- ORM能力:即通过golang中的object(常见为struct)能够与DB表中的记录一一映射,通过操作对象打到快速读写DB的能力。
- 关联关系:实现了BelongTo(M2O)、HasMany(O2M)、HasOne(O2O)、ManyToMany(M2M)等常见的关系。即通过一次API调用,实现关联表的相关读写操作,避免在业务逻辑中维护关联关系。
- Create,Save,Update,Delete,Find 中钩子方法:可定义struct的各种操作hook,在DB操作前后对写入、读取的数据进行格式化等处理。
- 内嵌SQL builder,支持select、join、clause设置等。
更多特性以及用法见官方文档。
准备
在准备分析之前,需要准备一些测试代码,用官方的demo就可以,在本地使用docker搭建mysql环境,然后直接运行相关代码。注意点:
- 开启日志
通过配置开启gorm的日志,可以看到其内部的一些输出。除了错误信息外(默认会开启),目前仅有执行SQL语句的输出,同时也包括其执行耗时、rowsAffected等明细数据。
// callback execute结尾有输出相关API调用对应的SQL,以及耗时等信息。
if stmt.SQL.Len() > 0 {
db.Logger.Trace(stmt.Context, curTime, func() (string, int64) {
sql, vars := stmt.SQL.String(), stmt.Vars
if filter, ok := db.Logger.(ParamsFilter); ok {
sql, vars = filter.ParamsFilter(stmt.Context, stmt.SQL.String(), stmt.Vars...)
}
return db.Dialector.Explain(sql, vars...), db.RowsAffected
}, db.Error)
}
// 输出的日志格式:
// [4.347ms] [rows:1] SELECT * FROM `t_User` WHERE `t_User`.`DeletedAt` IS NULL ORDER BY `t_User`.`ID` LIMIT ?
注意:日志输出顺序不等于sql执行顺序。比如在部分关系的处理上,对Relation表的查询需要依赖Primary的查询结果id。但是从日志上看可能会看到Relation表日志先输出。实际在执行调度上是通过callback类似机制触发,不过日志打印先行执行而已。
此外,为了避免对DB的影响,gorm也提供了dryRun模式,仅用于输出SQL。
- 数据结构查看方法
gorm一些核心数据结果阅读代码可能比较隐晦,尤其各种reflect的代码充斥其中。可以通过调用其非公开API并打印日志,来观察其核心数据结构。比如下面代码演示如何查看核心数据结schema、fields。
func SchemaInfo(db *gorm.DB) {
user := User{}
db.Debug().First(&user)
// 参考execute中的代码,根据struct结构解析其schema以及fields
sch, err := schema.Parse(&User{}, &sync.Map{}, db.Config.NamingStrategy)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(sch.Name)
fmt.Println(sch.ModelType)
fmt.Println(sch.Table)
fmt.Println(sch.PrimaryFieldDBNames)
fmt.Println("CreateClauses:", sch.CreateClauses)
fmt.Println("QueryClauses:", sch.QueryClauses)
for _, c := range sch.QueryClauses {
fmt.Println("QueryClauses Type:", reflect.TypeOf(c))
}
for i, f := range sch.Fields {
fmt.Printf("-----------%d-----------\n", i)
fmt.Println("DBName: ", f.DBName)
fmt.Println("Name: ", f.Name)
fmt.Println("BindNames: ", f.BindNames)
fmt.Println("DataType: ", f.DataType)
fmt.Println("FieldType: ", f.FieldType)
fmt.Println("StructField: ", f.StructField)
fmt.Println("BindName: ", f.BindName())
}
}
DB配置以及连接管理
配置方式
gorm的配置及使用方式可以见官方文档。
从代码中可以看出,option是采用golang的Functional Options模式来实现,支持指定多个option。
// Option gorm option interface
type Option interface {
Apply(*Config) error
AfterInitialize(*DB) error
}
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
config := &Config{}
// ...
for _, opt := range opts {
if opt != nil {
if applyErr := opt.Apply(config); applyErr != nil {
return nil, applyErr
}
defer func(opt Option) {
if errr := opt.AfterInitialize(db); errr != nil {
err = errr
}
}(opt)
}
}
// ...
}
常见的配置项
- SkipDefaultTransaction
是否禁用默认transcation,默认为false。及所有变更操作(create、update、delete)默认会开启事务(因为可能会涉及关系表处理等多次DB交互的场景)。禁用默认事务会带来性能的提升,但会影响数据一致性。
事务生效过程与其他db相关操作一样,都是通过callback的方式注册到写操作的各个 processor 的callback链条中。在 proces.Execute() 中被调度。
注册:default callback注册时,会根据此配置决定是否开启事务callback。
createCallback := db.Callback().Create()
createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
// other callbacks
createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
callback函数:
// 事务callback中执行DB事务开启
func BeginTransaction(db *gorm.DB) {
if !db.Config.SkipDefaultTransaction && db.Error == nil {
if tx := db.Begin(); tx.Error == nil {
db.Statement.ConnPool = tx.Statement.ConnPool
db.InstanceSet("gorm:started_transaction", true)
} else if tx.Error == gorm.ErrInvalidTransaction {
tx.Error = nil
} else {
db.Error = tx.Error
}
}
}
// 根据db执行结果commit or callback
func CommitOrRollbackTransaction(db *gorm.DB) {
if !db.Config.SkipDefaultTransaction {
if _, ok := db.InstanceGet("gorm:started_transaction"); ok {
if db.Error != nil {
db.Rollback()
} else {
db.Commit()
}
db.Statement.ConnPool = db.ConnPool
}
}
}
- NamingStrategy
用于处理命名映射,比如golang结构体字段名(仅允许大写开头的字母)和db表中列名的映射等。
- DryRun
DryRun模式开关,用于调试SQL。
- FullSaveAssociations
更新关联表,gorm更新时默认不更新关系表字段,需要时通过全局配置或者session开启。
- PrepareStmt
是否启用golang sql的 prepared-statements 模式。即开启后db的执行会通过 Stmt 模式执行,适用于批量或者多goroutine并存的场景。
- DisableNestedTransaction
- AllowGlobalUpdate
GORM 默认不允许进行全局 update/delete,该操作会返回 ErrMissingWhereClause 错误。在Query、Delete callback中会校验where条件是否满足(soft delete条件除外)。
func checkMissingWhereConditions(db *gorm.DB) {
if !db.AllowGlobalUpdate && db.Error == nil {
where, withCondition := db.Statement.Clauses["WHERE"]
if withCondition {
if _, withSoftDelete := db.Statement.Clauses["soft_delete_enabled"]; withSoftDelete {
whereClause, _ := where.Expression.(clause.Where)
withCondition = len(whereClause.Exprs) > 1
}
}
if !withCondition {
db.AddError(gorm.ErrMissingWhereClause)
}
return
}
}
连接池
默认情况下,gorm会使用gorm下各dialector提供的连接池实现,以mysql为例,使用的:
func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
if dialector.DriverName == "" {
dialector.DriverName = DefaultDriverName
}
if dialector.DefaultDatetimePrecision == nil {
dialector.DefaultDatetimePrecision = &defaultDatetimePrecision
}
if dialector.Conn != nil {
// 优先使用适用方传入的连接池。通常是 go-gorm/mysql
db.ConnPool = dialector.Conn
} else {
// 否则内部使用 go-sql-driver/mysql
db.ConnPool, err = sql.Open(dialector.DriverName, dialector.DSN)
if err != nil {
return err
}
}
}
用户也可以实现自己的连接池,只要满足接口要求,实现CRUD相关功能即可。
// ConnPool db conns pool interface
type ConnPool interface {
// prepare模式使用
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
// Exec executes a query without returning any rows.
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// QueryContext executes a query that returns rows, typically a SELECT.
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
// QueryRowContext executes a query that is expected to return at most one row. QueryRowContext always returns a non-nil value. Errors are deferred until Row's Scan method is called.
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}
连接池还有另外一种方式,如果开启了 prepared-statements 模式,gorm会在 sql.DB上层进行一次封装( PreparedStmtDB),其目的有二:
- 屏蔽stmt相关的逻辑,方便CRUD操作。即在执行相关sql前不再需要上层执行prepare stmt后才能执行query。
- 性能考虑,在stmt连接池中会在内存中自动缓存prepare的结果(连接有效),key为gorm拼接后的SQL。
type Stmt struct {
*sql.Stmt
Transaction bool
prepared chan struct{} // 多goroutine同步使用
prepareErr error
}
// 在 sql.DB 之上的连接池
type PreparedStmtDB struct {
Stmts map[string]*Stmt // stmts缓存
Mux *sync.RWMutex // 缓存锁
ConnPool
}
// Open initialize db session based on dialector
func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
// 是否启用 prepare 模式
if config.PrepareStmt {
preparedStmt := NewPreparedStmtDB(db.ConnPool)
db.cacheStore.Store(preparedStmtDBKey, preparedStmt)
// 倘若启用了 prepare 模式,会对 connPool 进行替换
db.ConnPool = preparedStmt
}
}
会话以及链式调用
gorm的API分位三类:
- Chain Methods:对应到源码中的 chainable_api.go ,通常用户SQL执行前的各种条件。比如 Where、GroupBy、Sort等。
- Finisher Methods:执行SQL,参见 finisher_api.go 。在finisher_api中会使用Chain Methods增加新的条件,比如 First 仅取一条数据。
- New Session Methods: 用于创建一个安全的可重用的会话。
先看一个官方的例子:
// 使用Where获取到了一个gorm.Statement
queryDB := DB.Where("name = ?", "jinzhu")
// 首次查询,条件会被加入到gorm.Statement
queryDB.Where("age > ?", 10).First(&user)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10
// 二次查询,条件也会被继续追加到同一个gorm.Statement
queryDB.Where("age > ?", 20).First(&user2)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20
使用 queryDB 进行了两次查询操作,但是第二次查询时会带入第一次查询通过链式调佣指定的条件,导致sql污染问题的产生,分析原因之前先了解下链式调用的原理。
链式调用原理
在DB结构体中,存在一个Statment对象,来保存一次会话所有查询条件。
type DB struct {
*Config
Error error
RowsAffected int64
// 会话状态信息
Statement *Statement
// 克隆次数
clone int
}
// 链式调用时,各种条件会被加入到Stament
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
为实现链式调用,在多次调用Chain Methods时,各种条件都会被加入到同一个Statment中,并最终被执行。所以上面的SQL污染问题的原因也在于此。在使用 DB.Where() 获取到DB对象 queryDB 后,后续使用同一个 queryDB 多次调用,Chain Methods指定的条件会被累加到一个Statment中。
但是如果直接使用 gorm.Open 返回的db对象,却没出现污染问题:
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// 'db' is a newly initialized `*gorm.DB`, which is safe to reuse.
db.Where("name = ?", "jinzhu").Where("age = ?", 18).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 18;
db.Where("name = ?", "jinzhu2").Where("age = ?", 20).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu2' AND age = 20;
原因就在于Chain Method的处理逻辑上,对于二者存在区别。以Where为例:
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
}
在Where内部,不是直接使用db对象的Statment,而是先通过getInstance获取到一个“新的”db对象。这里的新不一定会new一个对象,也有可能会复用。
func (db *DB) getInstance() *DB {
if db.clone > 0 {
// 新建一个 db 实例以及statment,clone为0
// 即首次调用 Chain Method 时,会走到此逻辑。
tx := &DB{Config: db.Config, Error: db.Error}
// 倘若是首次对 db 进行 clone,则需要构造出一个新的 statement 实例。由于其clone值为0,后续的调用会被复用。
if db.clone == 1 {
// clone with new statement
tx.Statement = &Statement{
DB: tx,
ConnPool: db.Statement.ConnPool,
Context: db.Statement.Context,
Clauses: map[string]clause.Clause{}, // 新建的 statement 实例中,clause为空 map。表示后续的链式操作,都会基于该 statement 实例进行。
Vars: make([]interface{}, 0, 8),
SkipHooks: db.Statement.SkipHooks,
}
if db.Config.PropagateUnscoped {
tx.Statement.Unscoped = db.Statement.Unscoped
}
} else {
// with clone statement
// Session、WithContext、Debug 等操作,会复用该 db 实例的 statement 实例。将该 db 实例的 statement 实例中的 clause、vars 等属性,复制到新的 db 实例中。
tx.Statement = db.Statement.clone()
tx.Statement.DB = tx
}
return tx
}
// 当clone小于或等于0,直接返回db本身;
// 同一个db对象,第二次调用Chain Method时,会走到此逻辑。复用Statment
return db
}
可见对于使用 gorm.Open 获取到的db对象和使用 Where 获取到db对象,根据其内部的clone计数标识,在调用后续的 Chain Method时会产生不同的效果。
- 前者:gorm.Open 初始化时clone为1,此时首次调用getInstance会命中新建逻辑。会新建一个statment对象(clone=0),供后续的调用复用(clone 标记为0)。
- 后者:由于之前已经通过 Chain Method 获取到了一份复制过的statment(clone值为0),后续的链式调用也会继续复用同一份。
类似数结构,gorm.Open 创建的DB对象可以被多次复制,并产生祖孙节点。所以为了避免污染,尽量使root节点本身或者使用会话模式。
会话模式
如何安全的复用statment以达到条件重复利用的目的呢?gorm提供了会话模式来作为解决方案。参考官方demo:
queryDB := DB.Where("name = ?", "jinzhu").Session(&gorm.Session{})
// First query
queryDB.Where("age > ?", 10).First(&user)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10
// Second query, safely isolated
queryDB.Where("age > ?", 20).First(&user2)
// SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 20
DB.Session() 会复制一份db对象,并将clone标识为2。如上,在Chain Method中调用getInstance时,会命中Statment的clone逻辑。此时虽然也会新建db、Statment,不同的是会将Statment中所有之前的数据copy一份到新建的db.Statment对象中。后续的每次Query调用也都会产生一次新增、copy操作实现类似条件继承的效果,而非直接复用或者纯新增。避免了污染的可能性。
SQL构建
gorm支持Raw SQL,更常见的用法是使用其SQL Builder能力来构建SQL。
// 使用 GORM API 构建 SQL
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// 业务逻辑...
}
// RAW SQL
rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows()
defer rows.Close()
for rows.Next() {
rows.Scan(&name, &age, &email)
// 业务逻辑...
}
Statment
SQL Builder的实现原理与其他实现差别不大,比如 go-sqlbuilder。本质上都是通过API设置好目的SQL的各部分参数,最后Build输出完成的SQL语句,具体可设置内容参见Statment的定义:
// Statement statement
type Statement struct {
*DB
TableExpr *clause.Expr
// 表名
Table string
// 操作的 po(persist object) 模型
Model interface{}
Unscoped bool
// 处理结果反序列化到此处
Dest interface{}
ReflectValue reflect.Value
// 各种条件语句,通过 AddClause 添加到Statement
Clauses map[string]clause.Clause
// 从 processor 中copy而来的操作相关的clause名称
BuildClauses []string
// 是否启用 distinct 模式
Distinct bool
// select 语句
Selects []string // selected columns
// omit 语句
Omits []string // omit columns
ColumnMapping map[string]string // map columns
// Join 语句
Joins []join
Preloads map[string][]interface{}
Settings sync.Map
// 连接池,通常情况下是 database/sql 库下的 *DB 类型. 在 prepare 模式为 gorm.PreparedStmtDB
ConnPool ConnPool
// 操作表的概要信息
Schema *schema.Schema
Context context.Context
// 在未查找到数据记录时,是否抛出 recordNotFound 错误
RaiseErrorOnNotFound bool
SkipHooks bool
SQL strings.Builder
Vars []interface{}
CurDestIndex int
attrs []interface{}
assigns []interface{}
scopes []func(*DB) *DB
}
重点结构:
- Table:指定查询表名称。通过 db.Table() 设置。 db.Table("users")
- Model:指定数据模型,通过 db.Model() 设置。
在构建SQL之前,如果指定了Model,则会根据其结构定义,使用 golang reflect 相关能力,解析出DB相关表结构信息。
// callback.Execute() 中会根据model解析出Schema
if stmt.Model != nil {
if err := stmt.Parse(stmt.Model); err != nil && (!errors.Is(err, schema.ErrUnsupportedDataType) || (stmt.Table == "" && stmt.TableExpr == nil && stmt.SQL.Len() == 0)) {
if errors.Is(err, schema.ErrUnsupportedDataType) && stmt.Table == "" && stmt.TableExpr == nil {
db.AddError(fmt.Errorf("%w: Table not set, please set it like: db.Model(&user) or db.Table(\"users\")", err))
} else {
db.AddError(err)
}
}
}
func (stmt *Statement) Parse(value interface{}) (err error) {
return stmt.ParseWithSpecialTableName(value, "")
}
func (stmt *Statement) ParseWithSpecialTableName(value interface{}, specialTableName string) (err error) {
if stmt.Schema, err = schema.ParseWithSpecialTableName(value, stmt.DB.cacheStore, stmt.DB.NamingStrategy, specialTableName); err == nil && stmt.Table == "" {
if tables := strings.Split(stmt.Schema.Table, "."); len(tables) == 2 {
stmt.TableExpr = &clause.Expr{SQL: stmt.Quote(stmt.Schema.Table)}
stmt.Table = tables[1]
return
}
stmt.Table = stmt.Schema.Table
}
return err
}
ParseWithSpecialTableName实现Schema核心解析逻辑,大致原理是使用golang的反射机制,解析struct定义,以及根据各字段的tag信息,解析出DB中字段的详细信息(数据类型、PrimaryKey、AutoIncrement、是否允许为空、默认值等),以及其与内存中的结构体字段的映射关系,以供后续的CRUD操作(QueryContext、ExecuteContext)使用。此部分内容后面详细分析。
- scopes
scopes用于存储一些需要动态设置Statment的callback函数,通常用于设置动态条件。各callback会在sql生成前被执行。
func (db *DB) executeScopes() (tx *DB) {
scopes := db.Statement.scopes
db.Statement.scopes = nil
for _, scope := range scopes {
db = scope(db)
}
return db
}
Clauses
*gorm.Statement 底层实际使用 Clause 列表来生成SQL,暴露的各种API基本上也是对Clause 的操作。从代码目录上看,clause基本就是对SQL各部分拆解后的封装,包括:Insert/Delete/Select/Update、From、Join、Where、GroupBy、OrderBy等。Clause 的接口要求:
type Interface interface {
Name() string // clause 名称
Build(Builder) // 生成对应的 sql 部分
MergeClause(*Clause) // 和同类 clause 合并
}
gorm中支持的 Clause 类型包括:
func (d Delete) Name() string {
return "DELETE"
}
func (from From) Name() string {
return "FROM"
}
func (groupBy GroupBy) Name() string {
return "GROUP BY"
}
func (insert Insert) Name() string {
return "INSERT"
}
func (limit Limit) Name() string {
return "LIMIT"
}
func (locking Locking) Name() string {
return "FOR"
}
func (OnConflict) Name() string {
return "ON CONFLICT"
}
func (orderBy OrderBy) Name() string {
return "ORDER BY"
}
func (returning Returning) Name() string {
return "RETURNING"
}
func (s Select) Name() string {
return "SELECT"
}
func (set Set) Name() string {
return "SET"
}
func (update Update) Name() string {
return "UPDATE"
}
func (Values) Name() string {
return "VALUES"
}
func (where Where) Name() string {
return "WHERE"
}
在上层设置好各种Clause之后,何时触发SQL的构建呢,以及如何处理各DB的差异?
以Mysql下的插入场景为例,观察其SQL构建过程。
- 初始化:在dialector的初始化过程中( gorm.Open() ),会注册cRUD的各processor所需的callback以及相关配置。
// callback配置
type Config struct {
LastInsertIDReversed bool
CreateClauses []string
QueryClauses []string
UpdateClauses []string
DeleteClauses []string
}
// mysql dialactor传入的配置参数,指定各Processor所需的clause名称,供Statment.Build时使用
// CreateClauses create clauses
CreateClauses = []string{"INSERT", "VALUES", "ON CONFLICT"}
// QueryClauses query clauses
QueryClauses = []string{}
// UpdateClauses update clauses
UpdateClauses = []string{"UPDATE", "SET", "WHERE", "ORDER BY", "LIMIT"}
// DeleteClauses delete clauses
DeleteClauses = []string{"DELETE", "FROM", "WHERE", "ORDER BY", "LIMIT"}
可见各dialector会决定其自身各操作所需的 Clause 名称,来选择其组建SQL所需的 Clause 条件,然后RegisterDefaultCallbacks 中注册各类CRUD操作的callback逻辑,以插入为例,回调操作(callbacks)包括:
// 设置写操作相关回调 以及 配置
createCallback := db.Callback().Create()
// 开启事务
createCallback.Match(enableTransaction).Register("gorm:begin_transaction", BeginTransaction)
// 业务回调: BeforeSave、BeforeCreate
createCallback.Register("gorm:before_create", BeforeCreate)
// 处理插入前的关系:仅 BelongTo 关系
createCallback.Register("gorm:save_before_associations", SaveBeforeAssociations(true))
// 插入SQL构建及执行
createCallback.Register("gorm:create", Create(config))
// 查理插入后关系:包括 HasOne、HasMany、Many2Many 关系
createCallback.Register("gorm:save_after_associations", SaveAfterAssociations(true))
// 业务回调:AfterCreate、AfterSave
createCallback.Register("gorm:after_create", AfterCreate)
// 结束事务
createCallback.Match(enableTransaction).Register("gorm:commit_or_rollback_transaction", CommitOrRollbackTransaction)
// 设置dialector所需 Clause 名称
createCallback.Clauses = config.CreateClauses
- gorm中的各个 Finisher Methods 会设置Dest,然后触发callbacks链的执行。包括上面代码中的各个callbacks。
- 其中 Create callback负责SQL构建以及数据的写入。
if db.Statement.SQL.Len() == 0 {
db.Statement.SQL.Grow(180)
db.Statement.AddClauseIfNotExists(clause.Insert{})
// 值转换,并添加必要的子句
db.Statement.AddClause(ConvertToCreateValues(db.Statement))
// 构建SQL语句:BuildClauses 即为dialector中指定的processor所需 Clause 名称
db.Statement.Build(db.Statement.BuildClauses...)
}
BuildClause 即为dialector中所指定构建Mysql插入语句时支持的clause名称,即上面的 "INSERT", "VALUES", "ON CONFLICT" 三部分。
最后的Build就比较简单了,取出Statment.Clause 中的各个Clause,调用其Build方法构建SQL子句,最后合并成完整的SQL。
func (stmt *Statement) Build(clauses ...string) {
var firstClauseWritten bool
for _, name := range clauses {
if c, ok := stmt.Clauses[name]; ok {
if firstClauseWritten {
stmt.WriteByte(' ') // add space between clauses
}
firstClauseWritten = true
if b, ok := stmt.DB.ClauseBuilders[name]; ok {
// dialector 自定义的 clause 构建方法
b(c, stmt)
} else {
// clause对应的build方法
c.Build(stmt)
}
}
}
}
其中,允许dialector自定义clause的Build实现。比如mysql就自定义了 ON CONFLICT 等的实现。