Skip to content

jimu-os/gobatis

Repository files navigation

GoBatis

Go Report Card

version

go1.21

GoBatisMyBatis go语言实现,GoBatis提供对 mapper 的上下文数据解析填充,并不保证对 sql 语句的语法检查,支持自定义数据映射.

XML 解析规则

gobatis 解析 xml 文件中的sql语句,会严格检查上下文中的数据类型,字符串类型参数会自定添加 '' 单引号,其他基础数据类型不会添加,对于复杂数据结构(复合结构,泛型结构体等)会持续跟进 ,目前仅支持基础数据类型。

上下文数据

上下文数据是由用户调用时候传递接,仅接受 map 或者结构体.

标签详情

标签 描述 功能
<mapper> 根节点
<insert> insert语句 生成插入语句
<select> select语句 生成查询语句
<update> update语句 生成更新语句
<delete> delete语句 生成删除语句
<where> where语句 where 标签内的标签将被解析,如果条件存在成立会自动补全 where关键字,若子标签完全不成立则不会补全where关键字
<for> for迭代 生成IN语句,指定需要生成IN条件的字段,可以生成对应的IN条件
<if> if条件 判断是否满足属性表达式的条件,满足条件就对标签内的sql进行解析

定义 Mapper

gobatis 中的 mapper 定义是基于结构体 和匿名函数字段来实现的(匿名函数字段,需要遵循一些规则):

  • 上下文参数,只能是结构体,指针结构体或者map
  • 至少有一个返回值,一个返回值只能是 error

快速入门

创建 table

创建一张表,用于测试

create table student
(
    id          int auto_increment primary key,
    name        varchar(20) null,
    age         int         null,
    create_time datetime    null
);

创建 映射模型

更具 数据库表,或者sql查询结果集 创建一个结构体用于接收查询数据,column 属性的值对应者 sql 表定义的列名

// Student 用户模型
type Student struct {
	Id         int    `column:"id"json:"id,omitempty"`
	Name       string `column:"name"json:"name,omitempty"`
	Age        int    `column:"age"json:"age,omitempty"`
	CreateTime string `column:"create_time"json:"create_time,omitempty"`
}

准备 运行环境

创建 mapper 结构体

type StudentMapper struct {
}

创建 mapper 文件

更具 mapper 结构体的名称创建一个 mapper xml文件

<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">

</mapper>

项目目录结构

|--root
|--model
|   |--student.go
|--mapper_test.go
|--StudentMapper.go
|--text.xml

初始化 gobatis

var err error
var open *sql.DB
var studentMapper *StudentMapper

func init() {
	studentMapper = &StudentMapper{}
	open, err = sql.Open("mysql", "xxx:xx@xx@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local")
	if err != nil {
		return
	}
	batis := gobatis.New(open)
	batis.Source("/")
	batis.ScanMappers(studentMapper)
}

数据插入数据

添加 mapper 方法

此时你的 mapper 应该是下面的样子

type StudentMapper struct {
	AddOne func(student model.Student) error
}

添加 xml insert 标签

根据定义的字段名称,对应在 mapper 文件中添加一个 id="AddOne" insert 标签, 标签内书写需要执行的sql语句,sql语句中的变量通过 {} 的形式去加载解析

<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <insert id="AddOne">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
</mapper>

调用执行插入数据

下面通过测试对刚刚定义的插入方法进行执行,所有的前置步骤都在上面的初始化中准备好了,直接调用 AddOne 字段即可实现数据插入

func TestInsert(t *testing.T) {
	s := model.Student{
		Name:       "test",
		Age:        1,
		CreateTime: time.Now().Format("2006-01-02 15:04:05"),
	}
	if err = studentMapper.AddOne(s); err != nil {
		t.Error(err.Error())
		return
	}
}

执行行数 和 自增主键

定义mapper字段 InsertId,它有3个返回值,第一个返回值是执行sql返回的影响行数,第二个返回值是返回自增长逐渐值, 默认第一个参数是返回影响行数。

type StudentMapper struct {
	AddOne   func(student model.Student) error
	InsertId func(student model.Student) (int64, int64, error)
}

定义xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <insert id="AddOne">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="InsertId">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
</mapper>

执行测试

