Skip to content

Commit e202727

Browse files
authored
Merge pull request #2760 from harawata/bind-in-foreach
Handle `<bind>` correctly inside `<foreach>`
2 parents 123c42e + eb8749e commit e202727

31 files changed

+606
-1227
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2009-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.ibatis.builder;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
import org.apache.ibatis.mapping.ParameterMapping;
22+
import org.apache.ibatis.mapping.ParameterMode;
23+
import org.apache.ibatis.parsing.TokenHandler;
24+
import org.apache.ibatis.reflection.MetaClass;
25+
import org.apache.ibatis.reflection.MetaObject;
26+
import org.apache.ibatis.reflection.property.PropertyTokenizer;
27+
import org.apache.ibatis.session.Configuration;
28+
import org.apache.ibatis.type.JdbcType;
29+
30+
public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
31+
32+
private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
33+
private final List<ParameterMapping> parameterMappings;
34+
private final Class<?> parameterType;
35+
private final MetaObject metaParameters;
36+
private final Object parameterObject;
37+
private final boolean paramExists;
38+
39+
public ParameterMappingTokenHandler(List<ParameterMapping> parameterMappings, Configuration configuration,
40+
Object parameterObject, Class<?> parameterType, Map<String, Object> additionalParameters, boolean paramExists) {
41+
super(configuration);
42+
this.parameterType = parameterObject == null ? (parameterType == null ? Object.class : parameterType)
43+
: parameterObject.getClass();
44+
this.metaParameters = configuration.newMetaObject(additionalParameters);
45+
this.parameterObject = parameterObject;
46+
this.paramExists = paramExists;
47+
this.parameterMappings = parameterMappings;
48+
}
49+
50+
public ParameterMappingTokenHandler(List<ParameterMapping> parameterMappings, Configuration configuration,
51+
Class<?> parameterType, Map<String, Object> additionalParameters) {
52+
super(configuration);
53+
this.parameterType = parameterType;
54+
this.metaParameters = configuration.newMetaObject(additionalParameters);
55+
this.parameterObject = null;
56+
this.paramExists = false;
57+
this.parameterMappings = parameterMappings;
58+
}
59+
60+
public List<ParameterMapping> getParameterMappings() {
61+
return parameterMappings;
62+
}
63+
64+
@Override
65+
public String handleToken(String content) {
66+
parameterMappings.add(buildParameterMapping(content));
67+
return "?";
68+
}
69+
70+
private ParameterMapping buildParameterMapping(String content) {
71+
Map<String, String> propertiesMap = parseParameterMapping(content);
72+
String property = propertiesMap.get("property");
73+
PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property);
74+
Class<?> propertyType;
75+
if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params
76+
propertyType = metaParameters.getGetterType(property);
77+
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
78+
propertyType = parameterType;
79+
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
80+
propertyType = java.sql.ResultSet.class;
81+
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
82+
propertyType = Object.class;
83+
} else {
84+
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
85+
if (metaClass.hasGetter(property)) {
86+
propertyType = metaClass.getGetterType(property);
87+
} else {
88+
propertyType = Object.class;
89+
}
90+
}
91+
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
92+
Class<?> javaType = propertyType;
93+
String typeHandlerAlias = null;
94+
ParameterMode mode = null;
95+
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
96+
String name = entry.getKey();
97+
String value = entry.getValue();
98+
if ("javaType".equals(name)) {
99+
javaType = resolveClass(value);
100+
builder.javaType(javaType);
101+
} else if ("jdbcType".equals(name)) {
102+
builder.jdbcType(resolveJdbcType(value));
103+
} else if ("mode".equals(name)) {
104+
mode = resolveParameterMode(value);
105+
builder.mode(mode);
106+
} else if ("numericScale".equals(name)) {
107+
builder.numericScale(Integer.valueOf(value));
108+
} else if ("resultMap".equals(name)) {
109+
builder.resultMapId(value);
110+
} else if ("typeHandler".equals(name)) {
111+
typeHandlerAlias = value;
112+
} else if ("jdbcTypeName".equals(name)) {
113+
builder.jdbcTypeName(value);
114+
} else if ("property".equals(name)) {
115+
// Do Nothing
116+
} else if ("expression".equals(name)) {
117+
throw new BuilderException("Expression based parameters are not supported yet");
118+
} else {
119+
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
120+
+ "}. Valid properties are " + PARAMETER_PROPERTIES);
121+
}
122+
}
123+
if (typeHandlerAlias != null) {
124+
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
125+
}
126+
if (!ParameterMode.OUT.equals(mode) && paramExists) {
127+
if (metaParameters.hasGetter(propertyTokenizer.getName())) {
128+
builder.value(metaParameters.getValue(property));
129+
} else if (parameterObject == null) {
130+
builder.value(null);
131+
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
132+
builder.value(parameterObject);
133+
} else {
134+
MetaObject metaObject = configuration.newMetaObject(parameterObject);
135+
builder.value(metaObject.getValue(property));
136+
}
137+
}
138+
return builder.build();
139+
}
140+
141+
private Map<String, String> parseParameterMapping(String content) {
142+
try {
143+
return new ParameterExpression(content);
144+
} catch (BuilderException ex) {
145+
throw ex;
146+
} catch (Exception ex) {
147+
throw new BuilderException("Parsing error was found in mapping #{" + content
148+
+ "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
149+
}
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,43 +15,27 @@
1515
*/
1616
package org.apache.ibatis.builder;
1717

18-
import java.sql.ResultSet;
19-
import java.util.ArrayList;
2018
import java.util.List;
21-
import java.util.Map;
2219
import java.util.StringTokenizer;
2320

2421
import org.apache.ibatis.mapping.ParameterMapping;
2522
import org.apache.ibatis.mapping.SqlSource;
26-
import org.apache.ibatis.parsing.GenericTokenParser;
27-
import org.apache.ibatis.parsing.TokenHandler;
28-
import org.apache.ibatis.reflection.MetaClass;
29-
import org.apache.ibatis.reflection.MetaObject;
3023
import org.apache.ibatis.session.Configuration;
31-
import org.apache.ibatis.type.JdbcType;
3224

3325
/**
3426
* @author Clinton Begin
3527
*/
36-
public class SqlSourceBuilder extends BaseBuilder {
28+
public class SqlSourceBuilder {
3729

38-
private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
39-
40-
public SqlSourceBuilder(Configuration configuration) {
41-
super(configuration);
30+
private SqlSourceBuilder() {
31+
super();
4232
}
4333

44-
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
45-
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
46-
additionalParameters);
47-
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
48-
String sql;
49-
if (configuration.isShrinkWhitespacesInSql()) {
50-
sql = parser.parse(removeExtraWhitespaces(originalSql));
51-
} else {
52-
sql = parser.parse(originalSql);
53-
}
54-
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
34+
public static SqlSource buildSqlSource(Configuration configuration, String sql,
35+
List<ParameterMapping> parameterMappings) {
36+
return new StaticSqlSource(configuration,
37+
configuration.isShrinkWhitespacesInSql() ? SqlSourceBuilder.removeExtraWhitespaces(sql) : sql,
38+
parameterMappings);
5539
}
5640

5741
public static String removeExtraWhitespaces(String original) {
@@ -68,95 +52,4 @@ public static String removeExtraWhitespaces(String original) {
6852
return builder.toString();
6953
}
7054

71-
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
72-
73-
private final List<ParameterMapping> parameterMappings = new ArrayList<>();
74-
private final Class<?> parameterType;
75-
private final MetaObject metaParameters;
76-
77-
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType,
78-
Map<String, Object> additionalParameters) {
79-
super(configuration);
80-
this.parameterType = parameterType;
81-
this.metaParameters = configuration.newMetaObject(additionalParameters);
82-
}
83-
84-
public List<ParameterMapping> getParameterMappings() {
85-
return parameterMappings;
86-
}
87-
88-
@Override
89-
public String handleToken(String content) {
90-
parameterMappings.add(buildParameterMapping(content));
91-
return "?";
92-
}
93-
94-
private ParameterMapping buildParameterMapping(String content) {
95-
Map<String, String> propertiesMap = parseParameterMapping(content);
96-
String property = propertiesMap.get("property");
97-
Class<?> propertyType;
98-
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
99-
propertyType = metaParameters.getGetterType(property);
100-
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
101-
propertyType = parameterType;
102-
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
103-
propertyType = ResultSet.class;
104-
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
105-
propertyType = Object.class;
106-
} else {
107-
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
108-
if (metaClass.hasGetter(property)) {
109-
propertyType = metaClass.getGetterType(property);
110-
} else {
111-
propertyType = Object.class;
112-
}
113-
}
114-
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
115-
Class<?> javaType = propertyType;
116-
String typeHandlerAlias = null;
117-
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
118-
String name = entry.getKey();
119-
String value = entry.getValue();
120-
if ("javaType".equals(name)) {
121-
javaType = resolveClass(value);
122-
builder.javaType(javaType);
123-
} else if ("jdbcType".equals(name)) {
124-
builder.jdbcType(resolveJdbcType(value));
125-
} else if ("mode".equals(name)) {
126-
builder.mode(resolveParameterMode(value));
127-
} else if ("numericScale".equals(name)) {
128-
builder.numericScale(Integer.valueOf(value));
129-
} else if ("resultMap".equals(name)) {
130-
builder.resultMapId(value);
131-
} else if ("typeHandler".equals(name)) {
132-
typeHandlerAlias = value;
133-
} else if ("jdbcTypeName".equals(name)) {
134-
builder.jdbcTypeName(value);
135-
} else if ("property".equals(name)) {
136-
// Do Nothing
137-
} else if ("expression".equals(name)) {
138-
throw new BuilderException("Expression based parameters are not supported yet");
139-
} else {
140-
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
141-
+ "}. Valid properties are " + PARAMETER_PROPERTIES);
142-
}
143-
}
144-
if (typeHandlerAlias != null) {
145-
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
146-
}
147-
return builder.build();
148-
}
149-
150-
private Map<String, String> parseParameterMapping(String content) {
151-
try {
152-
return new ParameterExpression(content);
153-
} catch (BuilderException ex) {
154-
throw ex;
155-
} catch (Exception ex) {
156-
throw new BuilderException("Parsing error was found in mapping #{" + content
157-
+ "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
158-
}
159-
}
160-
}
161-
16255
}

