Go语言的ORM框架GoMybatis的学习经历

前言

最近开始学习Google爸爸的Go语言,经过两天的学习,准备尝试搭建一个Web开发框架,折腾到数据库这一块时,了解到Go的orm框架目前用的最多的是xorm,是一种类Hibernate的orm框架,做Java的相信都感受过被Hibernate统治的恐惧,生成SQL的代码都写在Java类里,导致后期优化SQL极其痛苦。不过万幸的是,经过搜索发现了一个名叫GoMyBatis的orm框架,相信大家从名字上看就已经了然了,这是一个类似于Mybatis的orm框架,进入Github自己查看后,了解到在XML解析上已经最大限度上兼容了Mybatis的语法结构,这对无论是已有的项目做迁移,还是新的项目开发都是极大的助力。

开始正题

第一步 安装

官网传送门
GitHub传送门

命令行安装方式:

1
2
go get github.com/zhuxiujia/GoMybatis
go get github.com/go-sql-driver/mysql

第二句是安装Go的Mysql Driver

第二步 准备数据库

1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_bin NOT NULL,
`sex` int(1) NOT NULL,
`create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

SET FOREIGN_KEY_CHECKS = 1;

第三步 编写MapperXML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://raw.githubusercontent.com/zhuxiujia/GoMybatis/master/mybatis-3-mapper.dtd">

<mapper>

<!--<cache eviction="LRU" type="" />-->

<resultMap id="userResultMap" tables="sys_user">
<id property="id" column="id" langType="int64"/>
<result property="name" column="name" langType="string"/>
<result property="sex" column="sex" langType="int"/>
<result property="createDate" column="create_date" langType="time.Time"/>
<result property="updateTime" column="update_time" langType="time.Time"/>
</resultMap>

<insert id="insert">
<selectKey resultType="int64" order="AFTER">
SELECT LAST_INSERT_ID() AS id
</selectKey>
INSERT INTO
sys_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name != nil">name,</if>
<if test="sex != nil">sex,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != nil">#{name},</if>
<if test="sex != nil">#{sex},</if>
</trim>
</insert>

<select id="selectById" resultMap="userResultMap">
SELECT
*
FROM sys_user
WHERE
id=#{id}
</select>
</mapper>

根据这个XML我们可以看到,主要的变化是在langType上(废话),langType的值改成了对应Go里的类型。
其次,是insert select update等标签的属性移除了一些,比如说parameterType useCache等。
具体可以查看一下header里的mybatis-3-mapper.dtd文件,了解哪些标签和属性发生了变更。

注意,这里有个问题,就是selectKey标签, 在Mybatis里放在insert标签中,用于自增主键的反向注入。但是GoMybatis里并没有对该标签进行支持,虽然dtd文件里没有移除该标签。

以下是在GitHub里Issue中询问后作者的回复。
图片

1
2
目前框架不会去支持自动递增的id,因为需要兼容类似Tidb等分布式数据库(自增id就没有任何意义而且性能提升不大)不管是 分布式数据库还是 分库分表中间件都不建议id为自增的,为了以后数据库扩展需要。
所以建议id不要用自增,而是给它赋值一个初始值比较好。建议代码里你可以使用uuid,string代替自增主键。

作者的建议是很好,不过有些时候做一些不考虑分布式及大数据的小玩意儿是,用自增主键来偷个小懒儿也是极好的。好吧,不支持就不支持,不是大事儿,毕竟就如作者所说,在大项目中单单从扩展需要上就应该使用UUID做为主键。

第四步 编写Pojo和Dao

1
2
3
4
5
6
7
8
9
10
11
package pojo

import "time"

type User struct {
Id int64 `json:"id"`
Name string `json:"name"`
Sex int `json:"sex"`
CreateDate time.Time `json:"createDate"`
UpdateTime time.Time `json:"updateTime"`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package dao

import . "pojo"
import "utils"
import "io/ioutil"

type UserDao struct {
Insert func(user User) (int64, error)
SelectById func(id int64) (User, error) `mapperParams:"id"`
}

var userDao = UserDao{}
func init(){
conn := utils.DBConnUtil{}.GetConn()
bytes, _ := ioutil.ReadFile("UserDao.xml")
conn.Engine.WriteMapperPtr(&userDao, bytes)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package utils

import (
_ "github.com/go-sql-driver/mysql"
"github.com/zhuxiujia/GoMybatis"
)

type DBConnUtil struct {
Engine GoMybatis.GoMybatisEngine
}

func (conn DBConnUtil) GetConn() DBConnUtil{
conn.Engine = GoMybatis.GoMybatisEngine{}.New()
err := conn.Engine.Open("mysql", "*") //此处请按格式填写你的mysql链接,这里用*号代替
if err != nil {
panic(err.Error())
}
conn.Engine.SetLogEnable(true)
//conn.Engine.SetLog(&GoMybatis.LogStandard{
// PrintlnFunc: func(messages []byte) {
// fmt.Printf(messages)
// },
//})
return conn
}

有朋友估计会好奇,为啥分成两个文件写,好像没必要,不着急,后面会讲到。

这里第一个代码块里15行,填入MapperXml的路径,是从开启运行服务的文件目录开始的相对路径。

|
···|src
······test.go
······|dao
·········|xxx.xml

假设是如上结构,则地址为src/dao/xxx.xml

第五步 编写Test

这里使用的是Go语言原生支持的testing框架,从Java过来的同学可以理解为Go版的JUnit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
."dao"
"fmt"
."pojo"
"testing"
)

func Test_insert(t *testing.T) {
//bytes, _ := ioutil.ReadFile("src/UserDao.xml")
//fmt.Println(bytes)
user := User{Name: "Victor", Sex: 1}


var result, err = GetUserDao().Insert(user)
if err != nil {
panic(err)
}
fmt.Println("result=", result)
fmt.Println("id=", user.Id)
}

func Test_selectById(t *testing.T) {
var result, err = GetUserDao().SelectById(2)
if err != nil {
panic(err)
}
fmt.Println("user=", result)
}

运行测试,发现通过了,但是检查select出来的数据时,发现user的create_date居然是默认值?检查数据库发现create_date是有值的呀,没办法了断点到源码里面找原因。

1
2
3
4
5
6
7
} else if tItemTypeFieldType.Kind() == reflect.Struct && tItemTypeFieldType.String() == "time.Time" {
newValue, e := time.Parse(string(time.RFC3339), value)
if e != nil {
return false
}
resultValue.Set(reflect.ValueOf(newValue))
}

断点跟踪到GoMybatisSqlResultDecoder.go : 162行时发现

1
newValue, e := time.Parse(string(time.RFC3339), value)

value的值为string的2019-04-11 23:46:17,执行过后e为true, 导致return false.
而检查time.RFC3339发现

1
RFC3339     = "2006-01-02T15:04:05Z07:00"

原来是time.Parse传入的第一个参数为模版,而当前数据库查出来的时间和模版对不上,所以出现了异常。
可问题是找到了,但是怎么处理就尴尬了,难不成所有查询SQL上都去格式化一次日期格式?那得把人恶心,再次万幸的是,因为GoMybatis目前在搜索引擎里能搜到的资料并不多,所以使用golang timestamp做为关键字搜索时,偶然看到了一篇帖子(传送门),里面提到了一个参数parseTime=true,加在数据库连接里,顿感有戏,虽然这篇帖子跟我遇到问题不同的。
立即加上后测试,发现一切正常了。

这里Java过来的同学要注意一下,zeroDateTimeBehavior=convertToNull这个参数相信很多人都很眼熟吧,因为Java中java.sql.Timestamp是不支持0000-00-00 00:00:00这样的时间的,而Mysql里却又是允许的,所以这样的时间被查出来转换成java.sql.Timestamp时就会报错,为了避免这种情况所以一般在数据库连接里我们都会加上这个参数,但是Go中允许0000-00-00 00:00:00这样的时间存在的,所以在驱动中则没有编写这个参数,加上反而会报错。

至此,GoMybatis就算成功跑起来了。
但是此时我们的DBConnUtil和UserDao每次被调用实际上都被初始化了一次,高并发的情况是很浪费性能和内存的事。在Java中因为有Spring这样的神器在,我们从来都不用担心这个问题,所有的Bean全部交给Spring去管理,加个annotation还特么自动扫描,连配置都省了。但是在这里,我们就得自己去实现一个单例模式了。

第六步 优化Dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package utils

import (
_ "github.com/go-sql-driver/mysql"
"github.com/zhuxiujia/GoMybatis"
"sync"
)

type DBEngineUtil struct {
Engine GoMybatis.GoMybatisEngine
}

var this *DBEngineUtil
var once sync.Once
func GetDBEngineUtil() *DBEngineUtil{

once.Do(func() {
this = &DBEngineUtil{
Engine: GoMybatis.GoMybatisEngine{}.New(),
}
})
return this
//this.Engine = GoMybatis.GoMybatisEngine{}.New()
//return this
}

func (this DBEngineUtil) Open() DBEngineUtil{
err := this.Engine.Open("mysql", "*") //此处请按格式填写你的mysql链接,这里用*号代替
if err != nil {
panic(err.Error())
}
this.Engine.SetLogEnable(true)
return this
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package dao

import (
"fmt"
"github.com/zhuxiujia/GoMybatis"
"io/ioutil"
. "pojo"
"sync"
"utils"
)

type UserDao struct {
GoMybatis.SessionSupport
Insert func(user User) (int64, error)
SelectById func(id int) (User, error) `mapperParams:"id"`
}

var this *UserDao
var engineUtil = utils.GetDBEngineUtil()
var once sync.Once


func GetUserDao() *UserDao{
once.Do(func() {
this = &UserDao{}
var bytes, _ = ioutil.ReadFile("src/dao/UserDao.xml")
engineUtil.Engine.WriteMapperPtr(this, bytes)
fmt.Println("****************************", "单例完成")
})
engineUtil.Open()
return this
}

这里面可以看到,如下一个语法结构的代码,

1
2
3
4
var once sync.Once
once.Do(func() {
...
})

这也是Go原生支持的单例模式的写法,保证线程安全的,可以放心使用。

调用方式现在就改变成

1
dao.GetUserDao().Insert(user)

酸爽宜人。

查看日志输出
图片

如此执行后,发现单例完成只输出了一次,完美。

到这里GoMybatis基本算上手,GoMybatis也支持事务及多数据源等功能,在这里就不一一赘述了(就是懒)。希望本篇blog,在帮助到GoMybatis初学者的同时,也能起到一点点推广作用。

后记

在测试使用GoMybatis事务时,又发现一个不得不提的事情。
想要启用事务,必须在Mapper的struct中添加如下这句代码。

1
NewSession func() (GoMybatis.Session, error)

这里还是只定义不实现,GoMybatis会通过代理来自动实现。
但是如果要给每个Dao或者Mapper里都去加上的话,确实是件很恶心的事。
在java,这种情况,我们常常会提一个父类出来,把这个定义移到父类中去,就不用每个dao里都去加了。不过在Go中没有继承这个概念,只有组合这个概念,又叫隐式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Car struct {
Color string
Brand string
}

type Truck struct {
Car
LoadCapacity int //载重量
}

func test() {
truck := Truck{}
truck.Brand = "A"
truck.Color = "blue"
truck.LoadCapacity = 20
}

如上所示,在Java中货车Truck Struct应该继承Car Struct,在Go中只需要Truck Struct中加上Car,并且不定义变量名,实际意义就是把Car组合到Truck中,而调用Car里的属性时就可以直接使用truck.Color,感受不到Car的存在,所以也叫隐式调用。

因此我给UserDao创建了一个“父类”,BaseDao。

1
2
3
4
5
6
7
package dao

import "github.com/zhuxiujia/GoMybatis"

type BaseDao struct {
NewSession func() (GoMybatis.Session, error)
}

然后实际测试使用发现通过隐式调用定义的NewSession并没有被识别到,没有被注入代理方法。
断点跟踪阅读源码之后发现:

1
2
3
4
5
6
7
8
9
if f.CanSet() {
switch ft.Kind() {
case reflect.Struct:
case reflect.Func:
if buildFunc != nil {
buildRemoteMethod(f, ft, sf, buildFunc(sf))
}
}
}

GoMybatisProxy.go: 48行,case reflect.Struct里没有任何实现。当执行到这里时我们定义的隐式调用被跳过了。看来GoMybatis并不支持该做法。
不死心的我又跑到GitHub上去提问:
图片

果然得到了暂未支持的回复,且作者说因为隐式调用本质上是组合而不是继承,有重复定义的风险,暂时不考虑这样做,未来可能优化一个统一的NewSession接口。

图片

然后我也就此放弃了。。。
可是!!!就在刚刚,间隔一天的刚刚,又再次收到作者回复,告诉我已经做好了统一的隐式调用的NewSession接口!并且已经更新了!大哥!你的未来就是明天吗?太特么激动了。

赶紧更新测试,了解过后发现就是不让我们自己定义BaseDao,统一用官方提供的GoMybatis.SessionSupport。里边儿代码如下:

1
2
3
4
5
6
package GoMybatis


type SessionSupport struct {
NewSession func() (Session, error) //session为事务操作
}

一眼看完,跟咱之前定义的BaseDao一毛一样,就是改了个名儿。
测试运行后,发现根本跑不通,反馈给作者,作者说不可能,他运行测试样例都能跑通。
害得我又只能断点跟踪进去找证据:

1
2
3
4
5
6
7
UseMapperValue(bean, func(funcField reflect.StructField) func(args []reflect.Value, tagArgs []TagArg) []reflect.Value {
//构建期
var funcName = funcField.Name
var returnType = returnTypeMap[funcName]
if returnType == nil {
panic("[GoMybatis] struct have no return values!")
}

在GoMybatis.go: 64行, 这里抛出了异常。panic("[GoMybatis] struct have no return values!")
断点到这里时,funcName = NewSession ,returnType = nil。确认Bug无疑。

作者说他能跑通,我又跑去运行测试样例,发现也确实能跑通,我就纳闷儿了。。。
不信邪,仔细检查发现测试样例中两种NewSession的方式都写着。

1
2
3
type ExampleActivityMapper struct {
GoMybatis.SessionSupport //session事务操作 写法1. ExampleActivityMapper.SessionSupport.NewSession()
NewSession func() (GoMybatis.Session, error) //session事务操作.写法2 ExampleActivityMapper.NewSession()

不能跑通才奇了怪了,把老方式写法2给注释掉,果然也报错了,跟我遇到的一毛一样。
再次反馈后作者已fix。

总结

不知不觉就写了这么多了,总体试用下来感觉还是非常不错的,熟悉的配方熟悉的味道,基本就是没有Spring加持的Mybatis。同时也深深感受到Spring的Java人的影响,虽然极大的方便我们开发,同时也极大的限制了我们新程序员对于底层原理的理解,因此也限制了他们学习新语言的兴趣。至于性能上,因为Go语言的高效,性能并没有什么问题,作者也给出了高并发下测试结果,有兴趣的同学可自行去Github上查看。

另外GoMybatis的源码量并不大,也就几十个.go文件,建议可以阅读以下,对于初学Go语言的同学应该会有很大的帮助,特别是反射这一块。

再次后记

GoMybatis在事务上还是略显简陋, 不支持嵌套事务, 详情请看Issue传送门.

如果哪位帅哥美女看到这里, 请看我另一篇博文Go语言ORM框架XORM上手实战及全自动事务托管实现(类似Spring)

至此,GoMybatis的学习经验讲述完毕!Thanks for your reading。

文章目录
  1. 1. 前言
  2. 2. 开始正题
    1. 2.1. 第一步 安装
    2. 2.2. 第二步 准备数据库
    3. 2.3. 第三步 编写MapperXML
    4. 2.4. 第四步 编写Pojo和Dao
    5. 2.5. 第五步 编写Test
    6. 2.6. 第六步 优化Dao
    7. 2.7. 后记
  3. 3. 总结
  4. 4. 再次后记
,