当前位置: 首页 > 工具软件 > Rocker > 使用案例 >

使用Rocker模板引擎解决动态拼接SQL语句的问题

童冠玉
2023-12-01
相信大部分服务端程序员都和我有一样的感觉,就是在Java代码里根据条件拼接SQL语句是个技术含量很低,又很麻烦,还容易出错的问题。
最简单直观的方式当然是用String/StringBuffer/StringBuilder自己拼,但是因为Java不支持多行字符串,也不能自动解析字符串里的变量,因此写起来、改起来都很麻烦。
也有不少框架给出了自己的方案,比如MyBatis可以在Xml里根据条件来拼,JOOQ可以用链式的方法调用来拼。
但是这些方案我感觉都不是很方便,因为大部分场景都是先在查询编辑器里面写好、调好SQL语句,再移植到Java代码中的。有时要修改SQL语句时,还不得不打印输出sql语句,再贴回到查询编辑器里调试,不是一般的麻烦。
MyBatis的方案比较符合现实需求,但是为了拼个字符串引入一整个ORM框架想想也醉了。
因此一个比较合适的方案是采用一种模板引擎,把SQL语句写在模板中,其中的变化部分用模板引擎来填充及控制是否输出。
模板引擎很多了,比较了一下,对性能特别敏感的我自然而然的选择了Rocker,项目地址 https://github.com/fizzed/rocker
Rocker是一个比较成熟的web框架ninja的子项目,想来质量还是可靠的。
但是Rocker似乎很小众,度娘、谷哥甚至万能的StackOverflow均表示无力,官方文档也只介绍了语法,其它安装配置使用一概欠奉,因此我只好自己踩坑了。

Rocker模板引擎的优点
* 速度快,Rocker自己有个官方的 benchmark项目,其速度基本上是Velocity, FreeMarker的4倍以上
* 模板在构建期转译成Java源码,再通过Java编译器编译成class文件
* 模板中的表达式直接在编译期转为Java源码,不需要在运行时解释执行
* 调用时,直接传入Java值或对象,不需要先把参数写入模板上下文中
* 模板中的表达式因为是直接复制成Java源码,因此语法和Java完全一样,学习成本低

在Maven项目里添加依赖
* 目前最新版本是0.13.1
        <dependency>
        	<groupId>com.fizzed</groupId>
        	<artifactId>rocker-runtime</artifactId>
        	<version>${rocker.version}</version>
        </dependency>

添加Rocker编译器插件
* 此Maven插件会自动把模板转译为Java源码
            <plugin>
                <groupId>com.fizzed</groupId>
                <artifactId>rocker-maven-plugin</artifactId>
                <version>${rocker.version}</version>
                <executions>
                    <execution>
                        <id>generate-rocker-templates</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <javaVersion>${project.build.jdk}</javaVersion>                  <!-- 产生的Java源文件的版本 -->
                            <templateDirectory>src/main/java</templateDirectory>     <!-- 模板源文件目录,Rocker会自动扫描其子目录,并用目录名作为产生的Java文件的包名
                            <outputDirectory>gen</outputDirectory>                            <!-- 输出目录,相对项目根目录 ->
                            <discardLogicWhitespace>true</discardLogicWhitespace> <!-- 删除模板标记带来的空行,此选项也可以在模板中用"@option discardLogicWhitespace=true"打开 ->
                        </configuration>
                    </execution>
                </executions>
            </plugin>

* 注意:
在MyEclipse里面,在execution元素上会报一个错误: Plugin execution not covered by lifecycle configuration
百度到的办法,在plugins外面添加pluginManagement,此方法确实能屏蔽掉错误,但是导致构建时此步骤不执行,因此不能使用
最后还是StackOverflow上的办法,直接Ctrl+1调出Quick Fix,然后选择"Permanently mark goal execute in pom.xml as ignored in Eclipse build"忽略掉即可,目前没感到有其它影响

