xorm的一次get请求做了什么?
func TestUser_Info(t *testing.T) {
user := &model.User{}
//var form *model.User
println("BEFORE", unsafe.Pointer(user), unsafe.Pointer(&user.NickName))
sql.Session().ID(1).Get(user)
println("AFTER", unsafe.Pointer(user), unsafe.Pointer(&user.NickName))
}
运行测试方法,跟着断点调试下去
xorm中对get方法的描述是:从数据库中检索一条记录,检索的过程中会把非空字段当做条件。xorm认定非空字段条件:number != 0,string != “”,slice != nil,如果是自动关闭的回话,xorm会自动关闭它(默认为自动关闭的回话,所以在业务代码中基本不需要defer session.Close())
如果bean不是指针或者bean是指针同时bean的value也是指针,报错。这两种情况是大部分goalng 新手容易掉进去的坑,框架首先判断了这两种情况,通过值传递做的更改不算数。
这里调用了setRefBean 设置bean的引用,就是根据bean初始化一些参数。
方法调用了rValue去获得real value ,用了golang的反射,rValue调用了ValueOf,ValueOf函数返回一个Value类型值,该值代表运行时的数据。
调用ValueOf会发生指针逃逸,指针会从栈上逃到堆上 escapes(i)
*返回unpackEface(i),unpackEface会把空指针搞成一个实际对象(如果i为空的话),所以&User{},var user User没区别。
*TIPS:|=
是位运算符表示按位或后赋值。
得到实际值后,xorm调用autoMapType自动映射类型信息,这个函数有锁。首先尝试在engine.Tables中寻找t类型代表的表名字,如果有,使用,如果没有,自己映射一个,算个缓存吧。现在是第一次访问,进入if判断。
这个函数的目的是把下图的table信息获取出来。
调用tbNameForMap(v)
Implements会报告这个struct是否实现了TableName接口。如果实现了调用返回,这就是为什么Struct implement TableName 能够自定义表名。(下面代码顺序是错的只供阅读)
tpTableName = reflect.TypeOf((*TableName)(nil)).Elem()
// TableName table name interface to define customerize table name
type TableName interface {
TableName() string
}
return v.Interface().(TableName).TableName()
如果没有实现,调用xorm中的Obj2Table,这又是另一组接口,可以自定义实现,随便怎么搞。
默认实现也有很多,能满足大部分需求。
跑完tbNameForMap(v),知道我们的表名了t_user,开始处理列,接下来是对某一列的处理。最终的目的是为了构造
这个结构体,首先看看这个属性有没有xorm这个tag
如果xorm不为空,先去处理tags
从代码来不同tag之间用空格分割。
循环对tags进行处理,如果当前的tag是-,忽略。现在我们的tags不是空 ,进入if,申明了一个tag context.
这里首先对xorm:"extends"进行了处理。
如果现在的字段是extends并且是一个结构体,那么递归调用mapType,如果当前的extends是一个指针,那么xorm会先把指针搞成struct,然后在进行mapType,这里有一个不大常用的golang 的关键字,fallthrouth,意思是继续执行下一个case而不去校验下一个条件是否符合。
继续向下会发现一个xorm的tagHandles,里面存放了每一种tag怎么处理。下图是默认的tag处理方式。
名称 | 作用 |
---|---|
<- | 只从db中取出来,不放进去 |
-> | 和上面相反 |
PK | primary key主键约束 |
NULL | 列可以为空 |
NOT | NOT |
AUTOINCR | 自增 |
DEFAULT | 默认值 |
CREATED | 标记创建时间 |
UPDATED | 和上个功能类似 |
DELETED | 和上个功能类似,时间不为空默认表示被删除了 |
version | 乐观锁 |
UTC | 时区处理(没用过) |
LOCAL | 本地时区 |
NOTNULL | 不能为空 |
INDEX | 是否是索引 |
UNIQUE | 是否唯一 |
CACHE | 当前字段缓存 |
NOCACHE | 不缓存 |
COMMENT | 注释 |
这里普及一下乐观锁和悲观锁
悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。
乐观锁可以代码实现。
继续进行,下一步会判断sql type,也就是golang 的数据类型和sql之间的数据类型的转换。
上面图处理了所有的类型映射。下面是所有能够处理的数据库类型
Bit = "BIT"
TinyInt = "TINYINT"
SmallInt = "SMALLINT"
MediumInt = "MEDIUMINT"
Int = "INT"
Integer = "INTEGER"
BigInt = "BIGINT"
Enum = "ENUM"
Set = "SET"
Char = "CHAR"
Varchar = "VARCHAR"
NVarchar = "NVARCHAR"
TinyText = "TINYTEXT"
Text = "TEXT"
NText = "NTEXT"
Clob = "CLOB"
MediumText = "MEDIUMTEXT"
LongText = "LONGTEXT"
Uuid = "UUID"
UniqueIdentifier = "UNIQUEIDENTIFIER"
SysName = "SYSNAME"
Date = "DATE"
DateTime = "DATETIME"
Time = "TIME"
TimeStamp = "TIMESTAMP"
TimeStampz = "TIMESTAMPZ"
Decimal = "DECIMAL"
Numeric = "NUMERIC"
Real = "REAL"
Float = "FLOAT"
Double = "DOUBLE"
Binary = "BINARY"
VarBinary = "VARBINARY"
TinyBlob = "TINYBLOB"
Blob = "BLOB"
MediumBlob = "MEDIUMBLOB"
LongBlob = "LONGBLOB"
Bytea = "BYTEA"
Bool = "BOOL"
Boolean = "BOOLEAN"
Serial = "SERIAL"
BigSerial = "BIGSERIAL"
Json = "JSON"
Jsonb = "JSONB"
golang | sql |
---|---|
int,int8,int16,int32,uint,uint8,uint32 | int |
int64,uint64 | bigint |
float32 | float |
float64 | double |
Complex64(复数),Complex128 | varchar |
array,slice,map | 如果内容是byte用blob,其他的用text |
bool | bool |
string | varchar |
struct | 结构体的处理是如果能转换成time.Time 就是time.Time如果不是,就是text |
指针 | 递归调用Type2SQLType()方法 |
… | 如果这些都没有命中,那么默认的sql type 是text |
类型映射完成之后,接下来映射列名,和表名类似,用的是一套接口。
上图是这个方法中对tag的处理,如果不存在tags 会进入到else中。
上图所示,else中有个关键的地方是,xorm首先判断当前的field是否是可以寻址的,可寻址的数据类型有
an element of a slice
an element of an addressable array
a field of an addressable struct
the result of dereferencing a pointer.
上图是个有趣的写法,把Interface转换为core.Conversion来判断当前的结构是否实现了Conversion接口。
如果实现了Conversion接口就用实现的去作,如果没有实现就用默认的type2SQLType方法去作转化。这个地方变量命名好像有点问题,把ok改成notok更直观。一次循环结束的结果是构造了一个col对象,这时候table.AddColumn(col)
上图所示,这里对id有个特殊处理,就是说如果你的代码里没有特殊表示主键字段,那么ID就是主键。
随后处理缓存,顺便一提,xorm中使用的缓存算法是LRU算法,使用方法也很简单,API设置一下就行了
cacher := xorm.NewLRUCacher(xorm.NewMemoryStore(), 1000)
x.SetDefaultCacher(cacher)
这个时候mapType方法跑完了
、
EXTENDS也跑完了。
setRefBean也跑完了,这时候所有的信息已经准备好了,接下来就是连接数据库进行查询了。
查询之前好像还缺少数据库语句,那么接下来要作的事情就是根据之前得到的信息,构造出数据库语句。
首先判断一下是不是手写的语句,如果是手写的语句,就不要去genGetSql了,我们现在不是那么进入这个if判断。这时候我们运行的是get方法所以api偷偷给我们加上了一个Limit(1)
上图的方法是把之前生成的列信息搞成sql列,搞列的过程中,有些tags就起作用了如果是OnlyToDB的,就不会在这个查询中出现这个列。其中也有对表别名的处理。
有了列之后开始去添加查询条件,就像一开始所说的,如果struct中的属性不是zero那么会默认当成一个查询条件。
这个方法先判断是不是调用了noAutoCondition,如果是就不拼接自动生成的条件,如果不是去buildConds,
buildConds方法有点长,只要记住它作的事情就是把struct中不是zero的值拿出来按照规范拼接成查询条件就行了。
statement.cond = statement.cond.And(autoCond)
这里statement.cond对象是另外一个包的对象,
这个包可以用golang 代码来写sql.
最后单独处理一下主键。
这里的buider对象是另外一个包的内容,这里不详细展开。
经过一系列转换之后,我们的sql语句拼接完成了,这时候可以去访问数据库了。
访问之前看看是不是开启了缓存,如果开启了缓存,现在缓存中找找看。
如果这个缓存中能够找到记录,xorm会打印日志,平时我没有看到日志,也没有设置过缓存,所以缓存默认是关闭的状态。
接下来进入数据库访问的过程。
首先调用
最后调用
所以真正做事情的对象是DB这个对象,DB这个对象组合了,sql.DB底层还是用的golang 自己的sql包
上图所示这个时候已经查询完成了,现在就是把查询得到的值在赋值给bean,方法大致和之前类似。
最后查询之后放入缓存,至于用不用缓存就看用户设置了。
以上就是xorm中一次get的整个流程。