gobatis

package module
v0.3.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 16, 2024 License: Apache-2.0 Imports: 21 Imported by: 0

README

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
}

Documentation

Index

Constants

View Source
const (
	MySQL = iota
	PostgreSQL
)
View Source
const (
	Select = "select"
	Insert = "insert"
	Update = "update"
	Delete = "delete"
	Mapper = "mapper"
	For    = "for"
	If     = "if"
	Value  = "value"
	VALUE  = "VALUE"
	Values = "values"
	VALUES = "VALUES"
	Where  = "where"
	WHERE  = "WHERE"
)

Variables

Null 采用结构体全名模式进行校验 通过判断接口接口实现,对后续的自定义结构体解析处理,和空值处理存在冲突,暂时没有处理冲突,接口判断是强制判定,无法让指针和值类型同时验证成功

Functions

func Analysis

func Analysis(element *etree.Element, ctx map[string]any) ([]string, string, []string, []any, error)

Analysis 解析xml标签

func AnalysisExpr

func AnalysisExpr(template string) string

AnalysisExpr 翻译表达式

func AnalysisForTemplate

func AnalysisForTemplate(template string, ctx map[string]any, v any) (string, string, []any, error)

AnalysisForTemplate 解析 for 标签的 文本模板 template for标签下的文本内容 ctx 并不是全局的上下文数据,如果 for循环的 item是个 obj ,则ctx将表示 obj v 如果 for循环的 item是个 基本类型 v 将代表它

func AnalysisTemplate

func AnalysisTemplate(template string, ctx map[string]any) (string, string, []any, error)

AnalysisTemplate 模板解析器

func Args

func Args(db reflect.Value, values []reflect.Value) (ctx reflect.Value, args map[string]any, tx reflect.Value, auto bool)

Args 参数赋值处理 处理定义函数的入参,返回一个参数序列给到后面的函数调用入参

func BaseTypeKey

func BaseTypeKey(v reflect.Value) string

BaseTypeKey 通过 BaseTypeKey 得到的变量默认全包名对泛型参数进行特殊处理的,不会加上类型中的 [xxx]定义部分信息

func DatabaseType

func DatabaseType(key string, dataType ToDatabase)

DatabaseType 对外提供添加 自定义sql语句数据类型解析支持

func Element

func Element(element *etree.Element, template string, ctx map[string]any) (string, string, []any, error)

func End

func End(tag string, auto bool, result []reflect.Value, errType, BeginCall reflect.Value)

End 错误提交及回滚

func ExecResultMapper

func ExecResultMapper(result []reflect.Value, exec sql.Result) (count int64, err error)

ExecResultMapper SQL执行结果赋值 规则: insert,update,delete,默认第一个返回值为 执行sql 影响的具体行数 insert 第二个返回参数是 自增长主键

func ForElement

func ForElement(element *etree.Element, template string, ctx map[string]any) (string, string, []any, error)

func GolangType

func GolangType(key string, dataType ToGolang)

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

func IfElement

func IfElement(element *etree.Element, template string, ctx map[string]any) (string, string, []any, error)

func MapperCheck

func MapperCheck(fun reflect.Value) (bool, error)

MapperCheck 检查 不同类别的sql标签 Mapper 函数是否符合规范 规则: 入参只能有一个并且只能是 map 或者 结构体,对返回值最后一个参数必须是error

func Namespace

func Namespace(namespace string) string

func NullConfig added in v0.2.2

func NullConfig(null any)

NullConfig 添加 null 数据 null 数据不会被 databaseToGolang 映射函数处理,相应的如果要在自定义空值中处理特殊的赋值逻辑,通过Scan 接口也可以实现,例如obj.String中处理逻辑 obj.String 做了对 普通字符串和数据库时间类型的通用匹配

type String struct {
	V string
}

func (s *String) Scan(data any) error {
	if data != nil {
		switch data.(type) {
		case time.Time:
			s.V = data.(time.Time).Format("2006-01-02 15:04:05")
		case string:
			s.V = data.(string)
		case []byte:
			s.V = string(data.([]byte))
		}
	}
	return nil
}

参考 obj/string.go

func QueryResultMapper

func QueryResultMapper(value reflect.Value, result []reflect.Value)

