Gorm系列之2

Gorm系列之2

特别指出

特别指出的是,该系列基础代码来自git上的开源项目7days-golang,项目地址:https://github.com/geektutu/7days-golang。

原项目作者:极客兔兔,个人主页:https://geektutu.com/。

除基础代码外,部分解释内容也摘自作者的系列博文,地址:https://geektutu.com/post/gee.html

大神极客兔兔在他的博客中对该项目有自底向上的详细讲解,并将每个项目分成7天来学习,希望深入分析源码的朋友可以移步上面的传送门。相比原作者,我将更多的自顶向下入手,先从整体分析项目结构,再深入其中一些关键的部分,适用于希望快速了解项目结构的人和初学者;同时在原项目基础上做了一定的增添。

项目传送门

https://github.com/CAGeng/Gorm

认识schema

上一节说到session是数据库操作的高层封装,为了往下深入一步,从/session/table.go中挑一个函数——Model函数,从这里看起。

func (s *Session) Model(value interface{}) *Session{
	if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model){
		s.refTable = schema.Parse(value, s.dialect)
	}
	return s
}

这个函数的用法已经提到过,如下:

//确定表格式
s := NewSession().Model(&User{})

user是我们自己定义的数据格式,用它作为入参调用session的成员函数Model,将会解析user的格式并存入session的成员变量reftable中,这么做了之后,数据库中一张表的格式就确定下来——或者说完成了ORM中的对象关系映射。Model调用了schema.Parse函数,这引导我们深入的下一站应该是schema。

/schema/schema.go中定义了两种数据结构,分别用来表示关系型数据库的列信息(属性)和表信息(包含多个列):

//  /schema/schema.go

//field表示数据库的一列
type Field struct {
	Name string
	Type string
	Tag string
}

//schema表示数据库的一张表
type Schema struct {
	Model interface{}
	Name string
	Fields []*Field
	FieldNames []string
	fieldMap map[string]*Field
}

然后看/schema/schema.go中的parse函数:

// /schema/schema.go

func Parse(dest interface{}, d dialect.Dialect) *Schema{
	modelType := reflect.Indirect(reflect.ValueOf(dest)).Type()
	schema := &Schema{
		Model: dest,
		Name: modelType.Name(),
		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,
                //这里用了dialect
				Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))),
			}
			//p.Tag 即额外的约束条件
			if v,ok := p.Tag.Lookup("Gorm");ok{
				field.Tag = v
			}
			schema.Fields = append(schema.Fields,field)
			schema.FieldNames = append(schema.FieldNames, p.Name)
			schema.fieldMap[p.Name] = field

		}
	}
	return schema
}

这个函数用到了go中的反射(reflect)机制,这里不详细展开,其作用是传递一个自定义类进去,通过反射机制能够获得类的各个成员及其成员类型。Parse函数要做的就是根据类的成员信息,对应到数据库中一张关系表的结构,后者是用Schema这个类表示的,也正是session的成员变量reftable。所以,schema是对表结构的定义

认识dialect

既然要映射到一个表,那表里的数据类型是什么?一方面,go中的类(test中的user)里已经定义好了数据类型,但是这并不是数据库中的数据类型,例如,go的内建类型intint8int16 等类型均对应 SQLite 中的 integer 类型。所以我们需要一种方法来完成这种映射;同时我们不希望这种映射是写死的,因为我们希望能便捷的支持多种数据库。这就是我们需要定义的dialect,此部分代码的位置在**/dialect/dialect.go**。

实际上dialect只是一个接口或者说父类,不需要定义函数的具体工作,所以这部分是比较简单的,Dialect的定义:

//   /dialect/dialect.go

type Dialect interface {
	//用于将 Go 语言的类型转换为该数据库的数据类型。
	DataTypeOf(typ reflect.Value) string
	//返回某个表是否存在的 SQL 语句,参数是表名(table)。
	TableExistSQL(tableName string) (string,[]interface{})
}

目前这个接口只需要实现两个方法:数据映射DataTypeOf和判断表是否存在的sql语句TableExistSQL。具体怎么用,我们看它的“子类”是如何实现这个接口的。

实现dialect——sqlite3.go

//  /dialect/sqlite3.go
//提供对sqlite3的支持,结构sqlite3要实现接口dialect的所有方法
type sqlite3 struct {}

//下面这句用来检查结构sqlite3是否实现接口dialect的所有方法
var _ Dialect = (*sqlite3)(nil)

func init(){
	RegisterDialect("sqlite3",&sqlite3{})
}

func (s *sqlite3) DataTypeOf(typ reflect.Value) string {
	switch typ.Kind() {
	case reflect.Bool:
		return "bool"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
		return "integer"
	case reflect.Int64, reflect.Uint64:
		return "bigint"
	case reflect.Float32, reflect.Float64:
		return "real"
	case reflect.String:
		return "text"
	case reflect.Array, reflect.Slice:
		return "blob"
	case reflect.Struct:
		//时间
		if _, ok := typ.Interface().(time.Time); ok {
			return "datetime"
		}
	}
	panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind()))

}

