Skip to content

Commit 10adf96

Browse files
committed
Added auto-generated prepared statements parameters support.
1 parent 749dbf8 commit 10adf96

File tree

9 files changed

+287
-11
lines changed

9 files changed

+287
-11
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.mybatis.scripting.freemarker;
2+
3+
import freemarker.template.TemplateModel;
4+
5+
import java.util.List;
6+
7+
/**
8+
* @author elwood
9+
*/
10+
public class AdditionalParamsTemplateModel implements TemplateModel {
11+
private final List additionalParams;
12+
13+
public AdditionalParamsTemplateModel(List additionalParams) {
14+
this.additionalParams = additionalParams;
15+
}
16+
17+
public List getAdditionalParams() {
18+
return additionalParams;
19+
}
20+
}

src/main/java/org/mybatis/scripting/freemarker/FreeMarkerSqlSource.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.io.CharArrayWriter;
1111
import java.io.IOException;
12+
import java.util.ArrayList;
1213
import java.util.HashMap;
1314
import java.util.Map;
1415

@@ -33,17 +34,20 @@ public BoundSql getBoundSql(Object parameterObject) {
3334
// Add to passed parameterObject our predefined directive - MyBatisParamDirective
3435
// It will be available as "p" inside templates
3536
Object dataContext;
37+
ArrayList additionalParams = new ArrayList();
3638
if (parameterObject != null) {
3739
if (parameterObject instanceof Map) {
3840
HashMap<String, Object> map = new HashMap<>((Map<String, Object>) parameterObject);
3941
map.put(MyBatisParamDirective.DEFAULT_KEY, new MyBatisParamDirective());
42+
map.put("__additional_params__", additionalParams);
4043
dataContext = map;
4144
} else {
42-
dataContext = new ParamObjectAdapter(parameterObject);
45+
dataContext = new ParamObjectAdapter(parameterObject, additionalParams);
4346
}
4447
} else {
4548
HashMap<Object, Object> map = new HashMap<>();
4649
map.put(MyBatisParamDirective.DEFAULT_KEY, new MyBatisParamDirective());
50+
map.put("__additional_params__", additionalParams);
4751
dataContext = map;
4852
}
4953

@@ -58,6 +62,18 @@ public BoundSql getBoundSql(Object parameterObject) {
5862
// they will be replaced to '?' by MyBatis engine further
5963
String sql = writer.toString();
6064

65+
if (!additionalParams.isEmpty()) {
66+
if (!(parameterObject instanceof Map)) {
67+
throw new UnsupportedOperationException("Auto-generated prepared statements parameters" +
68+
" are not available if using parameters object. Use @Param-annotated parameters instead.");
69+
}
70+
71+
Map<String, Object> parametersMap = (Map<String, Object>) parameterObject;
72+
for (int i = 0; i < additionalParams.size(); i++) {
73+
parametersMap.put("_p" + i, additionalParams.get(i));
74+
}
75+
}
76+
6177
// Pass retrieved SQL into MyBatis engine, it will substitute prepared-statements parameters
6278
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
6379
Class<?> parameterType1 = parameterObject == null ? Object.class : parameterObject.getClass();
Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package org.mybatis.scripting.freemarker;
22

33
import freemarker.core.Environment;
4-
import freemarker.template.TemplateDirectiveBody;
5-
import freemarker.template.TemplateDirectiveModel;
6-
import freemarker.template.TemplateException;
7-
import freemarker.template.TemplateModel;
4+
import freemarker.ext.util.WrapperTemplateModel;
5+
import freemarker.template.*;
86

97
import java.io.IOException;
8+
import java.util.List;
109
import java.util.Map;
1110

1211
/**
@@ -19,13 +18,64 @@
1918
* &lt;@p name="paramName"/&gt;
2019
* </pre></blockquote>
2120
*
21+
* Also directive supports `value` attribute. If it is specified, param will take passed value
22+
* and create the corresponding #{}-parameter. This is useful in loops:
23+
*
24+
* <blockquote><pre>
25+
* &lt;#list ids as id&gt;
26+
* &lt;@p value=id/&gt;
27+
* &lt;#if id_has_next&gt;,&lt;/#if&gt;
28+
* &lt;/#list&gt;
29+
* </pre></blockquote>
30+
*
31+
* will be translated into
32+
*
33+
* <blockquote><pre>
34+
* #{_p0},#{_p1},#{_p2}
35+
* </pre></blockquote>
36+
*
37+
* And MyBatis engine will convert it to `?`-params finally.
38+
*
2239
* @author elwood
2340
*/
2441
public class MyBatisParamDirective implements TemplateDirectiveModel {
2542
public static String DEFAULT_KEY = "p";
2643

2744
@Override
2845
public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException {
29-
env.getOut().write(String.format("#{%s}", params.get("name")));
46+
SimpleScalar name = (SimpleScalar) params.get("name");
47+
if (params.containsKey("value")) {
48+
Object valueObject = params.get("value");
49+
Object value;
50+
if (valueObject == null) {
51+
value = null;
52+
} else if (valueObject instanceof WrapperTemplateModel) {
53+
value = ((WrapperTemplateModel) valueObject).getWrappedObject();
54+
} else if (valueObject instanceof SimpleScalar) {
55+
value = ((SimpleScalar) valueObject).getAsString();
56+
} else if (valueObject instanceof SimpleNumber) {
57+
value = ((SimpleNumber) valueObject).getAsNumber();
58+
} else if (valueObject instanceof SimpleDate) {
59+
value = ((SimpleDate) valueObject).getAsDate();
60+
} else {
61+
throw new UnsupportedOperationException(
62+
String.format("Type %s is not supported yet in this context.",
63+
valueObject.getClass().getSimpleName()));
64+
}
65+
66+
TemplateModel additionalParamsObject = env.getGlobalVariables().get("__additional_params__");
67+
List additionalParams;
68+
if (additionalParamsObject instanceof DefaultListAdapter) {
69+
additionalParams = (List) ((DefaultListAdapter) additionalParamsObject).getWrappedObject();
70+
} else {
71+
additionalParams = ((AdditionalParamsTemplateModel) additionalParamsObject)
72+
.getAdditionalParams();
73+
}
74+
String generatedParamName = "_p" + additionalParams.size();
75+
env.getOut().write(String.format("#{%s}", generatedParamName));
76+
additionalParams.add(value);
77+
} else {
78+
env.getOut().write(String.format("#{%s}", name));
79+
}
3080
}
3181
}

src/main/java/org/mybatis/scripting/freemarker/ParamObjectAdapter.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import freemarker.ext.beans.BeanModel;
44
import freemarker.ext.beans.BeansWrapperBuilder;
5-
import freemarker.template.Configuration;
6-
import freemarker.template.TemplateHashModel;
7-
import freemarker.template.TemplateModel;
8-
import freemarker.template.TemplateModelException;
5+
import freemarker.ext.util.WrapperTemplateModel;
6+
import freemarker.template.*;
7+
8+
import java.util.ArrayList;
99

1010
/**
1111
* Important: if you are using some object that already has property "p", then
@@ -15,9 +15,15 @@
1515
*/
1616
public class ParamObjectAdapter implements TemplateHashModel {
1717
private final BeanModel beanModel;
18+
private final ArrayList additionalParams;
1819

19-
public ParamObjectAdapter(Object paramObject) {
20+
public ParamObjectAdapter(Object paramObject, ArrayList additionalParams) {
2021
beanModel = new BeanModel(paramObject, new BeansWrapperBuilder(Configuration.VERSION_2_3_22).build());
22+
this.additionalParams = additionalParams;
23+
}
24+
25+
public ArrayList getAdditionalParams() {
26+
return additionalParams;
2127
}
2228

2329
@Override
@@ -26,6 +32,9 @@ public TemplateModel get(String key) throws TemplateModelException {
2632
if (value == null && MyBatisParamDirective.DEFAULT_KEY.equals(key)) {
2733
return new MyBatisParamDirective();
2834
}
35+
if (value == null && "__additional_params__".equals(key)) {
36+
return new AdditionalParamsTemplateModel(additionalParams);
37+
}
2938
return value;
3039
}
3140

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.mybatis.scripting.freemarker;
2+
3+
/**
4+
* Class to test auto-generated prepared statement parameters.
5+
*
6+
* @author elwood
7+
*/
8+
public class PreparedParam {
9+
public static class InnerClass {
10+
private String strValue = "InnerString";
11+
12+
public String getStrValue() {
13+
return strValue;
14+
}
15+
16+
public void setStrValue(String strValue) {
17+
this.strValue = strValue;
18+
}
19+
}
20+
21+
private InnerClass innerObject = new InnerClass();
22+
private Object nullValue = null;
23+
24+
public InnerClass getInnerObject() {
25+
return innerObject;
26+
}
27+
28+
public void setInnerObject(InnerClass innerObject) {
29+
this.innerObject = innerObject;
30+
}
31+
32+
public Object getNullValue() {
33+
return nullValue;
34+
}
35+
36+
public void setNullValue(Object nullValue) {
37+
this.nullValue = nullValue;
38+
}
39+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.mybatis.scripting.freemarker;
2+
3+
import org.apache.ibatis.annotations.Lang;
4+
import org.apache.ibatis.annotations.Param;
5+
import org.apache.ibatis.annotations.Select;
6+
7+
import java.util.List;
8+
9+
/**
10+
* This mapper demonstrates the usage of auto-generating prepared statement
11+
* parameters instead of usual inline strategy.
12+
*
13+
* @author elwood
14+
*/
15+
public interface PreparedParamsMapper {
16+
@Lang(FreeMarkerLanguageDriver.class)
17+
@Select("preparedIn.ftl")
18+
List<Name> findByNames(@Param("ids") List<String> ids);
19+
20+
/**
21+
* This is doesn't work - because params objects are unsupported when using
22+
* auto-generated prepared parameters (it is impossible to add parameters
23+
* to MyBatis engine). This call will throw exception.
24+
*/
25+
@Lang(FreeMarkerLanguageDriver.class)
26+
@Select("prepared.ftl")
27+
Name findUsingParamsObject(PreparedParam param);
28+
29+
@Lang(FreeMarkerLanguageDriver.class)
30+
@Select("prepared.ftl")
31+
Name findUsingParams(@Param("innerObject") PreparedParam.InnerClass innerClass);
32+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.mybatis.scripting.freemarker;
2+
3+
import org.apache.ibatis.exceptions.PersistenceException;
4+
import org.apache.ibatis.io.Resources;
5+
import org.apache.ibatis.jdbc.ScriptRunner;
6+
import org.apache.ibatis.mapping.Environment;
7+
import org.apache.ibatis.session.Configuration;
8+
import org.apache.ibatis.session.SqlSession;
9+
import org.apache.ibatis.session.SqlSessionFactory;
10+
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
11+
import org.apache.ibatis.transaction.TransactionFactory;
12+
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
13+
import org.hsqldb.jdbc.JDBCDataSource;
14+
import org.junit.Assert;
15+
import org.junit.BeforeClass;
16+
import org.junit.Test;
17+
18+
import java.io.Reader;
19+
import java.sql.Connection;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
/**
24+
* Test of using FreeMarker to generate prepared statements parameters.
25+
*
26+
* @author elwood
27+
*/
28+
public class PreparedParamsTest {
29+
protected static SqlSessionFactory sqlSessionFactory;
30+
31+
@BeforeClass
32+
public static void setUp() throws Exception {
33+
Class.forName("org.hsqldb.jdbcDriver");
34+
35+
JDBCDataSource dataSource = new JDBCDataSource();
36+
dataSource.setUrl("jdbc:hsqldb:mem:db3");
37+
dataSource.setUser("sa");
38+
dataSource.setPassword("");
39+
40+
try (Connection conn = dataSource.getConnection()) {
41+
try (Reader reader = Resources.getResourceAsReader("org/mybatis/scripting/freemarker/create-db.sql")) {
42+
ScriptRunner runner = new ScriptRunner(conn);
43+
runner.setLogWriter(null);
44+
runner.setErrorLogWriter(null);
45+
runner.runScript(reader);
46+
conn.commit();
47+
}
48+
}
49+
50+
TransactionFactory transactionFactory = new JdbcTransactionFactory();
51+
Environment environment = new Environment("development", transactionFactory, dataSource);
52+
53+
// You can call configuration.setDefaultScriptingLanguage(FreeMarkerLanguageDriver.class)
54+
// after this to use FreeMarker driver by default.
55+
Configuration configuration = new Configuration(environment);
56+
57+
configuration.addMapper(PreparedParamsMapper.class);
58+
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
59+
}
60+
61+
@Test
62+
public void testInCall() {
63+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
64+
PreparedParamsMapper mapper = sqlSession.getMapper(PreparedParamsMapper.class);
65+
List<Name> names = mapper.findByNames(new ArrayList<String>() {{
66+
add("Pebbles");
67+
add("Barney");
68+
add("Betty");
69+
}});
70+
Assert.assertTrue(names.size() == 3);
71+
}
72+
}
73+
74+
/**
75+
* PersistenceException will be thrown with cause of UnsupportedOperationException
76+
*/
77+
@Test(expected = PersistenceException.class)
78+
public void testParamsObjectCall() {
79+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
80+
PreparedParamsMapper mapper = sqlSession.getMapper(PreparedParamsMapper.class);
81+
mapper.findUsingParamsObject(new PreparedParam());
82+
}
83+
}
84+
85+
@Test
86+
public void testNoParamsCall() {
87+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
88+
PreparedParamsMapper mapper = sqlSession.getMapper(PreparedParamsMapper.class);
89+
Name name = mapper.findUsingParams(new PreparedParam.InnerClass());
90+
Assert.assertTrue(name != null && name.getFirstName().equals("Wilma"));
91+
}
92+
}
93+
}

src/test/resources/sql/prepared.ftl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<#assign strValue='Wilma' />
2+
<#assign intValue=5/>
3+
<#assign doubleValue=10.5/>
4+
<#assign objectValue=innerObject/>
5+
<#assign innerStringProp=innerObject.strValue/>
6+
7+
select * from names where firstName = <@p value='Wilma'/>
8+
and '${strValue}' = <@p value=strValue/>
9+
and ${intValue} = <@p value=intValue/>
10+
and ${doubleValue} = <@p value=doubleValue/>
11+
and '${innerStringProp}' = <@p value=innerObject.strValue/>

src/test/resources/sql/preparedIn.ftl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
select * from names where firstName in (
2+
<#list ids as id>
3+
<@p value=id/>
4+
<#if id_has_next>,</#if>
5+
</#list>
6+
)

0 commit comments

Comments
 (0)