QueryResultMapper SQL 查询结果赋值 value 对应查询结果集 result 对应mapper函数返回值

func ResultMapping

func ResultMapping(value any) map[string]string

ResultMapping 解析结构体上的column标签 生成 数据库字段 到 结构体字段名 的映射匹配 value 如果传递的是结构体,则会解析对应的 column 标签 或者字段本身 value 如果是 map 则不会做任何处理,返回 nil

func Return

func Return(result []reflect.Value) (ret []reflect.Value)

Return 处理返回值排序 排序规则为 error 类型会放在最后面,其他顺序不变

func SelectCheck

func SelectCheck(columns []string, resultType any) (bool, error)

func StatementElement

func StatementElement(element *etree.Element, template string, ctx map[string]any) (string, string, []any, error)

func TimeData

func TimeData(value reflect.Value, data any) error

TimeData 时间类型数据

func TimeDataPointer

func TimeDataPointer(value reflect.Value, data any) error

func ToDatabaseTime

func ToDatabaseTime(data any) (any, error)

func ToDatabaseTimePointer

func ToDatabaseTimePointer(data any) (any, error)

func TypeKey

func TypeKey(t any) string

TypeKey 通过反射得到一个类型的类型字符串, 适用于普通类型

func UnTemplate

func UnTemplate(template string) string

UnTemplate 解析 {xx} 模板 解析为三个部分 ["{","xx","}"]

Types

type Combine

type Combine struct {
	Value     any
	Template  string
	Separator string
	Politic
}

func (Combine) ForEach

func (c Combine) ForEach() (string, string, []any, error)

type GoBatis

type GoBatis struct {
	*zap.Logger

	// SqlSource 用于保存 xml 配置的文件的根路径配置信息,Build会通过SqlSource属性去加载 xml 文件
	SqlSource string
	// NameSpaces 保存了每个 xml 配置的根元素构建出来的 Sql 对象
	NameSpaces map[string]*Sql

	Type int
	// contains filtered or unexported fields
}

func New

func New(db *sql.DB) *GoBatis

func (*GoBatis) Load

func (batis *GoBatis) Load(files embed.FS)

Load 加载 mapper 静态文件

func (*GoBatis) LoadByRootPath added in v0.3.1

func (batis *GoBatis) LoadByRootPath(root string, files embed.FS)

LoadByRootPath 根据提供的更路径及其 files 加载 mapper 文件 {root:表示根路径,根路径应该和提供的 files 嵌入文件资源对应}

func (*GoBatis) Logs

func (batis *GoBatis) Logs(logger *zap.Logger)

Logs 切换日志实例

func (*GoBatis) ScanMappers

func (batis *GoBatis) ScanMappers(mappers ...any)

ScanMappers 扫描解析

func (*GoBatis) Source

func (batis *GoBatis) Source(source string)

Source 加载 mapper文件 source 应当是项目中的 mapper 文件根路径文件夹名称

type MapperFunc

type MapperFunc func([]reflect.Value) []reflect.Value

type Other

type Other struct {
}

func (Other) ForEach

func (s Other) ForEach(value any, template string, separator string) (string, string, []any, error)

type Politic

type Politic interface {
	// ForEach value 待处理迭代的数据 ctx 上下文数据 item 上下文数据key序列
	ForEach(value any, template string, separator string) (string, string, []any, error)
}

Politic for 标签迭代实现接口扩展 标准切片之外的 List 数据支持

type Slice

type Slice struct {
}

func (Slice) ForEach

func (s Slice) ForEach(value any, template string, separator string) (string, string, []any, error)

type Sql

type Sql struct {
	// Element 表示 一个 Mapper 文件的更元素
	Element *etree.Element
	// Statement 表示每个 更元素下面的 sql语句标签
	Statement map[string]*etree.Element
}

Sql 单个xml的解析结构

func NewSql

func NewSql(root *etree.Element) *Sql

func (*Sql) LoadSqlElement

func (receiver *Sql) LoadSqlElement()

type Struct

type Struct struct {
}

func (Struct) ForEach

func (s Struct) ForEach(value any, template string, separator string) (string, string, []any, error)

type ToDatabase

type ToDatabase func(data any) (any, error)

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

type ToGolang

type ToGolang func(value reflect.Value, data any) error

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

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL