Skip to content

Commit ce6df48

Browse files
committed
Add option for allowing null value on foreach tag
1 parent 2c7a809 commit ce6df48

File tree

12 files changed

+178
-12
lines changed

12 files changed

+178
-12
lines changed

src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ private void settingsElement(Properties props) {
268268
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
269269
configuration.setLogPrefix(props.getProperty("logPrefix"));
270270
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
271+
configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
271272
}
272273

273274
private void environmentsElement(XNode context) throws Exception {

src/main/java/org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
22
<!--
33
4-
Copyright 2009-2018 the original author or authors.
4+
Copyright 2009-2020 the original author or authors.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -271,6 +271,7 @@ suffixOverrides CDATA #IMPLIED
271271
<!ELEMENT foreach (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
272272
<!ATTLIST foreach
273273
collection CDATA #REQUIRED
274+
nullable (true|false) #IMPLIED
274275
item CDATA #IMPLIED
275276
index CDATA #IMPLIED
276277
open CDATA #IMPLIED

src/main/java/org/apache/ibatis/builder/xml/mybatis-mapper.xsd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
33
4-
Copyright 2009-2018 the original author or authors.
4+
Copyright 2009-2020 the original author or authors.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -599,6 +599,7 @@
599599
<xs:element ref="bind"/>
600600
</xs:choice>
601601
<xs:attribute name="collection" use="required"/>
602+
<xs:attribute name="nullable" type="xs:boolean"/>
602603
<xs:attribute name="item"/>
603604
<xs:attribute name="index"/>
604605
<xs:attribute name="open"/>

src/main/java/org/apache/ibatis/scripting/xmltags/ExpressionEvaluator.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2019 the original author or authors.
2+
* Copyright 2009-2020 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.
@@ -40,9 +40,20 @@ public boolean evaluateBoolean(String expression, Object parameterObject) {
4040
}
4141

4242
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
43+
return evaluateIterable(expression, parameterObject, false);
44+
}
45+
46+
/**
47+
* @since 3.5.5
48+
*/
49+
public Iterable<?> evaluateIterable(String expression, Object parameterObject, boolean nullable) {
4350
Object value = OgnlCache.getValue(expression, parameterObject);
4451
if (value == null) {
45-
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
52+
if (nullable) {
53+
return null;
54+
} else {
55+
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
56+
}
4657
}
4758
if (value instanceof Iterable) {
4859
return (Iterable<?>) value;

src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2019 the original author or authors.
2+
* Copyright 2009-2020 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.
@@ -16,6 +16,7 @@
1616
package org.apache.ibatis.scripting.xmltags;
1717

1818
import java.util.Map;
19+
import java.util.Optional;
1920

2021
import org.apache.ibatis.parsing.GenericTokenParser;
2122
import org.apache.ibatis.session.Configuration;
@@ -28,6 +29,7 @@ public class ForEachSqlNode implements SqlNode {
2829

2930
private final ExpressionEvaluator evaluator;
3031
private final String collectionExpression;
32+
private final Boolean nullable;
3133
private final SqlNode contents;
3234
private final String open;
3335
private final String close;
@@ -36,9 +38,21 @@ public class ForEachSqlNode implements SqlNode {
3638
private final String index;
3739
private final Configuration configuration;
3840

41+
/**
42+
* @deprecated Since 3.5.5, use the {@link #ForEachSqlNode(Configuration, SqlNode, String, Boolean, String, String, String, String, String)}.
43+
*/
44+
@Deprecated
3945
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
46+
this(configuration, contents, collectionExpression, null, index, item, open, close, separator);
47+
}
48+
49+
/**
50+
* @since 3.5.5
51+
*/
52+
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, String index, String item, String open, String close, String separator) {
4053
this.evaluator = new ExpressionEvaluator();
4154
this.collectionExpression = collectionExpression;
55+
this.nullable = nullable;
4256
this.contents = contents;
4357
this.open = open;
4458
this.close = close;
@@ -51,8 +65,9 @@ public ForEachSqlNode(Configuration configuration, SqlNode contents, String coll
5165
@Override
5266
public boolean apply(DynamicContext context) {
5367
Map<String, Object> bindings = context.getBindings();
54-
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
55-
if (!iterable.iterator().hasNext()) {
68+
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings,
69+
Optional.ofNullable(nullable).orElseGet(configuration::isNullableOnForEach));
70+
if (iterable == null || !iterable.iterator().hasNext()) {
5671
return true;
5772
}
5873
boolean first = true;

src/main/java/org/apache/ibatis/scripting/xmltags/XMLScriptBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2019 the original author or authors.
2+
* Copyright 2009-2020 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.
@@ -171,12 +171,13 @@ public ForEachHandler() {
171171
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
172172
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
173173
String collection = nodeToHandle.getStringAttribute("collection");
174+
Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
174175
String item = nodeToHandle.getStringAttribute("item");
175176
String index = nodeToHandle.getStringAttribute("index");
176177
String open = nodeToHandle.getStringAttribute("open");
177178
String close = nodeToHandle.getStringAttribute("close");
178179
String separator = nodeToHandle.getStringAttribute("separator");
179-
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
180+
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
180181
targetContents.add(forEachSqlNode);
181182
}
182183
}

src/main/java/org/apache/ibatis/session/Configuration.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public class Configuration {
113113
protected boolean callSettersOnNulls;
114114
protected boolean useActualParamName = true;
115115
protected boolean returnInstanceForEmptyRow;
116+
protected boolean nullableOnForEach;
116117

117118
protected String logPrefix;
118119
protected Class<? extends Log> logImpl;
@@ -266,6 +267,28 @@ public void setReturnInstanceForEmptyRow(boolean returnEmptyInstance) {
266267
this.returnInstanceForEmptyRow = returnEmptyInstance;
267268
}
268269

270+
/**
271+
* Sets the default value of 'nullable' attribute on 'foreach' tag.
272+
*
273+
* @param nullableOnForEach If nullable, set to {@code true}
274+
* @since 3.5.5
275+
*/
276+
public void setNullableOnForEach(boolean nullableOnForEach) {
277+
this.nullableOnForEach = nullableOnForEach;
278+
}
279+
280+
/**
281+
* Returns the default value of of 'nullable' attribute on 'foreach' tag.
282+
*
283+
* <p>Default is {@code false}.
284+
*
285+
* @return If nullable, set to {@code true}
286+
* @since 3.5.5
287+
*/
288+
public boolean isNullableOnForEach() {
289+
return nullableOnForEach;
290+
}
291+
269292
public String getDatabaseId() {
270293
return databaseId;
271294
}

src/test/java/org/apache/ibatis/builder/CustomizedSettingsMapperConfig.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
22
<!--
33
4-
Copyright 2009-2019 the original author or authors.
4+
Copyright 2009-2020 the original author or authors.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -54,6 +54,7 @@
5454
<setting name="vfsImpl" value="org.apache.ibatis.io.JBoss6VFS"/>
5555
<setting name="configurationFactory" value="java.lang.String"/>
5656
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
57+
<setting name="nullableOnForEach" value="true"/>
5758
</settings>
5859

5960
<typeAliases>

src/test/java/org/apache/ibatis/builder/XmlConfigBuilderTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ void shouldSuccessfullyLoadMinimalXMLConfigFile() throws Exception {
100100
assertNull(config.getLogImpl());
101101
assertNull(config.getConfigurationFactory());
102102
assertThat(config.getTypeHandlerRegistry().getTypeHandler(RoundingMode.class)).isInstanceOf(EnumTypeHandler.class);
103+
assertThat(config.isNullableOnForEach()).isFalse();
103104
}
104105
}
105106

@@ -194,6 +195,7 @@ void shouldSuccessfullyLoadXMLConfigFile() throws Exception {
194195
assertThat(config.getLogImpl().getName()).isEqualTo(Slf4jImpl.class.getName());
195196
assertThat(config.getVfsImpl().getName()).isEqualTo(JBoss6VFS.class.getName());
196197
assertThat(config.getConfigurationFactory().getName()).isEqualTo(String.class.getName());
198+
assertThat(config.isNullableOnForEach()).isTrue();
197199

198200
assertThat(config.getTypeAliasRegistry().getTypeAliases().get("blogauthor")).isEqualTo(Author.class);
199201
assertThat(config.getTypeAliasRegistry().getTypeAliases().get("blog")).isEqualTo(Blog.class);

src/test/java/org/apache/ibatis/submitted/foreach/ForEachTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import static com.googlecode.catchexception.apis.BDDCatchException.*;
1919
import static org.assertj.core.api.BDDAssertions.then;
2020

21+
import java.io.IOException;
2122
import java.io.Reader;
23+
import java.sql.SQLException;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
2426
import java.util.Collections;
@@ -139,4 +141,76 @@ void shouldRemoveIndexVariableInTheContext() {
139141
}
140142
}
141143

144+
@Test
145+
void shouldAllowNullWhenAttributeIsOmitAndConfigurationIsDefault() throws IOException, SQLException {
146+
SqlSessionFactory sqlSessionFactory;
147+
try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/foreach/mybatis-config.xml")) {
148+
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
149+
}
150+
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
151+
"org/apache/ibatis/submitted/foreach/CreateDB.sql");
152+
153+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
154+
Mapper mapper = sqlSession.getMapper(Mapper.class);
155+
User user = new User();
156+
user.setFriendList(null);
157+
mapper.countUserWithNullableIsOmit(user);
158+
Assertions.fail();
159+
} catch (PersistenceException e) {
160+
Assertions.assertEquals("The expression 'friendList' evaluated to a null value.", e.getCause().getMessage());
161+
}
162+
}
163+
164+
@Test
165+
void shouldAllowNullWhenAttributeIsOmitAndConfigurationIsTrue() {
166+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
167+
sqlSessionFactory.getConfiguration().setNullableOnForEach(true);
168+
Mapper mapper = sqlSession.getMapper(Mapper.class);
169+
User user = new User();
170+
user.setFriendList(null);
171+
int result = mapper.countUserWithNullableIsOmit(user);
172+
Assertions.assertEquals(6, result);
173+
}
174+
}
175+
176+
@Test
177+
void shouldNotAllowNullWhenAttributeIsOmitAndConfigurationIsFalse() {
178+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
179+
sqlSessionFactory.getConfiguration().setNullableOnForEach(false);
180+
Mapper mapper = sqlSession.getMapper(Mapper.class);
181+
User user = new User();
182+
user.setFriendList(null);
183+
mapper.countUserWithNullableIsOmit(user);
184+
Assertions.fail();
185+
} catch (PersistenceException e) {
186+
Assertions.assertEquals("The expression 'friendList' evaluated to a null value.", e.getCause().getMessage());
187+
}
188+
}
189+
190+
@Test
191+
void shouldAllowNullWhenAttributeIsTrueAndConfigurationIsFalse() {
192+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
193+
sqlSessionFactory.getConfiguration().setNullableOnForEach(false);
194+
Mapper mapper = sqlSession.getMapper(Mapper.class);
195+
User user = new User();
196+
user.setFriendList(null);
197+
int result = mapper.countUserWithNullableIsTrue(user);
198+
Assertions.assertEquals(6, result);
199+
}
200+
}
201+
202+
@Test
203+
void shouldNotAllowNullWhenAttributeIsFalseAndConfigurationIsTrue() {
204+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
205+
sqlSessionFactory.getConfiguration().setNullableOnForEach(true);
206+
Mapper mapper = sqlSession.getMapper(Mapper.class);
207+
User user = new User();
208+
user.setFriendList(null);
209+
mapper.countUserWithNullableIsFalse(user);
210+
Assertions.fail();
211+
} catch (PersistenceException e) {
212+
Assertions.assertEquals("The expression 'friendList' evaluated to a null value.", e.getCause().getMessage());
213+
}
214+
}
215+
142216
}

0 commit comments

Comments
 (0)