src/main/java/org/apache/ibatis/executor/BaseExecutor.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2023 the original author or authors.
2+
* Copyright 2009-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -212,7 +212,9 @@ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBo
212212
if (parameterMapping.getMode() != ParameterMode.OUT) {
213213
Object value;
214214
String propertyName = parameterMapping.getProperty();
215-
if (boundSql.hasAdditionalParameter(propertyName)) {
215+
if (parameterMapping.hasValue()) {
216+
value = parameterMapping.getValue();
217+
} else if (boundSql.hasAdditionalParameter(propertyName)) {
216218
value = boundSql.getAdditionalParameter(propertyName);
217219
} else if (parameterObject == null) {
218220
value = null;

src/main/java/org/apache/ibatis/mapping/ParameterMapping.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
*/
2828
public class ParameterMapping {
2929

30+
private static final Object UNSET = new Object();
3031
private Configuration configuration;
3132

3233
private String property;
@@ -38,6 +39,7 @@ public class ParameterMapping {
3839
private String resultMapId;
3940
private String jdbcTypeName;
4041
private String expression;
42+
private Object value = UNSET;
4143

4244
private ParameterMapping() {
4345
}
@@ -99,6 +101,11 @@ public Builder expression(String expression) {
99101
return this;
100102
}
101103

104+
public Builder value(Object value) {
105+
parameterMapping.value = value;
106+
return this;
107+
}
108+
102109
public ParameterMapping build() {
103110
resolveTypeHandler();
104111
validate();
@@ -205,6 +212,14 @@ public String getExpression() {
205212
return expression;
206213
}
207214

215+
public Object getValue() {
216+
return value;
217+
}
218+
219+
public boolean hasValue() {
220+
return value != UNSET;
221+
}
222+
208223
@Override
209224
public String toString() {
210225
final StringBuilder sb = new StringBuilder("ParameterMapping{");
@@ -218,6 +233,7 @@ public String toString() {
218233
sb.append(", resultMapId='").append(resultMapId).append('\'');
219234
sb.append(", jdbcTypeName='").append(jdbcTypeName).append('\'');
220235
sb.append(", expression='").append(expression).append('\'');
236+
sb.append(", value='").append(value).append('\'');
221237
sb.append('}');
222238
return sb.toString();
223239
}

0 commit comments

Comments
 (0)