GeeORM 笔记总结
1. 核心思想
 1.1 标准库 database/sql
SQL 语句的执行是对标准库方法的封装:
type Session struct {
	db       *sql.DB
	...
}
...
func (s *Session) Exec() (sql.Result, error) {
	defer s.Clear()
	log.Info(s.sql.String(), s.sqlVars)
	res, err := s.DB().Exec(s.sql.String(), s.sqlVars...)
	if err != nil {
		log.Error(err)
	}
	return res, err
}
 1.2 反射 reflect
ORM 对象关系映射,因为对象结构和表结构是未知的,所以使用反射机制进行处理。
type Schema struct {
	Model      any
	Name       string
	Fields     []*Field
	FieldNames []string
	fieldMap   map[string]*Field
}
func Parse(dst any, d dialect.Dialect) *Schema {
	modelType := reflect.Indirect(reflect.ValueOf(dst)).Type()
	schema := &Schema{
		Model:    dst,
		Name:     modelType.Name(),
		fieldMap: make(map[string]*Field),
	}
	for i := 0; i < modelType.NumField(); i++ {
		f := modelType.Field(i)
		if !f.Anonymous && ast.IsExported(f.Name) {
			field := &Field{
				Name: f.Name,
				Type: d.DataTypeOf(reflect.Indirect(reflect.New(f.Type))),
			}
			if v, ok := f.Tag.Lookup("geeorm"); ok {
				field.Tag = v
			}
			schema.Fields = append(schema.Fields, field)
			schema.FieldNames = append(schema.FieldNames, f.Name)
			schema.fieldMap[f.Name] = field
		}
	}
	return schema
}
2. 设计
2.1 分级 Log
const (
	InfoLevel = iota
	ErrorLevel
	Disabled
)
// SetLevel set log level for logger
func SetLevel(level int) {
	mu.Lock()
	defer mu.Unlock()
	for _, logger := range loggers {
		logger.SetOutput(os.Stdout)
	}
	if ErrorLevel < level {
		errorLog.SetOutput(io.Discard)
	}
	if InfoLevel < level {
		infoLog.SetOutput(io.Discard)
	}
}
通过设置的level来决定哪些级别的log的输出被抛弃(设置为io.Discard)。
2.2 会话 Session
Session 用于和数据库交互,调用标准库执行 SQL 语句。
type Session struct {
	db       *sql.DB
	dialect  dialect.Dialect
	tx       *sql.Tx
	refTable *schema.Schema
	clause   clause.Clause
	sql      strings.Builder
	sqlVars  []any
}
db:标准库sql.DB实例dialect:不同数据库的 Dialecttx:标准库sql.Tx实例,支持事务refTable:对应的表结构clause:SQL 子语句sql:待执行的 SQL 语句sqlVars:待执行的 SQL 语句参数
2.3 Engine
框架的入口,用于和数据库交互前的准备工作(建立/测试连接)和交互后的收尾工作(关闭连接)。
type Engine struct {
	db      *sql.DB
	dialect dialect.Dialect
}
func NewEngine(driver, source string) (*Engine, error) {
	db, err := sql.Open(driver, source)
	if err != nil {
		log.Error(err)
		return nil, err
	}
	// Send ping to make sure the database connection is alive
	if err = db.Ping(); err != nil {
		log.Error(err)
		return nil, err
	}
	// make sure the specific dialect exists
	d, ok := dialect.GetDialect(driver)
	if !ok {
		log.Errorf("dialect %s Not Found", driver)
		return nil, DialectNotFoundErr
	}
	e := &Engine{db: db, dialect: d}
	log.Info("Connect database success")
	return e, nil
}
NewEngine流程:
- 建立数据库连接
 - 发送 Ping 检测连接
 - 获取是否有配置数据 Dialect
 
2.4 Dialect
不同的数据库,其 SQL 语句,数据类型可能有所不同。可以针对每种数据库的不同之处设置对应的 Dialect。
var dialectsMap = map[string]Dialect{}
type Dialect interface {
	DataTypeOf(typ reflect.Value) string
	TableExistSQL(tableName string) (string, []any)
}
func RegisterDialect(name string, dialect Dialect) {
	dialectsMap[name] = dialect
}
func GetDialect(name string) (Dialect, bool) {
	dialect, ok := dialectsMap[name]
	return dialect, ok
}
Dialect:接口DataTpyeOF:通过 Golang 类型获取对应的数据库类型TabelExistsSQL:获取数据库的表是否存在的查询语句
dialectMap:存储数据对应的 DialectRegisterDialect:注册 DialectGetDialect:通过数据库类型获取 Dialect
2.5 Schema
Schema 表示数据库的表结构,用于建立对象和表结构的映射(ORM)。
type Schema struct {
	Model      any
	Name       string
	Fields     []*Field
	FieldNames []string
	fieldMap   map[string]*Field
}
Model:对象实例Name:表名/结构体名Fields:结构体字段列表FieldNames:结构体字段名/表字段名 列表fieldMap:用于通过字段名快速获取字段
Session 复用
func (s *Session) Clear() {
    s.sql.Reset()
    s.sqlVars = nil
    s.clause = clause.Clause{}
}
func (s *Session) Exec() (sql.Result, error) {
	defer s.Clear()
	log.Info(s.sql.String(), s.sqlVars)
	res, err := s.DB().Exec(s.sql.String(), s.sqlVars...)
	if err != nil {
		log.Error(err)
	}
	return res, err
}
在执行完成一次 SQL 语句之后,重置 Session 的状态,可以执行其他的 SQL。复用 Session 可以避免创建过多的实例并简化代码。
2.6 Clause
SQL 语句可以拆分为多个子句(clause),例如:
SELECT col1, col2, ...
    FROM table_name
    WHERE [ conditions ]
    GROUP BY col1
    HAVING [ conditions ]
可以拆分为:
- SELECT Clause:
SELECT col1, col2, ... FROM table_name - WHERE Clause:
WHERE conditions - GROUP BY Clause:
GROUP BY col1 - HAVING Clause:
HAVING conditions 
通过不同的 clause 之间的组合,可以构成完整的 SQL 语句。
type Clause struct {
	sql     map[Type]string
	sqlVars map[Type][]any
}
type Type int
const (
	INSERT Type = iota
	VALUES
	SELECT
	LIMIT
	WHERE
	ORDERBY
	UPDATE
	DELETE
	COUNT
)
Clause:子句sql:子句类型对应的 SQL 语句sqlVars:子句类型对应的 SQL 参数
Type:子句类型,通过常量预设
Clause 生成函数
type generator func(vals ...any) (string, []any)
var generators map[Type]generator
func init() {
	generators = make(map[Type]generator)
	generators[INSERT] = _insert
	...
}
func _insert(vals ...any) (string, []any) {
	// INSERT INTO $tableName ($fields)
	tableName := vals[0]
	fields := strings.Join(vals[1].([]string), ",")
	return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []any{}
}
generators:全局变量,子句类型对应的SQL生成函数_insert:生成 INSERT 语句
2.7 链式调用
链式调用是一种简化代码的编程方式,能够使代码更简洁、易读。
原理: 某个对象调用某个方法后,将该对象的引用/指针返回,即可以继续调用该对象的其他方法。
SQL 语句由多个子语句构成,可以通过链式调用组合成完整的 SQL 语句。
Session 负责和数据交互,那么其构建 SQL 语句的函数返回值可以设置为*Session类型以支持链式调用。
func (s *Session) Where(desc string, args ...any) *Session {
	var vars []any
	s.clause.Set(clause.WHERE, append(append(vars, desc), args...)...)
	return s
}
...
func (s *Session) Find(vals any) error {
	s.CallMethod(BeforeQuery, nil)
	dstSlice := reflect.Indirect(reflect.ValueOf(vals))
	dstType := dstSlice.Type().Elem()
	table := s.Model(reflect.New(dstType).Elem().Interface()).RefTable()
	s.clause.Set(clause.SELECT, table.Name, table.FieldNames)
	sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT)
	rows, err := s.Raw(sql, vars...).QueryRows()
	if err != nil {
		return err
	}
	for rows.Next() {
		dst := reflect.New(dstType).Elem()
		var fieldVals []any
		for _, name := range table.FieldNames {
			fieldVals = append(fieldVals, dst.FieldByName(name).Addr().Interface())
		}
		if err := rows.Scan(fieldVals...); err != nil {
			return err
		}
		s.CallMethod(AfterQuery, dst.Addr().Interface())
		dstSlice.Set(reflect.Append(dstSlice, dst))
	}
	return rows.Close()
}
例如:
s := geeorm.NewEngine("sqlite3", "gee.db").NewSession()
var users []User
s.Where("Age > 18").Limit(3).Find(&users)
2.8 Hook
钩子函数,主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。
对于 SQL 执行来说,CRUD 操作适合于添加钩子函数。例如:在查询结束后,对查询结果中的信息进行脱敏处理。
const (
    BeforeQuery  = "BeforeQuery"
    AfterQuery   = "AfterQuery"
    BeforeUpdate = "BeforeUpdate"
    AfterUpdate  = "AfterUpdate"
    BeforeDelete = "BeforeDelete"
    AfterDelete  = "AfterDelete"
    BeforeInsert = "BeforeInsert"
    AfterInsert  = "AfterInsert"
)
// CallMethod calls the registered hooks
func (s *Session) CallMethod(method string, value any) {
    var fm reflect.Value
    if value == nil {
       fm = reflect.ValueOf(s.RefTable().Model).MethodByName(method)
    } else {
       fm = reflect.ValueOf(value).MethodByName(method)
    }
    param := []reflect.Value{reflect.ValueOf(s)}
    if fm.IsValid() {
       if v := fm.Call(param); len(v) > 0 {
          if err, ok := v[0].Interface().(error); ok {
             log.Error(err)
          }
       }
    }
    return
}
钩子函数约定的类型为:Hook_name (s *Session) error
CallMethod流程:
- 通过反射获取对象实现的钩子函数
 - 获取钩子函数入参,并调用
 - 返回执行结果
 