func TestInsertId(t *testing.T) {
	var count, id int64
	s = model.Student{
		Name:       "test",
		Age:        2,
		CreateTime: time.Now().Format("2006-01-02 15:04:05"),
	}

	if count, id, err = studentMapper.InsertId(s); err != nil {
		t.Error(err.Error())
		return
	}
	t.Log("count:", count, "id:", id)
}

实现批量插入

添加新的方法,此时你的 mapper 应该是下面的样子

type StudentMapper struct {
	AddOne   func(student model.Student) error
	InsertId func(student model.Student) (int64, int64, error)
	Adds     func(ctx any) error
}

定义新的 mapper insert

此时你的的 mapper 应该是如下,添加了一个新的 insert 标签 id="Adds",其中 使用<for></for> 标签对传递的数组数据进行了解析 slice="{arr}" 属性指定了属性名称为 arr 的数据,item="stu"表示的是迭代过程中的对象参数,更具数据元素来定,如果是基础数据,那么代表数据本身

<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <insert id="AddOne">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="InsertId">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="Adds">
        insert into student (name, age, create_time) values
        <for slice="{arr}" item="stu">
            ({stu.Name},{stu.Age},{stu.CreateTime})
        </for>
    </insert>
</mapper>

调用执行

func TestSliceInsert(t *testing.T) {
	var arr []model.Student
	for i := 0; i < 10; i++ {
		s := model.Student{
			Name:       fmt.Sprintf("test_%d", i),
			Age:        i + 2,
			CreateTime: time.Now().Format("2006-01-02 15:04:05"),
		}
		arr = append(arr, s)
	}
	err = studentMapper.Adds(
		map[string]any{
			"arr": arr,
		},
	)
	if err != nil {
		t.Error(err.Error())
		return
	}
}

数据查询

定义查询

定义了mapper字段 QueryAll 查询全部采用对应的切片模型进行接收即可,查询多条数据结果集的时候任然可以使用单个模型接收, 只是单个模型的数据仅仅取到结果集的第一条数据。

type StudentMapper struct {
	AddOne   func(student model.Student) error
	InsertId func(student model.Student) (int64, int64, error)
	Adds     func(ctx any) error
	
	QueryAll func() ([]model.Student, error)
}
<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <insert id="AddOne">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="InsertId">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="Adds">
        insert into student (name, age, create_time) values
        <for slice="{arr}" item="stu">
            ({stu.Name},{stu.Age},{stu.CreateTime})
        </for>
    </insert>

    <select id="QueryAll">
        select * from student
    </select>
</mapper>

执行

func TestQueryAll(t *testing.T) {
	var stus []model.Student
	if stus, err = studentMapper.QueryAll(); err != nil {
		t.Error(err.Error())
		return
	}
	t.Log(stus)
}

分页查询

添加分页 mapper 字段 QueryPage,作为测试我们不进行参数传递,它返回3个参数,第一个参数是分页数据,第二个参数,是sql 条件所统计的总数, 查询mapper不返回 int64 的参数就不会自动统计数量

type StudentMapper struct {
	AddOne   func(student model.Student) error
	InsertId func(student model.Student) (int64, int64, error)
	Adds     func(ctx any) error

	QueryAll  func() ([]model.Student, error)
	QueryPage func() ([]model.Student, int64, error)
}
<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <insert id="AddOne">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="InsertId">
        insert into student (name, age, create_time)
        values ({Name},{Age},{CreateTime});
    </insert>
    <insert id="Adds">
        insert into student (name, age, create_time) values
        <for slice="{arr}" item="stu">
            ({stu.Name},{stu.Age},{stu.CreateTime})
        </for>
    </insert>

    <select id="QueryAll">
        select * from student
    </select>

    <select id="QueryPage">
        select * from student limit 2 offset 0
    </select>
</mapper>

执行

func TestQueryPage(t *testing.T) {
	var stus []model.Student
	var count int64
	if stus, count, err = studentMapper.QueryPage(); err != nil {
		t.Error(err.Error())
		return
	}
	t.Log("rows:", stus, "count:", count)
}

事务支持

定义一个数据修改操作,通过外部传递一个事务 tx 由它来完成数据库操作后的提交或是回滚,我们定义一个 Update 第二个参数传递事务

type StudentMapper struct {
	AddOne   func(student model.Student) error
	InsertId func(student model.Student) (int64, int64, error)
	Adds     func(ctx any) error

	QueryAll  func() ([]model.Student, error)
	QueryPage func() ([]model.Student, int64, error)

	Update func(student model.Student, tx *sql.Tx) (int64, error)
}

