一个轻量级的SQL和参数动态生成工具库。文档地址
My life for Auir!

SQL对开发人员来说是核心的资产之一,在开发中经常需要书写冗长、动态的SQL,许多项目中仅仅采用Java来书写动态SQL,会导致SQL分散、不易调试和阅读。所谓易于维护的SQL应该兼有动态性和可调试性的双重特性。在Java中书写冗长的SQL,虽然能很好的做到动态性,缺大大降低了SQL本身的可调试性,开发人员必须运行项目调试打印出SQL才能知道最终的SQL长什么样。所以为了做到可调试性,开发人员开始将SQL单独提取出来存放到配置文件中来维护,这样方便开发人员复制出来粘贴到SQL工具中来直接运行,但无逻辑功能的配置文件虽然解决了可调试性的问题,却又丧失了动态SQL的能力。所以,才不得不诞生出类似于mybatis这样灵活的半ORM工具来解决这两个的问题,但众多的项目却并未集成mybaits这样的工具。
[Zealot][1]是基于Java语言开发的SQL及对应参数动态拼接生成的工具包,其核心设计目标是帮助开发人员书写和生成出具有动态的、可复用逻辑的且易于维护的SQL及其对应的参数。为了做到可调试性,就必须将SQL提取到配置文件中来单独维护;为了保证SQL根据某些条件,某些逻辑来动态生成,就必须引入表达式语法或者标签语法来达到动态生成SQL的能力。因此,两者结合才诞生了Zealot。
注:zealot即狂热者,是游戏[星际争霸][2]中的单位。
适用于Java web项目,JDK1.6及以上
这里以Maven为例,Maven的引入方式如下:
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>zealot</artifactId>
<version>1.1.3</version>
</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(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)
.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 (?, ?) 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信息:" + sql + "\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 (?, ?) 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代码解耦,易于维护和阅读。
在你的Java web项目项目中,创建一个继承自AbstractZealotConfig的核心配置类,如以下示例:
package com.blinkfox.config;
import com.blinkfox.zealot.bean.XmlContext;
import com.blinkfox.zealot.config.AbstractZealotConfig;
/**
* 我继承的zealotConfig配置类
* Created by blinkfox on 2016/11/4.
*/
public class MyZealotConfig extends AbstractZealotConfig {
@Override
public void configXml(XmlContext ctx) {
}
@Override
public void configTagHandler() {
}
}
代码解释:
(1). configXml()方法主要配置你自己SQL所在XML文件的命名标识和对应的路径,这样好让zealot能够读取到你的XML配置的SQL文件;
(2). configTagHandler()方法主要是配置你自定义的标签和对应标签的处理类,当你需要自定义SQL标签时才配置。
然后,在你的web.xml中来引入zealot,这样容器启动时才会去加载和缓存对应的xml文档,示例配置如下:
<context-param>
<param-name>zealotConfigClass</param-name>
<param-value>com.blinkfox.config.MyZealotConfig</param-value>
</context-param>
<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"?>
<zealots>
<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 com.blinkfox.zealot.bean.XmlContext;
import com.blinkfox.zealot.config.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 configXml(XmlContext ctx) {
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][3],你自己具体的项目中有自己的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]
Zealot的核心功能就在于它XML格式的 SQL配置文件。配置文件也仅仅是一个普通的XML文件,在XML中只需要少许的配置就可以动态生成自己所需要的查询条件。在XML中zealots标签作为根标签,其中的zealot则是一个独立SQL的元素标签,在zealot标签中才包含like、andLike、andBetween、andIn等条件标签,以下重点介绍各条件标签。
Zealot中默认自带了以下4类条件标签,分别是:equal、like、between、in,分别对应着SQL查询中的等值匹配条件、模糊匹配条件、区间匹配条件以及范围匹配条件;四类条件标签又各自额外附带了两个连接前缀,分别是:and和or,用于表示逻辑与和或的情形,这两者更为常用,目前还未加入非的情形。所以,zealot中总共带有12个条件标签,各标签的属性和生成SQL的示例如下:
MVEL2表达式,关于[MVEL的语法文档][4]参考这里。MVEL2做解析。标签:<equal field="nickname" value="nickName"></equal>
SQL片段的生成结果:nickname = ?
解释:必然生成此条SQL片段和参数
<andEqual match="?email != empty" field="email" value="email"></andEqual>
SQL片段的生成结果:AND email = ?
解释:如果email不等于空时,才生成此条SQL片段和参数
<andLike match="?email != empty" field="email" value="email"></andLike>
SQL片段的生成结果:AND email LIKE ?
解释:如果email不等于空时,才生成此条SQL片段和参数
注意: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。
<andIn match="?sexs != empty" field="sex" value="sexs"></andIn>
SQL片段的生成结果:AND sex in (?, ?)
解释:如果sexs不等于空时,才生成此条SQL片段和参数(这里的sexs假设有两个值)
text标签主要用于在标签内部自定义需要的文本和需要传递的各种参数,为SQL书写提供灵活性
<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`组合成的数组或集合,或者直接传递集合或数组(此处组合而成的数组,如果是集合就把'{'换成'['即可)。
从前面所知,条件标签是生成动态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接口,细节的代码处理逻辑和注释说明如下:
```java package com.blinkfox.handler;
import com.blinkfox.zealot.bean.BuildSource; import com.blinkfox.zealot.bean.SqlInfo; import com.blinkfox.zealot.consts.ZealotConst; import com.bl
$ claude mcp add zealot \
-- python -m otcore.mcp_server <graph>