特别指出的是,该系列基础代码来自git上的开源项目7days-golang,项目地址:https://github.com/geektutu/7days-golang。
原项目作者:极客兔兔,个人主页:https://geektutu.com/。
除基础代码外,部分解释内容也摘自作者的系列博文,地址:https://geektutu.com/post/gee.html
大神极客兔兔在他的博客中对该项目有自底向上的详细讲解,并将每个项目分成7天来学习,希望深入分析源码的朋友可以移步上面的传送门。相比原作者,我将更多的自顶向下入手,先从整体分析项目结构,再深入其中一些关键的部分,适用于希望快速了解项目结构的人和初学者;同时在原项目基础上做了一定的增添。
https://github.com/CAGeng/Gorm
上一节说到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是对表结构的定义。
既然要映射到一个表,那表里的数据类型是什么?一方面,go中的类(test中的user)里已经定义好了数据类型,但是这并不是数据库中的数据类型,例如,go的内建类型int
、int8
、int16
等类型均对应 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
//提供对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
}
这段很清楚简单了,大家自己看吧。
到目前,我们已经大体了解了从ORM框架一步一步转化为一条数据库可执行的SQL语句的过程,小总结一下:
整体上的引用关系为
--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。
首先在 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 语句
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函数测试一下:
可以看到拼接出来的语句长这样。
现在我们已经完成了子句拼接整句,但我们要能够通过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以及反射机制。