一个简单、强大的Java动态SQL和参数生成工具库。文档地址
My life for Auir!
SQL对开发人员来说是核心的资产之一,在开发中经常需要书写冗长、动态的SQL,许多项目中仅仅采用Java来书写动态SQL,会导致SQL分散、不易调试和阅读。所谓易于维护的SQL应该兼有动态性和可调试性的双重特性。在Java中书写冗长的SQL,虽然能很好的做到动态性,缺大大降低了SQL本身的可调试性,开发人员必须运行项目调试打印出SQL才能知道最终的SQL长什么样。所以为了做到可调试性,开发人员开始将SQL单独提取出来存放到配置文件中来维护,这样方便开发人员复制出来粘贴到SQL工具中来直接运行,但无逻辑功能的配置文件虽然解决了可调试性的问题,却又丧失了动态SQL的能力。所以,才不得不诞生出类似于mybatis这样灵活的半ORM工具来解决这两个的问题,但众多的项目却并未集成mybaits这样的工具。
Zealot是基于Java语言开发的SQL及对应参数动态拼接生成的工具包,其核心设计目标是帮助开发人员书写和生成出具有动态的、可复用逻辑的且易于维护的SQL及其对应的参数。为了做到可调试性,就必须将SQL提取到配置文件中来单独维护;为了保证SQL根据某些条件,某些逻辑来动态生成,就必须引入表达式语法或者标签语法来达到动态生成SQL的能力。因此,两者结合才诞生了Zealot。
注:zealot即狂热者,是游戏星际争霸中的单位。
- 轻量级,jar包仅仅
72
k大小,简单、无副作用的集成和使用 - 提供了纯
Java
代码和XML
两种方式书写维护SQL Java
的方式采用流式API的方式书写动态SQL,易于书写和阅读XML
的方式让SQL和Java代码解耦和,易于维护- 具有动态性、可复用逻辑和可半调试性的优点
- 具有可扩展性,可自定义
XML
标签和处理器来完成自定义逻辑的SQL和参数生成
- 完善各种动态SQL操作符和XML标签的
与
、或
、非
的逻辑 - 新增了Zealot配置和调用的更多重载方法
- 新增了通过注解来配置自定义标签和其对应的Handler
- 新增了扫描XML文件所在位置(可多个位置,用逗号隔开,目录或具体XML文件均可),默认扫描项目资源目录下
zealot
目录及子目录下的xml文件 - 新增了
removeIfExist()
方法用来消除where 1 = 1
等类似无用SQL片段的SQL
适用于Java (web)项目,JDK1.6及以上
这里以Maven为例,Maven的引入方式如下:
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>zealot</artifactId>
<version>1.3.1</version>
</dependency>
compile 'com.blinkfox:zealot:1.3.1'
如果你的项目是SpringBoot2.x,那么可以直接引入Zealot的Starter,且几乎不需要做任何其他方面的配置即可使用,这里是zealot-spring-boot-starter的引入及使用方式说明。
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>zealot-spring-boot-starter</artifactId>
</dependency>
如果你的项目是SpringBoot2.x
,那么可以直接引入Zealot的Starter,且几乎不需要做任何其他方面的配置即可使用,这里是zealot-spring-boot-starter的引入及使用方式说明。
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>zealot-spring-boot-starter</artifactId>
</dependency>
在Java中书写中等长度的SQL,用+
连接的字符串尤其是动态字符串,会导致SQL的可读性极差且拼接性能较低,在Zealot v1.0.4
版本中提供了一个额外高效的SQL字符串链式拼接工具Khala
(已被弃用),但Khala只提供拼接字符串的功能,并不具有返回动态SQL和参数的特性,便决定在v1.1.0
版本中新增了ZealotKhala
,ZealotKhala
类也采用流式API的方式可以书写出更流畅的动态SQL,且会得到动态SQL的有序参数。其使用示例如下:
public class ZealotKhalaTest {
/**
* 测试使用ZealotKhala书写的sql.
*/
@Test
public void testSql() {
String userName = "zhang";
String startBirthday = "1990-03-25";
String endBirthday = "2010-08-28";
Integer[] sexs = new Integer[]{0, 1};
SqlInfo sqlInfo = ZealotKhala.start()
.select("u.id, u.name, u.email, d.birthday, d.address")
.from("user AS u")
.leftJoin("user_detail AS d").on("u.id = d.user_id")
.where("u.id != ''")
.andLike("u.name", userName)
// 该doAnything方法可以在拼接期间做任何代码插入和注入,如果是Java8的话,可以转为Lamda表达式
.doAnything(true, new ICustomAction() {
@Override
public void execute(final StringBuilder join, final List<Object> params) {
join.append("abc111");
params.add(5);
log.info("执行了自定义操作,可任意拼接字符串和有序参数...");
}
})
.andMoreThan("u.age", 21)
.andLessThan("u.age", 13)
.andMoreEqual("d.birthday", startBirthday)
.andLessEqual("d.birthday", endBirthday)
.andBetween("d.birthday", startBirthday, endBirthday)
.andIn("u.sex", sexs)
.andIsNotNull("u.state")
.orderBy("d.birthday").desc()
.end();
String sql = sqlInfo.getSql();
Object[] arr = sqlInfo.getParamsArr();
// 断言并输出sql信息
assertEquals("SELECT u.id, u.name, u.email, d.birthday, d.address FROM user AS u "
+ "LEFT JOIN user_detail AS d ON u.id = d.user_id WHERE u.id != '' AND u.name LIKE ? "
+ "abc111 AND u.age > ? AND u.age < ? AND d.birthday >= ? AND d.birthday <= ? "
+ "AND d.birthday BETWEEN ? AND ? AND u.sex IN (?, ?) AND u.state IS NOT NULL "
+ "ORDER BY d.birthday DESC", sql);
assertArrayEquals(new Object[]{"%zhang%", 5, 21, 13, "1990-03-25", "2010-08-28",
"1990-03-25", "2010-08-28", 0, 1} ,arr);
log.info("-- testSql()方法生成的sql信息:\n" + sql + "\n-- 参数为:\n" + Arrays.toString(arr));
}
}
打印结果如下:
-- testSql()方法生成的sql信息:
SELECT u.id, u.name, u.email, d.birthday, d.address FROM user AS u LEFT JOIN user_detail AS d ON u.id = d.user_id WHERE u.id != '' AND u.name LIKE ? abc111 AND u.age > ? AND u.age < ? AND d.birthday >= ? AND d.birthday <= ? AND d.birthday BETWEEN ? AND ? AND u.sex in (?, ?) AND u.state IS NOT NULL ORDER BY d.birthday DESC
-- 参数为:
[%zhang%, 5, 21, 13, 1990-03-25, 2010-08-28, 1990-03-25, 2010-08-28, 0, 1]
对于很长的动态或统计性的SQL采用Java书写不仅冗长,且不易于调试和维护,因此更推荐你通过xml文件来书写sql,使得SQL和Java代码解耦,更易于维护和阅读。使用xml方式需要经过一些配置,使系统能读取到xml中的SQL信息到缓存中,使动态拼接更高效。
自1.3.0开始,沿用了默认大于配置的方式,为了能够识别到Zealot XML文件的命名空间和路径,可以不需要开发者再在ZealotConfig文件中的configXml()
方法中配置了,可以直接指定XML文件所在项目的资源目录或相对路径即可,且XML文件的zealots
根节点需要添加该文件区别其他zealot xml文件的命名空间(nameSpace)。可以直接在你项目的初始化启动类或者方法里面做如下配置即可:
// 在你的启动类或方法中加入该语句,来初始化加载zealot xml目录中的xml命名空间和其对应的位置
// 如果参数 xmlLocations 值为空的话,则默认扫描你项目资源目录(及子目录)`zealot`下的所有Zealot XML SQL文件.
ZealotConfigManager.getInstance().initLoadXmlLocations("myzealots/xml");
注:参数
xmlLocations
,表示zealot的XML文件所在的位置,多个用逗号隔开,可以是目录也可以是具体的xml文件.
如果采用这种扫描配置的方式,zealot中XML文件的zealots
根节点中需要指定命名空间(nameSpace)属性,用来和其他zealot xml文件区分该来,同时方便zealot的调用,XML示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 查询老师相关的SQL信息,命名空间nameSpace为zealots的根节点属性,各xml文件的nameSpace不能相同,如果不填nameSpace则需在ZealotConfig中配置与xml的nameSpace映射值. -->
<zealots nameSpace="myTeacher">
<!-- 根据Id查询学生信息. -->
<zealot id="queryTeacherById">
SELECT * FROM t_teacher AS t WHERE
<equal field="t.c_id" value="id"/>
</zealot>
</zealots>
/**
* 测试从扫描的zealot xml文件中生成sql.
*/
@Test
public void testQueryTeacherSql() {
SqlInfo sqlInfo = Zealot.getSqlInfo("myTeacher", "queryTeacherById",
ParamWrapper.newInstance("id", "123").toMap());
String expectedSql = "SELECT * FROM t_teacher AS t WHERE t.c_id = ?";
assertEquals(expectedSql, sqlInfo.getSql());
assertArrayEquals(new Object[]{"123"}, sqlInfo.getParamsArr());
}
当你的系统中需要用到自定义标签时,对你的Handler的配置也不需要再在ZealotConfig文件中的configTagHandler()
方法中配置了,可以直接指定各Handler类所在项目的资源目录或相对路径即可。同时在Handler中使用@Tagger
和@Taggers
注解来标注该标签处理器所对应的标签、前缀、操作符等,配置和Handler使用示例如下:
// 在你的启动类或方法中加入该语句,来初始化加载zealot handler目录中的注解和标签处理器
// 参数 handlerLocations 值不能为空,如果为空,也不会报错,不过不会产生任何作用而已.
ZealotConfigManager.getInstance()
.initLoadXmlLocations("myzealots/xml, xml/zealots, abc/zealot-user.xml")
.initLoadHandlerLocations("com.blinkfox.zealot.test.handler, com.blinkfox.myProject.zealot.Hello.java");
注:参数
handlerLocations
,表示zealot的XML文件所在的位置,多个用逗号隔开,可以是目录也可以是具体的xml文件.
Handler和注解的示例如下:
import com.blinkfox.zealot.bean.BuildSource;
import com.blinkfox.zealot.bean.SqlInfo;
import com.blinkfox.zealot.config.annotation.Tagger;
import com.blinkfox.zealot.core.IConditHandler;
/**
* 测试Tagger注解的Handler.
*
* @author blinkfox on 2018/4/28.
*/
@Tagger(value = "helloTagger", prefix = "Hello", symbol = "Tagger")
public class TaggerTestHandler implements IConditHandler {
/**
* 由于只是用来测试注解,所以这里只做简单的拼接.
* @param source 构建所需的资源对象
* @return sqlInfo
*/
@Override
public SqlInfo buildSqlInfo(BuildSource source) {
source.getSqlInfo().getJoin().append(source.getPrefix())
.append(" ").append(source.getSuffix());
return source.getSqlInfo();
}
}
import com.blinkfox.zealot.bean.BuildSource;
import com.blinkfox.zealot.bean.SqlInfo;
import com.blinkfox.zealot.config.annotation.Tagger;
import com.blinkfox.zealot.config.annotation.Taggers;
import com.blinkfox.zealot.core.IConditHandler;
/**
* 测试'Taggers'注解的Handler.
* @see com.blinkfox.zealot.config.annotation.Taggers
*
* @author blinkfox on 2018/4/28.
*/
@Taggers({
@Tagger(value = "hello", prefix = "hello", symbol = "blinkfox"),
@Tagger(value = "hi", prefix = "hi", symbol = "blinkfox"),
@Tagger(value = "hw", prefix = "hello", symbol = "world")
})
public class TaggersTestHandler implements IConditHandler {
/**
* 由于这个类只是用来测试注解,所以这里只做简单的字符串拼接.
* @param source 构建所需的资源对象
* @return sqlInfo
*/
@Override
public SqlInfo buildSqlInfo(BuildSource source) {
source.getSqlInfo().getJoin().append(source.getPrefix())
.append(" ").append(source.getSuffix());
return source.getSqlInfo();
}
}
这样就可以在例的xml文件中使用自定义的标签了,xml中的使用示例如下:
<!-- 根据Id查询课程信息 -->
<zealot id="testTaggerHanderSql">
<!-- helloTagger 的自定义标签在 com.blinkfox.zealot.test.handler.TaggerTestHandler 类中通过自定义注解来定义和实现的 -->
<helloTagger />
<!-- sayHello 等自定义标签在 com.blinkfox.zealot.test.handler.TaggersTestHandler 类中通过自定义注解来定义和实现的 -->
<hello />
<hi />
<hw />
</zealot>
Zealot的配置中,除了xml和Handler的配置外,还有一些其他普通配置项,主要有以下三种,在启动类或者方法中,通过Java的配置方式如下:
NormalConfig.getInstance() // 获取普通配置的唯一实例
.setDebug(false) // 设置是否开启debug模式,这样每次调用都会实时从最新的xml文件中获取sql,默认值为false.
.setPrintBanner(true) // 设置是否打印zealot的启动banner,默认为true.
.setPrintSqlInfo(true); // 设置是否打印zealot的sql日志,默认为true.
老版本的配置方式是采用Java配置的方式,需要新建一个ZealotConfig
配置类(继承自AbstractZealotConfig
),老版本通过Java启动类或方法加载配置的方式即为:
// 直接指定类的class
ZealotConfigManager.getInstance().initLoad(MyZealotConfig.class);
// 或者类的class全名
ZealotConfigManager.getInstance().initLoad("com.blinkfox.config.MyZealotConfig");
// 或者类的实例,这个实例就可以直接从Spring等容器中获取了,不一定是new出来的
ZealotConfigManager.getInstance().initLoad(new MyZealotConfig());
新版本也完全兼容以前的版本,所以配置方式只需扩展两个参数即可,如下:
ZealotConfigManager.getInstance().initLoad(MyZealotConfig.class, "zealot/xml", "com.blinkfox.zealot.test.handler");
// 或者
ZealotConfigManager.getInstance().initLoad("com.blinkfox.config.MyZealotConfig", "zealot/xml", "com.blinkfox.zealot.test.handler");
// 或者
ZealotConfigManager.getInstance().initLoad(new MyZealotConfig(), "zealot/xml", "com.blinkfox.zealot.test.handler");
当然,不填写后面两个的扫描位置,继续再MyZealotConfig
类中指定配置xml路径和handler对应的标签、class也完全是可以的。
在你的Java web项目项目中,创建一个继承自AbstractZealotConfig
的核心配置类,如以下示例:
package com.blinkfox.config;
import XmlContext;
import AbstractZealotConfig;
/**
* 我继承的zealotConfig配置类
* Created by blinkfox on 2018/05/01.
*/
public class MyZealotConfig extends AbstractZealotConfig {
/**
* 1.3.0版本之后,该方法可以不必再覆盖了,按需覆盖。
*/
@Override
public void configNormal(NormalConfig normalConfig) {
// 1.1.5版本新增的方法
}
/**
* 1.3.0版本之后,该方法可以不必再覆盖了,按需覆盖。
*/
@Override
public void configXml(XmlContext ctx) {
}
/**
* 1.3.0版本之后,该方法可以不必再覆盖了,按需覆盖。
*/
@Override
public void configTagHandler() {
}
}
代码解释:
(1).
configNormal()
方法是1.1.5
版本新增的方法,主要用来配置Zealot的通用配置信息,包括是否开启debug
模式,加载完毕之后是否打印banner
等;
(2).
configXml()
方法主要配置你自己SQL所在XML文件的命名标识和对应的路径,这样好让zealot能够读取到你的XML配置的SQL文件;
(3).
configTagHandler()
方法主要是配置你自定义的标签和对应标签的处理类,当你需要自定义SQL标签时才配置。
注: 以上三个方法自1.3.0版本之后,不再必须覆盖了,有了扫描和默认配置功能之后就可以按需覆盖了。
接下来就是要在启动过程中加载配置类到内存中,如果你是Java web项目,则可以在web.xml
文件中来引入zealot,这样容器启动时才会去加载和缓存对应的xml文档,示例配置如下:
<!-- zealot相关配置的配置 -->
<context-param>
<!-- paramName必须为zealotConfigClass名称,param-value对应刚创建的Java配置的类路径 -->
<param-name>zealotConfigClass</param-name>
<param-value>com.blinkfox.config.MyZealotConfig</param-value>
<!-- zealotXmlLocations参数,表示zealot xml 文件所在的位置(可以是xml文件,也可以是目录),如果是多个位置则用逗号(',')分割 -->
<param-name>zealotXmlLocations</param-name>
<param-value></param-value>
<!-- zealotHandlerLocations参数,表示zealot 自定义标签处理器所在的位置(只是目录),如果是多个位置则用逗号(',')分割. -->
<param-name>zealotHandlerLocations</param-name>
<param-value>com.blinkfox.zealot.test.handler</param-value>
</context-param>
<!-- listener-class必须配置,JavaEE容器启动时才会执行 -->
<listener>
<listener-class>com.blinkfox.zealot.loader.ZealotConfigLoader</listener-class>
</listener>
!> 注:如果你不是Java web项目,或者你就想通过Java代码来初始化加载zealot的配置信息,可以这样来做:
ZealotConfigManager.getInstance().initLoad(MyZealotConfig.class);
接下来,就开始创建我们业务中的SQL及存放的XML文件了,在你项目的资源文件目录中,不妨创建一个管理SQL的文件夹,我这里取名为zealotxml
,然后在zealotxml
文件夹下创建一个名为zealot-user.xml
的XML文件,用来表示用户操作相关SQL的管理文件。在XML中你就可以创建自己的SQL啦,这里对user
表的两种查询,示例如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- 命名空间nameSpace属性是1.3.0开始增加的属性,如果配置了就可以不用在ZealotConfig中配置指定了,便于和其他zealot xml文件区分开. -->
<zealots nameSpace = "myUser">
<!-- 根据Id查询用户信息 -->
<zealot id="queryUserById">
select * from user where
<equal field="id" value="id"/>
</zealot>
<!-- 根据动态条件查询用户信息 -->
<zealot id="queryUserInfo">
select * from user where
<like field="nickname" value="nickName"/>
<andLike match="?email != empty" field="email" value="email"/>
<andBetween match="?startAge > 0 || ?endAge > 0" field="age" start="startAge" end="endAge"/>
<andBetween match="?startBirthday != empty || ?endBirthday != empty" field="birthday" start="startBirthday" end="endBirthday"/>
<andIn match="?sexs != empty" field="sex" value="sexs"/>
order by id desc
</zealot>
</zealots>
代码解释:
(1).
zealots
代表根节点,其下每个zealot
表示你业务中需要书写的一个完整SQL。
(2). 其中的
like
、andLike
、andBetween
、andIn
等标签及属性表示要动态生成的sql类型,如:等值查询、模糊查询、In查询、区间查询等;
(3). 标签中的属性match表示匹配到的条件,如果满足条件就生成该类型的SQL,不匹配就不生成,从而达到了动态生成SQL的需求,如果不写match,则表示必然生成;
(4). field表示对应的数据库字段;
(5). value、start、end则表示对应的参数。
(6).
?email != empty
前面的?
表示属性的安全访问,即使email不存在的时候也不会抛出异常,仍会返回false。更详细的使用可以参考MVEL属性安全访问的表达式语法。
回到你的Zealot核心配置类中,配置你Java代码中需要识别这个XML的标识和XML路径,我这里的示例如下:
package com.blinkfox.config;
import XmlContext;
import AbstractZealotConfig;
/**
* 我继承的zealotConfig配置类
* Created by blinkfox on 2016/11/4.
*/
public class MyZealotConfig extends AbstractZealotConfig {
public static final String USER_ZEALOT = "user_zealot";
@Override
public void configNormal(NormalConfig normalConfig) {
normalConfig.setDebug(true) // 是否开启debug模式,默认为false
.setPrintBanner(true) // 加载配置信息完毕后是否打印Banner,默认为true
.setPrintSqlInfo(true); // 是否打印Sql信息,默认为true,注意日志级别为info时才打印,如果是warn、error则不打印
}
@Override
public void configXml(XmlContext ctx) {
// 注:如果在zealots根节点中指定了命名空间nameSpace属性的值,那么就可以不用在此方法中配置了,只需指定扫描XML的位置即可.
ctx.add(USER_ZEALOT, "/zealotxml/zealot-user.xml");
}
@Override
public void configTagHandler() {
}
}
代码解释:
(1).
ctx.add(USER_ZEALOT, "/zealotxml/zealot-user.xml");
代码中第一个参数USER_ZEALOT
表示的是对XML做唯一标识的自定义静态常量,第二个参数就是你创建的对应的XML的资源路径。
最后,就是一个UserController的调用测试类,这里的目的用来调用执行,测试我们前面配置书写的SQL和参数,参考代码如下:
/**
* 用户信息相关的控制器
* Created by blinkfox on 16/7/24.
*/
public class UserController extends Controller {
public void queryUserById() {
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("id", "2");
SqlInfo sqlInfo = Zealot.getSqlInfo(MyZealotConfig.USER_ZEALOT, "queryUserById", paramMap);
String sql = sqlInfo.getSql();
Object[] params = sqlInfo.getParamsArr();
List<User> users = User.userDao.find(sql, params);
renderJson(users);
}
public void userZealot() {
// 构造测试需要的动态查询的参数
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("nickName", "张");
paramMap.put("email", "san");
paramMap.put("startAge", 23);
paramMap.put("endAge", 28);
paramMap.put("startBirthday", "1990-01-01 00:00:00");
paramMap.put("endBirthday", "1991-01-01 23:59:59");
paramMap.put("sexs", new Integer[]{0, 1});
// 执行Zealot方法,得到完整的SQL和对应的有序参数
SqlInfo sqlInfo = Zealot.getSqlInfo(MyZealotConfig.USER_ZEALOT, "queryUserInfo", paramMap);
String sql = sqlInfo.getSql();
Object[] params = sqlInfo.getParamsArr();
// 执行SQL查询并返回结果
List<User> users = User.userDao.find(sql, params);
renderJson(users);
}
}
代码解释:
(1). 说明一下,我测试项目中采用的框架是JFinal,你自己具体的项目中有自己的SQL调用方式,而Zealot的目的只是生成SQL和对应的有序参数而已。
(2). Zealot.getSqlInfo()方法有三个参数,第一个参数表示前面所写的XML的标识名称,第二个表示你XML中具体想生成的SQL的zealot id,第三个表示生成动态SQL的参数对象,该对象可以是一个普通的Java对象,也可以是Map等。
结果,第二个方法userZealot()中生成的SQL和参数打印如下:
生成SQL结果:
--生成sql的为:
select * from user where nickname LIKE ? AND email LIKE ? AND age BETWEEN ? AND ? AND birthday BETWEEN ? AND ? AND sex in (?, ?) order by id desc
-- 生成sql的参数为:
[%张%, %san%, 23, 28, 1990-01-01 00:00:00, 1991-01-01 23:59:59, 0, 1]
以下四个调用方法中,除了getSqlInfo(String nameSpace, String zealotId, Object paramObj)
方法外都是1.3.0版本新增的调用方法。
getSqlInfo(String nameSpace, String zealotId)
: 根据命名空间和zealotId来获取SQL,由于没有参数,所以SQL必然是静态SQL才行getSqlInfo(String nameSpace, String zealotId, Object paramObj)
: 根据命名空间、zealotId和参数对象(JavaBean或者Map)来获取SQLgetSqlInfoSimply(String nsAtZealotId)
: 将命名空间和zealotId合并在一起,通过@@
符号来分割,由于没有参数,所有SQL必然是静态SQL才行getSqlInfoSimply(String nsAtZealotId, Object paramObj)
: 将命名空间和zealotId合并在一起,通过@@
符号来分割,和参数对象(JavaBean或者Map)来获取SQL
Zealot的核心功能就在于它XML格式的SQL配置文件。配置文件也仅仅是一个普通的XML文件,在XML中只需要少许的配置就可以动态生成自己所需要的查询条件。在XML中zealots
标签作为根标签,其中的zealot
则是一个独立SQL的元素标签,在zealot
标签中才包含like
、andLike
、andBetween
、andIn
、isNull
、text
、import
、choose
等条件标签,以下重点介绍各条件标签。
Zealot中默认自带了以下几类条件标签,分别是:equal
、like
、between
、in
、isNull
、text
、import
、choose
等,分别对应着SQL查询中的等值匹配条件、模糊匹配条件、区间匹配条件、范围匹配条件及其他逻辑操作条件;某些条件标签又各自额外附带了两个连接前缀和否定情况,分别是:AND
、OR
、NOT
,用于表示逻辑与
、或
、非
的情形。各标签的属性和生成SQL的示例如下:
<equal match="" field="" value="" />
<andEqual match="" field="" value="" />
<orEqual match="" field="" value="" />
- match,表示匹配条件。非必要(填)属性,如果不写(填)此属性,则视为必然生成此条件SQL片段;否则匹配结果为true时才生成,匹配结果为false时,不生成。匹配规则使用
MVEL2
表达式,关于MVEL的语法文档参考这里。 - field,表示对应数据库的字段,可以是数据库的表达式、函数等。必要(填)属性。
- value,表示参数值,对应Java中的名称。必要(填)属性,值也是使用
MVEL2
做解析。
标签:<equal field="nickname" value="nickName"></equal>
SQL片段的生成结果:nickname = ?
解释:必然生成此条SQL片段和参数
<andEqual match="?email != empty" field="email" value="email"></andEqual>
SQL片段的生成结果:AND email = ?
解释:如果email不等于空时,才生成此条SQL片段和参数
notEqual
不等于andNotEqual
带and关键字的不等于orNotEqual
带or关键字的不等于moreThan
大于andMoreThan
带and关键字的大于orMoreThan
带or关键字的大于lessThan
小于andLessThan
带and关键字的小于orLessThan
带or关键字的小于moreEqual
大于等于andMoreEqual
带and关键字的大于等于orMoreEqual
带or关键字的大于等于lessEqual
小于等于andLessEqual
带and关键字的小于等于orLessEqual
带or关键字的小于等于
<like match="" field="" value="" pattern="" />
<andLike match="" field="" value="" pattern="" />
<orLike match="" field="" value="" pattern="" />
<!-- not like的 相关标签. -->
<notLike match="" field="" value="" pattern="" />
<andNotLike match="" field="" value="" pattern="" />
<orNotLike match="" field="" value="" pattern="" />
- match,表示匹配条件。非必要(填)属性,如果不写(填)此属性,则视为必然生成此条件SQL片段;否则匹配结果为true时才生成,匹配结果为false时,不生成。
- field,表示对应数据库的字段,可以是数据库的表达式、函数等。必要(填)属性。
- value,表示参数值,对应Java中的名称。条件必要(填)属性。pattern和value只能存在一个,value生成的SQL片段默认是两边模糊,即:
%%
。 - pattern,表示like匹配的模式,如:
abc%
、_bc
等。条件必要(填)属性。pattern和value只能存在一个,pattern用来指定自定义的匹配模式。
<andLike match="?email != empty" field="email" value="email"/ >
SQL片段的生成结果:AND email LIKE ?
解释:如果email不等于空时,才生成此条SQL片段和参数
<notLike field="email" value="%@gmail.com"/ >
SQL片段的生成结果:email NOT LIKE '%@gmail.com'
解释:匹配所有不是gmail的邮箱.
<between match="" field="" start="" end="" />
<andBetween match="" field="" start="" end="" />
<orBetween match="" field="" start="" end="" />
- match,表示匹配条件。非必要(填)属性,如果不写(填)此属性,则视为必然生成此条件SQL片段;否则匹配结果为true时才生成,匹配结果为false时,不生成。
- field,表示对应数据库的字段,可以是数据库的表达式、函数等。必要(填)属性。
- start,表示区间匹配条件的开始参数值,对应Java中的名称,条件必填。
- end,表示区间匹配条件的结束参数值,对应Java中的名称,条件必填。
!> 注意:Zealot中对start和end的空判断是检测是否是null,而不是空字符串,0等情况。所以,对start和end的空处理应该是null。
<andBetween match="?startAge != null || ?endAge != null" field="age" start="startAge" end="endAge"></andBetween>
start为null,end不为null,则SQL片段的生成结果:AND age >= ?
start不为null,end为null,则SQL片段的生成结果:AND age <= ?
start不为null,end不为null,则SQL片段的生成结果:AND age BETWEEN ? AND ?
start为null,end为null,则不生成SQL片段
**解释**:match标签是非必填的,区间查询中,靠start和end两种条件也可以组成一个简单的动态情形。如果start为空,end不为空,则是大于等于查询;如果start为空,end不为空,则是小于等于查询;如果start、end均不为空,则是区间查询;两者会均为空则不生产此条sql。
<in match="" field="" value="" />
<andIn match="" field="" value="" />
<orIn match="" field="" value="" />
<!-- not in 相关的标签. -->
<in match="" field="" value="" />
<andIn match="" field="" value="" />
<orIn match="" field="" value="" />
- match,表示匹配条件。非必要(填)属性,如果不写(填)此属性,则视为必然生成此条件SQL片段;否则匹配结果为true时才生成,匹配结果为false时,不生成。
- field,表示对应数据库的字段,可以是数据库的表达式、函数等。必要(填)属性。
- value,表示参数的集合,值可以是数组,也可以是Collection集合,还可以是单个的值。必要(填)属性。
<andIn match="?sexs != empty" field="sex" value="sexs"></andIn>
SQL片段的生成结果:AND sex in (?, ?)
解释:如果sexs不等于空时,才生成此条SQL片段和参数(这里的sexs假设有两个值)
<isNull match="" field="" />
<andIsNull match="" field="" />
<orIsNull match="" field="" />
<!-- IS NOT NULL 相关的标签. -->
<isNotNull match="" field="" />
<andIsNotNull match="" field="" />
<orIsNotNull match="" field="" />
- match,表示匹配条件。非必要(填)属性,如果不写(填)此属性,则视为必然生成此条件SQL片段;否则匹配结果为true时才生成,匹配结果为false时,不生成。
- field,表示对应数据库的字段,可以是数据库的表达式、函数等。必要(填)属性。
<andIsNull match="?id != empty" field="s.n_age" />
SQL片段的生成结果:AND s.n_age IS NULL
解释:如果 id 不等于空时,才生成此条SQL片段和参数
text标签主要用于在标签内部自定义需要的文本和需要传递的各种参数,为SQL书写提供灵活性。
<text match="" value="">
...
</text>
- match,同上。
- value,表示参数的集合,值可以是数组,也可以是Collection集合,还可以是单个的值。必填
<text match="" value="{name1, name2, email}">
and name in (?, ?)
and email = ?
</text>
SQL片段的生成结果:and name in (?, ?) and email = ?
解释:如果match为true、不填写或无match标签时,才生成此条SQL片段和自定义传递的参数,参数就是通过`name1`、`name2`和`email`组合成的数组或集合,或者直接传递集合或数组(此处组合而成的数组,如果是集合就把'{'换成'['即可)。
import标签主要用于在zealot标签中导入其它公共的zealot节点,便于程序代码逻辑的复用。
<import zealotid="" />
<import match="" zealotid="" />
<import match="" namespace="" zealotid="" value="" />
- match,表示匹配条件。非必要(填)属性,如果不写(填)此属性,则视为必然生成此条件SQL片段;否则匹配结果为true时才生成,匹配结果为false时,不生成。
- namespace,表示需要引用导入的节点所在的xml文件的命名空间,非必填属性。如果如果不写(填)此属性,则视为仅在本xml文件中查找对应的zealotId的节点。
- zealotid,表示要引用导入的zealot节点的ID,必填属性。
- value,表示需要传入到要引用的zealot节点中的上下文参数值,非必填属性。如果不写(填)此属性,则传递最顶层的上下文参数。
<zealot id="commonStuCondition">
<andMoreEqual match="?age > 0" field="s.n_age" value="age" />
<andBetween match="(?startBirthday != null) || (?endBirthday != null)" field="s.d_birthday" start="startBirthday" end="endBirthday" />
</zealot>
<zealot id="queryStudents">
...
<import zealotid="commonStuCondition" />
...
</zealot>
SQL片段的生成结果:AND s.n_age >= ? AND s.d_birthday BETWEEN ? AND ?
choose标签主要用于解决"无数的"多分支条件选择逻辑,对应的即是Java中if/else if/ ... /else if/else
这种逻辑。
<choose when="" then="" when2="" then2="" ... whenx="" thenx="" else="" />
- when,表示匹配条件,可以写无数个,对应于Java中的
if/else if
条件。必要(填)属性,如果不写(填)此属性,表示false,直接进入else
的逻辑块中。 - then,表示需要执行的逻辑,和
when
向对应,可以写无数个,内容是字符串或者zealot的字符串模版,必要(填)属性。如果如果不写(填)此属性,即使满足了对应的when
条件,也不会做SQL的拼接操作。 - else,表示所有when条件都不满足时才执行的逻辑,内容是字符串或者zealot的字符串模版,非必填属性。如果不写(填)此属性,则表示什么都不做(这样就无任何意义了)。
<zealot id="queryByChoose">
UPDATE t_student SET s.c_sex =
<choose when="?sex == 0" then="'female'" when2="?sex == 1" then2="'male'" else="unknown" />
, s.c_status =
<choose when="?state" then="'yes'" else="'no'" />
, s.c_age =
<choose when="age > 60" then="'老年'" when2="age > 40" then2="'中年'" when3="age > 20" then3="'青年'" when4="age > 10" then4="'少年'" else="'幼年'" />
WHERE s.c_id = '@{stuId}'
</zealot>
SQL片段的生成结果:UPDATE t_student SET s.c_sex = 'male' , s.c_status = 'no' , s.c_age = '幼年' WHERE s.c_id = '123'
从前面所知,条件标签是生成动态SQL和参数的核心,但是项目开发的过程中往往有更多多复杂的逻辑来生成某些SQL,甚至那些逻辑还要被多处使用到,默认的一些标签不能够满足开发需求,那么自定义自己的动态条件标签来实现就显得很重要了。所谓自定义标签和处理器就是设置自定义的标签名称、匹配条件、参数和数据库字段等,再通过自定义的处理器来控制生成SQL的逻辑,这样就可以达到生成我们需要的SQL的功能,这样的标签重大的意义在于能够最大化简化sql的书写和功能的复用。
假设user表中有id、email两个字段,后台封装了一个User的参数,其中包含userId和usermail的属性。如果userId不为空时,则根据id来等值查询;如果userId为空,usermail不为空时,则根据email来做模糊查询;此处也隐含的说明了如果userId和usermail均不为空时,仍然以id来做等值查询。对此需求查询我们仍然可以用前面的标签组合来实现。假如很多地方都需要这种逻辑的查询,那我们可以使用自定义的标签来实现和复用这种查询逻辑。
根据上面的查询需求,可以分析出标签属性具有有id
、email
两个数据库字段,userId和userEmail的两个Java参数值,可设置其标签属性分别为idValue
和emailValue
,因此标签为:
<zealot id="queryUserWithIdEmail">
select * from user where
<userIdEmail match="?userId != empty || ?userEmail != empty" idField="id" emailField="email" idValue="userId" emailValue="userEmail"></userIdEmail>
</zealot>
在你项目的某个package中,新建一个UserIdEmailHandler.java
的文件,并让它实现IConditHandler
接口,细节的代码处理逻辑和注释说明如下:
package com.blinkfox.handler;
import BuildSource;
import SqlInfo;
import ZealotConst;
import IConditHandler;
import ParseHelper;
import StringHelper;
import XmlNodeHelper;
import org.dom4j.Node;
import java.util.List;
/**
* 自定义的ID和Email条件查询的SQL处理器
* Created by blinkfox on 2016/11/11.
*/
public class UserIdEmailHandler implements IConditHandler {
@Override
public SqlInfo buildSqlInfo(BuildSource source) {
/* 获取拼接的参数和Zealot节点 */
SqlInfo sqlInfo = source.getSqlInfo();
Node node = source.getNode();
// 获取配置的属性值,getAndCheckNodeText()方法会检测属性值是否为空,如果为空,会抛出运行时异常
String idField = XmlNodeHelper.getAndCheckNodeText(node, "attribute::idField");
String emailField = XmlNodeHelper.getAndCheckNodeText(node, "attribute::emailField");
// getAndCheckNodeText()方法仅仅只获取属性的值,即使未配置或书写值,也会返回空字符串
String idValue = XmlNodeHelper.getNodeAttrText(node, "attribute::idValue");
String emailValue = XmlNodeHelper.getNodeAttrText(node, "attribute::emailValue");
/* 获取match属性值,如果匹配中 字符值没有,则认为是必然生成项 */
String matchText = XmlNodeHelper.getNodeAttrText(node, ZealotConst.ATTR_MATCH);
if (StringHelper.isBlank(matchText)) {
sqlInfo = buildIdEmailSqlInfo(source, idField, emailField, idValue, emailValue);
} else {
/* 如果match匹配成功,则生成数据库sql条件和参数 */
Boolean isTrue = (Boolean) ParseHelper.parseWithMvel(matchText, source.getParamObj());
if (isTrue) {
sqlInfo = buildIdEmailSqlInfo(source, idField, emailField, idValue, emailValue);
}
}
return sqlInfo;
}
/**
* 构建自定义的SqlInfo信息,区分是根据id做等值查询,还是根据email做模糊查询的情况
* @param source source
* @param idField idField
* @param emailField emailField
* @param idValue idValue
* @param emailValue emailValue
*/
private SqlInfo buildIdEmailSqlInfo(BuildSource source, String idField, String emailField,
String idValue, String emailValue) {
SqlInfo sqlInfo = source.getSqlInfo();
StringBuilder join = sqlInfo.getJoin();
List<Object> params = sqlInfo.getParams();
// 如果userId不为空,则根据id来做等值查询
Integer idText = (Integer) ParseHelper.parseWithMvel(idValue, source.getParamObj());
if (idText != null) {
// prefix是前缀,如"and","or"之类,没有则默认为空字符串""
join.append(source.getPrefix()).append(idField).append(ZealotConst.EQUAL_SUFFIX);
params.add(idText);
return sqlInfo.setJoin(join).setParams(params);
}
// 获取userEmail的值,如果userEmail不为空,则根据email来做模糊查询
String emailText = (String) ParseHelper.parseWithMvel(emailValue, source.getParamObj());
if (StringHelper.isNotBlank(emailText)) {
// prefix是前缀,如"and","or"之类,没有则默认为空字符串""
join.append(source.getPrefix()).append(emailField).append(ZealotConst.LIEK_SUFFIX);
params.add("%" + emailText + "%");
return sqlInfo.setJoin(join).setParams(params);
}
return sqlInfo;
}
}
在你继承的Zealot Java配置文件方法中添加配置自定义的标签和处理器,重启即可,代码示例如下:
/**
* 我继承的zealotConfig配置类
* Created by blinkfox on 2016/11/4.
*/
public class MyZealotConfig extends AbstractZealotConfig {
public static final String USER_ZEALOT = "user_zealot";
@Override
public void configXml(XmlContext ctx) {
ctx.add(USER_ZEALOT, "/zealot/zealot-user.xml");
}
@Override
public void configTagHandler() {
// 自定义userIdEmail标签和处理器
add("userIdEmail", UserIdEmailHandler.class);
// 有and前缀的自定义标签
add("andUserIdEmail", " and " ,UserIdEmailHandler.class);
}
}
测试代码和结果如下:
public void queryUserIdEmail() {
Map<String, Object> user = new HashMap<String, Object>();
user.put("userId", 3);
user.put("userEmail", "san");
SqlInfo sqlInfo = Zealot.getSqlInfo(MyZealotConfig.USER_ZEALOT, "queryUserWithIdEmail", user);
String sql = sqlInfo.getSql();
Object[] params = sqlInfo.getParamsArr();
System.out.println("----生成sql的为:" + sql);
System.out.println("----生成sql的参数为:" + Arrays.toString(params));
List<User> users = User.userDao.find(sql, params);
renderJson(users);
}
打印的sql结果:
----生成sql的为:select * from user where id = ?
----生成sql的参数为:[3]
当把userId的值设为null时,打印的sql结果:
----生成sql的为:select * from user where email LIKE ?
----生成sql的参数为:[%san%]
由于Zealot中SQL片段生成的标签大多是工具库预设或自定义的,但是在实现更为灵活的逻辑控制时就显得力不从心了,如果都通过自定义标签去实现更灵活多变的逻辑会显得很麻烦。因此,决定在1.0.6的版本中增加更为强大灵活的流程逻辑控制标签。自由的流程逻辑控制方式就意味着难以实现绑定变量参数的方式来生成SQL,而是即时生成替换变量值后的SQL。
Zealot中SQL片段标签和流程控制标签是可以混合使用的,看下面的SQL书写方式即可容易理解:
<!-- 根据流程控制标签查询用户信息 -->
<zealot id="queryUsersByFlowTag">
select * from user where
<like field="nickname" value="nickName"/>
@if{?email != empty}
AND email like '%@{email}%'
@end{}
order by id desc
</zealot>
当email不为空时就会生成类似如下的SQL了:
select * from user where nickname LIKE ? AND email like '%zhang%' order by id desc
Zealot的流程控制标签使用的是MVEL模板标签,所以,支持所有MVEL2.0的模板标签,这也正体现了Zealot动态SQL的强大特性。关于MVEL2.x的模板更详细的介绍请参考这里。
@{}表达式是最基本的形式。它包含一个对字符串求值的值表达式,并附加到输出模板中。例如:
Hello, my name is @{person.name}
静默代码标记允许您在模板中执行MVEL表达式代码。它不返回值,并且不以任何方式影响模板的格式。
@code{age = 23; name = 'John Doe'}
@{name} is @{age} years old
该模板将计算出:John Doe is 23 years old。
@if{}和@else{}标签在MVEL模板中提供了完全的if-then-else功能。 例如:
@if{foo != bar}
Foo not a bar!
@else{bar != cat}
Bar is not a cat!
@else{}
Foo may be a Bar or a Cat!
@end{}
MVEL模板中的所有块必须用@end{}
标签来终止,除非是if-then-else
结构,其中@else{}
标记表示前一个控制语句的终止。
foreach标签允许您在模板中迭代集合或数组。 注意:foreach的语法已经在MVEL模板2.0中改变,以使用foreach符号来标记MVEL语言本身的符号。
@foreach{item : products}
- @{item.serialNumber}
@end{}
Zealot中除了上面介绍的一些功能之外,还有其他额外的辅助、简化开发的功能,以下作简要介绍。
在v1.2.0
版本中增加了上下文参数包装器ParamWrapper
的功能,其本质上就是对HashMap
的封装。在以前的版本中需要自己封装JavaBean
对象或者Map对象来作为SQL拼接的上下文参数传入:
以前需要开发者自己封装Map:
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("sex", "1")
paramMap.put("stuId", "123")
paramMap.put("state", false)
SqlInfo sqlInfo = Zealot.getSqlInfo(MyZealotConfig.STUDENT_ZEALOT, "queryByChoose", paramMap);
现在的使用方式:
SqlInfo sqlInfo = Zealot.getSqlInfo(MyZealotConfig.STUDENT_ZEALOT, "queryByChoose",
ParamWrapper.newInstance("sex", "1").put("stuId", "123").put("state", false).toMap());
前后对比来看,再仅仅只需要传入个别自定义参数时,能简化部分代码量。
newInstance()
,创建新的ParamWrapper
实例。newInstance(Map<String, Object> paramMap)
,传入已有的Map
型对象,并创建新的ParamWrapper
实例。newInstance(String key, Object value)
,创建新的ParamWrapper
实例,并创建一对key和value的键值对。put(String key, Object value)
,向参数包装器中,put
对应的key和value值。toMap()
,返回填充了key、value后的Map对象。
在zealot中解析xml标签中的表达式或者模版是通过Mvel
表达式语言来实现的,主要方法解析方法是封装在了ParseHelper
的工具类中,通过该类让开发人员自己测试表达式也是极为方便的。以下作简要介绍。
主要方法:
- parseExpress(String exp, Object paramObj),解析出表达式的值,如果解析出错则不抛出异常,但会输出error级别的异常,不影响zealot的后续执行。
- parseExpressWithException(String exp, Object paramObj),解析出表达式的值,如果解析出错则抛出异常,会影响往zealot的后续执行。
使用示例:
@Test
public void testParseWithMvel() {
// 构造上下文参数
Map<String, Object> context = new HashMap<String, Object>();
context.put("foo", "Hello");
context.put("bar", "World");
String result = (String) ParseHelper.parseExpressWithException("foo + bar", context);
assertEquals("HelloWorld", result); // 解析得到`HelloWorld`字符串,断言为:true。
}
@Test
public void testParseStr2() {
Boolean result = (Boolean) ParseHelper.parseExpress("sex == 1", ParamWrapper.newInstance("sex", "1").toMap());
assertEquals(true, result); // 断言为:true。
}
主要方法:
- parseTemplate(String template, Object paramObj),解析出表达式的值,如果解析出错则抛出异常,影响zealot的后续执行。
使用示例:
@Test
public void testParseTemplate2() {
String result = ParseHelper.parseTemplate("@if{?foo != empty}@{foo} World!@end{}", ParamWrapper.newInstance("foo", "Hello").toMap());
assertEquals("Hello World!", result);// 解析得到`Hello World!`字符串,断言为:true。
}
主要方法:
- isMatch(String match, Object paramObj),是否匹配,常用于标签中的match值的解析,即如果match不填写,或者内容为空,或者解析出为正确的值,都视为
true
。 - isNotMatch(String match, Object paramObj),是否不匹配,同
isMatch
相反,只有解析到的值是false时,才认为是false。 - isTrue(String exp, Object paramObj),是否为true,只有当解析值确实为true时,才为true。
在拼接动态SQL中避免不了会出现1 = 1
等无用的子SQL片段,现在可以通过在生成完的SqlInfo对象中的removeIfExist(subSql)
方法来消除它,其他类似的子SQL也都可以消除。使用示例如下:
sqlInfo.removeIfExist(" 1 = 1 AND");
// 或者
sqlInfo.removeIfExist(" 1 <> 1");
Zealot类库遵守Apache License 2.0 许可证
- v1.3.1(2018-05-03)
- 修复了扫描xml路径始终为zealot的问题
- 修改了阿里规约插件检查出的一些代码坏味道问题
- v1.3.0(2018-05-02)
- 新增完善各种动态SQL操作符和XML标签的
与
、或
、非
的逻辑 - 新增了根据自定义模式构建模糊匹配的likePattern标签,新增了isNull、isNotNull等情况
- 新增了消除
where 1 = 1
等场景的SQL - 新增了Zealot调用的重载方法
- 新增了扫描XML路径来检测识别zealot xml(不配置扫描XML位置的话,默认扫描资源目录下
zealot/
目录中的xml文件) - 新增了扫描Handler注解(
@Tagger
、@Taggers
)的功能和快速配置
- 新增完善各种动态SQL操作符和XML标签的
- v1.2.1(2018-02-04)
- 新增了通过ZealotConfig的已有实例来配置zealot,这样方便了从spring等已有实例中做配置
- v1.2.0(2017-08-18)
- 新增
import
标签,用于引入公共的zealot
标签节点,便于逻辑和代码复用 - 新增
choose
标签,可以无限制写无数的if/else if/else
等条件选择分支逻辑,方便书写条件选择的动态SQL片段 - 新增
ParamWrapper
工具类,方便更快捷的创建参数的上下文对象 - 单元测试类的包结构重构
- 新增
- v1.1.6(2017-07-27)
- 将
slf4j
的日志改为了JDK
的日志
- 将
- v1.1.5(2017-07-24)
- 新增通用配置功能,包括debug模式、是否打印Sql信息、是否打印Banner等
- 新增依赖了
slf4j
的日志接口,各系统引入slf4j
的日志实现即可 - 去掉了被标注为
@Deprecated
的过时类ZealotKhala
- 一些类的代码重构和JavaDoc完善
- v1.1.4(2017-05-06)
- 修复了启动时打印banner出错的问题
- v1.1.3(2017-05-01)
- 新增了不等于的情况
- 完善单元测试和代码覆盖率
- v1.1.2(2017-04-22)
- 新增了zealot加载完成时的banner显示
- 新增或升级了一些pom文件中的插件,如:pmd、reports等
- 其他代码小细节修改
- v1.1.1(2017-04-16)
- 新增了ZealotKhala和xml标签的常用API,如:大于、小于、大于等于、小于等于等功能。
- 新增了Zealot中xml的text标签,使灵活性SQL拼接灵活性更强
- 新增了ZealotKhala的ICustomAction接口,使自定义的逻辑也能够通过链式写法完成,使SQL拼接逻辑更紧凑
- 标记
Khala.java
为推荐使用,即@Deprecated
。推荐使用ZealotKhala.java
,使SQL的动态性、灵活性更强。
- v1.1.0(2017-04-04)
- 新增了ZealotKhala,使ZealotKhala用Java也可以链式的书写动态SQL,和Zealot的XML标签相互应
- v1.0.7(2017-03-31)
- 使用Google CheckStyle来规范Java代码风格,重构了部分代码,使代码更整洁
- Khala字符串的链式拼接去掉了手动newInstance的方式,直接调用start()方法即可
- v1.0.6(2016-12-31)
- 新增灵活强大的流程逻辑控制标签
- 新增自定义标签的示例和单元测试
- v1.0.5(2016-12-29)
- 新增Zealot基本功能的单元测试
- 重构Zealot缓存加载的代码
- 新增了Khala的获取实例的方法
- v1.0.4(2016-11-12)
- 新增了SQL字符串链式拼接工具类Khala.java
- v1.0.3(2016-11-11)
- 修复了区间查询大于或等于情况下的bug
- XmlNodeHelper中新增getNodeAttrText()方法
- v1.0.2(2016-11-10)
- 将缓存文档改为缓存Zealot节点,使生成sql效率更快
- 代码细节重构调整
- v1.0.1(2016-11-08)
- 新增日志功能,替换System.out
- 新增自定义异常
- 完善文档注释
- v1.0.0(2016-11-04)
- 核心功能完成