极客兔兔七天系列学习笔记--GeeORM


ORM框架实现的是对象到数据库表的映射,对于任意对象的属性解析,反射恰逢其时。

Session

想要对数据库进行操作,我们在连接数据库后可以新建一个对话。在go语言中直接用db对象也是可以的。但经过Session封装后我们可以做更多的事情:

  1. Engine可以随时新建Session,进行并发。
  2. Session独立管理他所连接的数据库和对象,这一切都是可高度定制的。
  3. Session维护sql.Tx,选择开启事务与否。
点击查看代码
type Session struct {
	db       *sql.DB
	tx       *sql.Tx
	refTable *schema.Schema
	clause   clause.Clause
	dialect  dialect.Dialect
	sql      strings.Builder
	sqlVars  []interface{}
}

对象->表结构

对象有许多属性,包括属性名,类型和标识。我们要解析对象,获取到这些信息,才能操作数据库,例如下面的例子:

type User struct {
    Name string `geeorm:"PRIMARY KEY"`
    Age  int
}

对应schema语句:
CREATE TABLE User (Nametext PRIMARY KEY,Age integer);
通过反射我们可以很方便地获得每一个属性的信息,并且构造一个Schema结构体来封装相关信息。

点击查看代码
type Schema struct {
	Model      interface{}
	Name       string
	Fields     []*Field
	FieldNames []string
	fieldMap   map[string]*Field
}


func Parse(dest interface{}, dialect dialect.Dialect) *Schema {
	modelType := reflect.Indirect(reflect.ValueOf(dest)).Type()
	schema := &Schema{
		Name:     modelType.Name(),
		Model:    dest,
		FieldMap: make(map[string]*Field),
	}
	for i := 0; i < modelType.NumField(); i++ {
		p := modelType.Field(i)
		if p.Anonymous && ast.IsExported(p.Name) {
			field := &Field{
				Name: p.Name,
				Type: dialect.DataTypeOf(reflect.Indirect(reflect.New(p.Type))),
			}
			if v, ok := p.Tag.Lookup("geeorm"); ok {
				field.Tag = v
			}
			schema.Fields = append(schema.Fields, field)
			schema.FieldNames = append(schema.FieldNames, field.Name)
			schema.FieldMap[p.Name] = field
		}
	}
	return schema
}
同样,对于不同的数据库,对应的数据库语言包括类型都有较大差距,为了支持多种数据库,我们选择封装一个接口Dialect,让sqlite或mysql分别去实现,其中最重要的方法就是上述代码中的DataTypeOf(这里就是go语言变量类型到数据库变量类型的转换),初始化后,这些信息由map维护。

Clause模块

对于ORM框架这是锦上添花的操作,因为原始的sql语句过于复杂,开发人员更愿意使用简单的操作,例如组装。
我们可以先构造生产分句小零件的功能函数,他们每一个只产生固定规格的分句,例如select,value,where,然后让一个大家伙通过组装的方式拼凑出完整的sql语句和所需变量

type Clause struct {
	sql     map[Type]string
	sqlVars map[Type][]interface{}
}

func (clause *Clause) Set(name Type, vars ...interface{}) {
	if clause.sql == nil {
		clause.sql = make(map[Type]string)
		clause.sqlVars = make(map[Type][]interface{})
	}
	sql, args := generators[name](vars...)
	clause.sql[name] = sql
	clause.sqlVars[name] = args
}

func (clause *Clause) Build(orders ...Type) (string, []interface{}) {
	var sqls []string
	var vars []interface{}
	for _, order := range orders {
		sqls = append(sqls, clause.sql[order])
		vars = append(vars, clause.sqlVars[order]...)
	}
	return strings.Join(sqls, ","), vars
}

Hook

在可能增加功能的地方埋上钩子,选择将扩展的功能挂在到这个点,比如Password字段的脱敏处理。这里的钩子实现比较简单,我们预设了所有可能的钩子方法(字符串定义),通过反射判断对象的该方法是否合法并选择实现。前面我们通过Clause最终封装了完整的Insert等操作函数,现在只需要将方法插入操作的对应位置接口,如果钩子没有实现就不会被调用。这里我们有一个统一的函数模版:

const (
	BeforeQuery  = "BeforeQuery"
	AfterQuery   = "AfterQuery"
	BeforeUpdate = "BeforeUpdate"
	AfterUpdate  = "AfterUpdate"
	BeforeDelete = "BeforeDelete"
	AfterDelete  = "AfterDelete"
	BeforeInsert = "BeforeInsert"
	AfterInsert  = "AfterInsert"
)

func (s *Session) CallMethod(name string, value interface{}) {
	fn := reflect.ValueOf(s.RefTable().Model).MethodByName(name)
	if value != nil {
		fn = reflect.ValueOf(value).MethodByName(name)
	}
	params := []reflect.Value{reflect.ValueOf(s)}
	if fn.IsValid() {
		if values := fn.Call(params); len(values) > 0 {
			if err, ok := values[0].Interface().(error); ok {
				log.Error(err)
			}
		}
	}
}