2.9 事务支持
事务的 ACID:
- 原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
 - 一致性(Consistency): 几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。
 - 隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
 - 持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
 
对事物的支持使用标准库database/sql.Tx即可:
type Session struct {
	...
	tx       *sql.Tx
	...
}
func (s *Session) Begin() error {
	log.Info("transaction begin")
	var err error
	if s.tx, err = s.db.Begin(); err != nil {
		log.Error(err)
		return err
	}
	return nil
}
func (s *Session) Commit() error {
	log.Info("transaction commit")
	if err := s.tx.Commit(); err != nil {
		log.Error(err)
		return err
	}
	return nil
}
func (s *Session) Rollback() error {
	log.Info("transaction rollback")
	if err := s.tx.Rollback(); err != nil {
		log.Error(err)
		return err
	}
	return nil
}
自动化接口
type TxFunc func(s *session.Session) (any, error)
func (engine *Engine) Transaction(f TxFunc) (res any, err error) {
    s := engine.NewSession()
    if err = s.Begin(); err != nil {
       return nil, err
    }
    defer func() {
       if p := recover(); p != nil {
          _ = s.Rollback()
          panic(p) // re-throw panic after rollback
       } else if err != nil {
          _ = s.Rollback() // err is non-nil
       } else {
          err = s.Commit() // err is nil; if Commit returns error, update err
       }
    }()
    return f(s)
}
用户只需要将所有的操作放到一个回调函数中,作为入参传递给 engine.Transaction(),发生任何错误,自动回滚,如果没有错误发生,则提交。
2.10 数据库迁移
支持数据库迁移,当结构体发生改变时,可以同步更改表结构。
不同的数据库,迁移方式不同,以 SQLite 为例:
// return a - b
func difference(a []string, b []string) []string {
	setB := make(map[string]struct{})
	for _, v := range b {
		setB[v] = struct{}{}
	}
	diff := make([]string, 0)
	for _, v := range a {
		if _, ok := setB[v]; !ok {
			diff = append(diff, v)
		}
	}
	return diff
}
// Migrate table
func (engine *Engine) Migrate(value any) error {
	_, err := engine.Transaction(func(s *session.Session) (any, error) {
		if !s.Model(value).HasTable() {
			log.Infof("table %s doesn't exist, creat table", s.RefTable().Name)
			return nil, s.CreateTable()
		}
		table := s.RefTable()
		rows, _ := s.Raw(fmt.Sprintf("SELECT * FROM %s LIMIT 1", table.Name)).QueryRows()
		columns, _ := rows.Columns()
		addCols := difference(table.FieldNames, columns)
		delCols := difference(columns, table.FieldNames)
		log.Info("add cols %v, deleted cols %v", addCols, delCols)
		for _, col := range addCols {
			f := table.GetField(col)
			sqlStr := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s;", table.Name, f.Name, f.Type)
			if _, err := s.Raw(sqlStr).Exec(); err != nil {
				return nil, err
			}
		}
		if len(delCols) == 0 {
			return nil, nil
		}
		tmp := "tmp_" + table.Name
		fieldStr := strings.Join(table.FieldNames, ", ")
		s.Raw(fmt.Sprintf("CREATE TABLE %s AS SELECT %s FROM %s;", tmp, fieldStr, table.Name))
		s.Raw(fmt.Sprintf("DROP TABLE %s;", table.Name))
		s.Raw(fmt.Sprintf("ALTER TABLE %s RENAME TO %s;", tmp, table.Name))
		_, err := s.Exec()
		return nil, err
	})
	return err
}
- 找出需要删除/新增的字段
 - 创建新表,迁移数据,删除旧表
 - 将新表改名为原表名
 
3. 流程
连接数据库并执行 SQL 的流程如下:
- 连接数据库
 - 创建会话 Session
 - 通过不同子句 Clause 组合成完整的 SQL
 - 执行 SQL 并获取结果
 