func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) {
	args := []interface{}{tableName}  
	return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
}

这段很清楚简单了,大家自己看吧。

认识clause

到目前,我们已经大体了解了从ORM框架一步一步转化为一条数据库可执行的SQL语句的过程,小总结一下:

  • session维护打开的数据库,封装底层操作,暴露方便的数据库操作接口
  • schema作为session成员之一,定义数据表(table)的结构
  • dialect也是session成员之一,此接口需要实现一组方法,它们用来桥接golang和特定的数据库(数据类型映射)

整体上的引用关系为

--session-->schema
		-->dialect

我们现在使用框架执行特定类型的SQL语句已经没有问题了,例如:

建表语句
CREATE TABLE User (Name text ,Age integer ); 

但是有些时候,sql语句并不总是作为一个整体出现,而可能分为好几个子句,例如下面这个select语句:

SELECT col1, col2, ...
    FROM table_name
    WHERE [ conditions ]
    GROUP BY col1
    HAVING [ conditions ]

可以认为它是由5个子句拼接而成的,而在不同的情况下这每个子句并不都是必须的。我们肯定不希望为这些sql语句定义2^5种类型,作为函数放在框架中暴露给别人使用,而希望提供分开定义子句,之后合并为整句的方法,这样我们只需要5种子句的类型就可以了。实现这个功能的位置正是接下来要说的:/clause

generator.go

首先在 clause/generator.go 中实现各个子句的生成规则。举个例子,select子句:

func _select(values ...interface{}) (string, []interface{}) {
	// SELECT $fields FROM $tableName
	tableName := values[0]
	fields := strings.Join(values[1].([]string), ",")  
	return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{}
}

然后在 clause/clause.go 中实现结构体 Clause 拼接各个独立的子句。

  • Set 方法根据 Type 调用对应的 generator,生成该子句对应的 SQL 语句
  • Build 方法根据传入的 Type 的顺序,构造出最终的 SQL 语句
//Set 方法根据 Type 调用对应的 generator,生成该子句对应的 SQL 语句
func (c *Clause) Set(name Type, vars ...interface{}){
	调试使用
	//log.Mytprinter.IndentLvUp()
	//defer log.Mytprinter.IndentLvDown()
	//log.Mytprinter.Print("/clause/clause.go: Set begin")

	if c.sql == nil{
		c.sql = make(map[Type]string)
		c.sqlVars = make(map[Type][]interface{})
	}
	sql, vars := generators[name](vars...)
	c.sql[name] = sql
	c.sqlVars[name] = vars
}

//Build 方法根据传入的 Type 的顺序,构造出最终的 SQL 语句。
func (c *Clause) Build(orders ...Type)(string, []interface{}){
	//调试使用
	//log.Mytprinter.IndentLvUp()
	//defer log.Mytprinter.IndentLvDown()
	//log.Mytprinter.Print("/clause/clause.go: Build begin")

	var sqls []string
	var vars []interface{}
	for _,order := range orders{
		if sql, ok := c.sql[order]; ok{
			sqls = append(sqls,sql)
			vars = append(vars, c.sqlVars[order]...)
		}
	}
	return strings.Join(sqls," "),vars
}

然后我们在clause_test.go的testSelect函数中看看是怎么用的:

	var clause Clause
	clause.Set(LIMIT, 3)
	clause.Set(SELECT, "User", []string{"*"})
	clause.Set(WHERE, "Name = ?", "Tom")
	clause.Set(ORDERBY, "Age ASC")
	sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT)
	t.Log(sql, vars)
	if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?" {
		t.Fatal("failed to build SQL")
	}
	if !reflect.DeepEqual(vars, []interface{}{"Tom", 3}) {
		t.Fatal("failed to build SQLVars")
	}

逻辑很清楚,不解释了。

运行clause_test.go的TestClause_Build函数测试一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ru7bgf1t-1619063016008)(Gorm2.assets/image-20210422113315331.png)]

可以看到拼接出来的语句长这样。

利用clause实现insert和find功能

现在我们已经完成了子句拼接整句,但我们要能够通过session使用它,所以还需要进一步封装。先看看我们期待的使用方式:

//  /session/record_test  : TestSession_Insert
affected, err := s.Insert(user3)   //将user3插入

//  /session/record_test  : TestSession_Find
err := s.Find(&users)   //结果保存在users中

那我们就在**/session/record.go**中实现这两个功能,代码较长不粘贴了,主要应用的就是前面的set和build以及反射机制。

这一节就先到这里

你可能感兴趣的:(手撸框架系列,数据库,程序人生,经验分享)