编写sql语句,修改年龄大于5的数据姓名修改为AAA

<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <!--  略 ..  -->
    <update id="Update">
        update student set name={Name} where age>{Age}
    </update>
</mapper>

运行测试

func TestUpdate(t *testing.T) {
	var begin *sql.Tx
	var count int64
	begin, err = open.Begin()
	if err != nil {
		t.Error(err.Error())
		return
	}
	u := model.Student{
		Name: "AAA",
		Age:  5,
	}
	count, err = studentMapper.Update(u, begin)
	if err != nil {
		t.Error(err.Error())
		return
	}
	begin.Commit()
	t.Log(count)
}

if 标签的使用

编写 xml,QueryIf 查询中使用了where标签,在where标签中,使用if来对上下文参数进行判断,如果存在 if标签将被解析到语句中

<?xml version="1.0" encoding="ISO-8859-1"?>
<mapper namespace="StudentMapper">
    <!--  略 ..  -->
    <select id="QueryIf">
        select * from student
        <where>
            <if expr="{name!=nil}">
                name={name}
            </if>
        </where>
    </select>
</mapper>

定义mapper字段

type StudentMapper struct {
	AddOne   func(student model.Student) error
	InsertId func(student model.Student) (int64, int64, error)
	Adds     func(ctx any) error

	QueryAll  func() ([]model.Student, error)
	QueryPage func() ([]model.Student, int64, error)

	Update func(student model.Student, tx *sql.Tx) (int64, error)

	QueryIf func(any) (model.Student, error)
}

运行测试

func TestIf(t *testing.T) {
	var stu model.Student
	args := map[string]any{
		"id": 1,
		"name": "test_0",
	}
	if stu, err = studentMapper.QueryIf(args); err != nil {
		t.Error(err.Error())
		return
	}
	t.Log(stu)
}

自定义映射数据

GoBatis 提供了上下文参数中复杂数据类型如何解析对应到 SQL 中对应的参数以及 SQL 中的查询结果集如何映射到自定义的复杂数据类型中。

Go参数解析到 SQL

// ToDatabase mapper 中sql解析模板对应的复杂数据据类型解析器
// data : 对应的数据本身
// 对应需要返回一个非结构体的基础数据类型(int float,bool,string) 更具需要构成的实际sql决定,后续的sql解析将自动匹配数据类
type ToDatabase func(data any) (any, error)

// DatabaseType 对外提供添加 自定义sql语句数据类型解析支持
func DatabaseType(key string, dataType ToDatabase) 

需要注册一个 ToDatabase 的解析器,GoBatis中对时间类型做了内置支持如下

func ToDatabaseTime(data any) (any, error) {
	t := data.(time.Time)
	return t.Format("2006-01-02 15:04:05"), nil
}

func ToDatabaseTimePointer(data any) (any, error) {
	t := data.(*time.Time)
	return t.Format("2006-01-02 15:04:05"), nil
}

SQL结果集解析到Go

// ToGolang 处理数据库从查询结果集中的复杂数据类型的赋值
// value : 是在一个结构体内的字段反射,通过该函数可以对这个字段进行初始化赋值
// data  : 是value对应的具体参数值,可能是字符串,切片,map
type ToGolang func(value reflect.Value, data any) error

// GolangType 对外提供添加 自定义结果集数据类型解析支持
// key 需要通过 TypeKey 函数获取一个全局唯一的标识符
// dataType 需要提供 对应数据解析逻辑细节可以参考 TimeData 或者 TimeDataPointer
func GolangType(key string, dataType ToGolang)

需要注册一个 ToGolang 的解析器,GoBatis中对时间类型做了内置支持如下

// TimeData 时间类型数据
func TimeData(value reflect.Value, data any) error {
	t := data.(string)
	parse, err := time.Parse("2006-04-02 15:04:05", t)
	if err != nil {
		return err
	}
	value.Set(reflect.ValueOf(parse))
	return nil
}

func TimeDataPointer(value reflect.Value, data any) error {
	t := data.(string)
	parse, err := time.Parse("2006-04-02 15:04:05", t)
	if err != nil {
		return err
	}
	if value.CanSet() {
		value.Set(reflect.ValueOf(&parse))
	}
	return nil
}