Skip to content

Commit e501fec

Browse files
authored
Merge pull request #229 from jeffgbutler/gh-228
Add Ability to Write "in" Conditions that will render even when the list of value is empty
2 parents 52d939d + e8aeab3 commit e501fec

File tree

5 files changed

+65
-2
lines changed

5 files changed

+65
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ In the next major release of the library, all deprecated code will be removed.
2424
- Added the `applyOperator` function to make it easy to use non-standard database operators in expressions ([#220](https://github.com/mybatis/mybatis-dynamic-sql/issues/220))
2525
- Added convenience methods for count(column) and count(distinct column) ([#221](https://github.com/mybatis/mybatis-dynamic-sql/issues/221))
2626
- Added support for union queries in Kotlin ([#187](https://github.com/mybatis/mybatis-dynamic-sql/issues/187))
27+
- Added the ability to write "in" conditions that will render even if empty ([#228](https://github.com/mybatis/mybatis-dynamic-sql/issues/228))
2728
- Many enhancements for Spring including:
2829
- Fixed a bug where multi-row insert statements did not render properly for Spring ([#224](https://github.com/mybatis/mybatis-dynamic-sql/issues/224))
2930
- Added support for a parameter type converter for use cases where the Java type of a column does not match the database column type ([#131](https://github.com/mybatis/mybatis-dynamic-sql/issues/131))

src/main/java/org/mybatis/dynamic/sql/AbstractListValueCondition.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2016-2019 the original author or authors.
2+
* Copyright 2016-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.
@@ -25,6 +25,7 @@
2525
public abstract class AbstractListValueCondition<T> implements VisitableCondition<T> {
2626
protected Collection<T> values;
2727
protected UnaryOperator<Stream<T>> valueStreamTransformer;
28+
protected boolean skipRenderingWhenEmpty = true;
2829

2930
protected AbstractListValueCondition(Collection<T> values) {
3031
this(values, UnaryOperator.identity());
@@ -39,6 +40,17 @@ public final <R> Stream<R> mapValues(Function<T, R> mapper) {
3940
return valueStreamTransformer.apply(values.stream()).map(mapper);
4041
}
4142

43+
public boolean skipRenderingWhenEmpty() {
44+
return skipRenderingWhenEmpty;
45+
}
46+
47+
/**
48+
* Use with caution - this could cause the library to render invalid SQL like "where column in ()".
49+
*/
50+
protected void forceRenderingWhenEmpty() {
51+
skipRenderingWhenEmpty = false;
52+
}
53+
4254
@Override
4355
public <R> R accept(ConditionVisitor<T, R> visitor) {
4456
return visitor.visit(this);

src/main/java/org/mybatis/dynamic/sql/where/render/WhereConditionVisitor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public Optional<FragmentAndParameters> visit(AbstractListValueCondition<T> condi
5555
FragmentCollector fc = condition.mapValues(this::toFragmentAndParameters)
5656
.collect(FragmentCollector.collect());
5757

58-
if (fc.isEmpty()) {
58+
if (fc.isEmpty() && condition.skipRenderingWhenEmpty()) {
5959
return Optional.empty();
6060
}
6161

src/site/markdown/docs/conditions.md

+17
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,23 @@ The library supplies several specializations of optional conditions to be used i
124124
### Optionality with the "In" Conditions
125125
Optionality with the "in" and "not in" conditions is a bit more complex than the other types of conditions. The first thing to know is that no "in" or "not in" condition will render if the list of values is empty. For example, there will never be rendered SQL like `where name in ()`. So optionality of the "in" conditions is more about optionality of the *values* of the condition. The library comes with functions that will filter out null values, and will upper case String values to enable case insensitive queries. There are extension points to add additional filtering and mapping if you so desire.
126126

127+
We think it is a good thing that the library will not render invalid SQL. Normally an "in" condition will be dropped from rendering if the list of values is empty - either through filtering or from the creation of the list. But there is some danger with this stance. Because the condition could be dropped from the rendered SQL, more rows could be impacted than expected if the list ends up empty for whatever reason. Our recommended solution is to make sure that you validate list values - especially if they are coming from direct user input. Another option is to force the conditions to render even if they are empty - which will cause a database error in most cases. If you want to force "in" conditions to render even if they are empty, you will need to create your own condition and configure it to render when empty. This is easily done by subclassing one of the existing conditions. For example:
128+
129+
```java
130+
public class IsInRequired<T> extends IsIn<T> {
131+
protected IsInRequired(Collection<T> values) {
132+
super(values);
133+
forceRenderingWhenEmpty(); // calling this method will force the condition to render even if the values list is empty
134+
}
135+
136+
public static <T> IsInRequired<T> isIn(Collection<T> values) {
137+
return new IsInRequired<>(values);
138+
}
139+
}
140+
```
141+
142+
Note that we do not supply conditions like this as a part of the standard library because we believe that forcing the library to render invalid SQL is an extreme measure and should be undertaken with care.
143+
127144
The following table shows the different supplied In conditions and how they will render for different sets of inputs. The table assumes the following types of input:
128145

129146
- Example 1 assumes an input list of ("foo", null, "bar") - like `where(name, isIn("foo", null, "bar"))`

src/test/java/examples/animal/data/AnimalDataTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static examples.animal.data.AnimalDataDynamicSqlSupport.*;
1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2021
import static org.assertj.core.api.Assertions.within;
2122
import static org.junit.jupiter.api.Assertions.assertAll;
2223
import static org.mybatis.dynamic.sql.SqlBuilder.*;
@@ -26,10 +27,13 @@
2627
import java.sql.Connection;
2728
import java.sql.DriverManager;
2829
import java.util.ArrayList;
30+
import java.util.Collection;
31+
import java.util.Collections;
2932
import java.util.List;
3033
import java.util.Map;
3134

3235
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
36+
import org.apache.ibatis.exceptions.PersistenceException;
3337
import org.apache.ibatis.jdbc.ScriptRunner;
3438
import org.apache.ibatis.mapping.Environment;
3539
import org.apache.ibatis.session.Configuration;
@@ -52,6 +56,7 @@
5256
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
5357
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
5458
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
59+
import org.mybatis.dynamic.sql.where.condition.IsIn;
5560
import org.mybatis.dynamic.sql.where.render.WhereClauseProvider;
5661

5762
class AnimalDataTest {
@@ -564,6 +569,34 @@ void testInCondition() {
564569
}
565570
}
566571

572+
@Test
573+
void testInConditionWithEmptyList() {
574+
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
575+
AnimalDataMapper mapper = sqlSession.getMapper(AnimalDataMapper.class);
576+
577+
SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight)
578+
.from(animalData)
579+
.where(id, IsInRequired.isIn(Collections.emptyList()))
580+
.build()
581+
.render(RenderingStrategies.MYBATIS3);
582+
583+
assertThatExceptionOfType(PersistenceException.class).isThrownBy(() -> {
584+
mapper.selectMany(selectStatement);
585+
});
586+
}
587+
}
588+
589+
public static class IsInRequired<T> extends IsIn<T> {
590+
protected IsInRequired(Collection<T> values) {
591+
super(values);
592+
forceRenderingWhenEmpty();
593+
}
594+
595+
public static <T> IsInRequired<T> isIn(Collection<T> values) {
596+
return new IsInRequired<>(values);
597+
}
598+
}
599+
567600
@Test
568601
void testInCaseSensitiveCondition() {
569602
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {

0 commit comments

Comments
 (0)