避免模板文件被打包输出
* 还需要在resources节点中添加排除规则,避免模板文件构建时被打包到war包中
            <Excludes>
                <Exclude>**/*.rocker.raw</Exclude>
            </Excludes>

把源码输出目录添加到构建路径
* 在项目根目录创建gen目录
* MyEclipse中,右键点击该目录,菜单中 Build Path -> Use as Source Folder

创建Rocker模板
* 在模板源文件目录中添加模板 比如UserSearch.rocker.raw。这里的文件名UserSearch会被转译成产生的Java类名,因此建议用Java类名的命名方式。
* Rocker的模板文件后缀只能是.rocker.html或者.rocker.raw
* 模板语法见后文

转译模板为Java源文件
* 项目根目录运行mvn generate-sources。
* 如果使用MyEclipse,可以右键点击项目,然后选择Run As -> Maven generate-sources
* 如果构建出错,说明模板中有语法错误,请查看Maven的输出
* 在 [INFO] Parsing x rocker template files 后面的 [ERROR] 信息就是语法错误位置,包含模板文件名,行列号等
* 注意,因为rocker-compiler遇到第一个错误就退出,因此错误信息一般只有一行,不太显眼,而后面还会输出一大堆无用的异常信息来吸引注意力,我眼神不好就被坑了挺长时间……
* 无论用哪种方式调用转译,完后在MyEclipse中都要刷新一下gen目录下面的源码,确保IDE中和磁盘中的源文件同步。

在Java中使用转译出的Rocker模板类
* Rocker转译成功并不意味着输出的Java源文件就一定是正确的,如果有语法或语义错误就要回到模板源文件修改。
* 修改生成的java文件是没有用的,因为下次就会被覆盖掉。
* 如果生成的Java文件也没有错误,那就可以被其它Java代码引用了。
* 在Java源码中调用 <模板类名>.template(<模板参数>).render().toString() 就可以获得最终的字符串了,比如:
String familyName = "张";
String sql = UserSearch.template(familyName + "%").render().toString();

Rocker模板语法
* Rocker的模板标记、表达式都是用'@'开头
* 如果文本中有'@'字符,请使用"@@"转义。
* 文本中的'{', '}'只要是匹配的,Rocker就能正确处理,不会把它们当做控制字符,否则,请用"@{", "@}"进行转义
* Rocker注释为@* ... *@,可以跨多行
* 模板文件的结构为头部份和正文部分

模板头部分
@import: 用于导入Java包或类,除了结尾无分号外和Java一致,注意static import也支持。
@args (...): 指定模板的参数,和Java方法的参数声明语法一致,可以换行,Rocker能自动找到匹配的右括号
@option: 指定模板引擎的开关,比如 "@option discardLogicWhitespace=false", "@option targetCharset=UTF-16"

模板正文部分 - 表达式
@<value>: <value>为Java表达式,比如@keyword, @user.getName(), @users.get(0).getAge() 等
@(<expression>): <expression>为复杂的Java表达式;比如Java的三目表达式或数学运算等
@?<value>: 仅在<value>不为null时渲染输出
@<value>?:<other>: <value>不为null输出<value>,否则输出<other>

模板正文部分 - 控制语句
@for (...): 对Collection的遍历,支持Java的两种标准for语法,即for (... ; ... ; ...) 和 for (... : ...)
@for ((ForIterator i, <ItemType> item): items): Rocker的扩展for语法,可以用 i 获取到当前索引
@for (([ForIterator i, ]<KeyType> key, <ValueType> value): map): Rocker的扩展for语法,用于遍历Map, "ForIterator i"可省略
@continue, @break: 用于循环控制
@if (<expression>) { ... } [ else { ... } ] : 和Java语法一样

一个实际例子
* 注意,正文部分我把Rocker的控制语句都用"-- "注释起来,这样不影响Rocker解析,同时我的SQL的语法也是正确的,只要改成真实参数就可以直接放到查询编辑器里面执行
@import com.mycompany.product.model.*
@import com.mycompany.product.utils.*
@import java.util.List
@args (Long corpId, String type, String label, 
			String group, List<Long> posIdList, String level, String startDate,
			String endDate, Integer count)

select s.user_id, sum(s.score * d.factor) score 
from score s force index (created_def) 
 left join score_def d on s.def_id = d.id 
 left join user u on s.user_id = u.id
-- @if (!Util.empty(group)) {
 left join group_member g on s.corp_id = g.corp_id and g.group_name = '@group' and (g.member = concat('u',s.user_id) or g.member = concat('p',u.pos_id))
-- }
where 1=1
 and s.corp_id = @corpId
 and s.state = 3 
-- @if (type != null) {
 and d.type = '@type'
-- }
-- @if (label != null) {
 and d.label = '@label'
-- } 
-- @if (group != null) {
 and g.id is not null 
-- }
-- @if (posIdList != null) {
--   @if (posIdList.size() == 1) {
 and u.pos_id = @posIdList.get(0)
--   } else {
 and u.pos_id in (@Util.join(posIdList, ","))
--   }
-- }
-- @if (level != null) {
 and u.pos_level = '@level'
-- }
-- @if (startDate != null && endDate != null) {
 and s.created between '@startDate' and '@endDate' 
-- } else {
 and s.created between TIMESTAMPADD(day,-day(curdate())+1,CURDATE()) and now() 
-- }
GROUP BY s.user_id
order by score desc
limit @count?:Constants.DEFAULT_COUNT




 类似资料: