Skip to content

Commit 4c68944

Browse files
schaudermp911de
authored andcommitted
Fix derived queries with boolean literals.
`IsTrue` and `IsFalse` queries no longer use a literal in the query, but a bind parameter. This allows Spring Data JDBC or the JDBC driver to convert the passed boolean value to whatever is required in the database. For Oracle converter where added to support storing and loading booleans as NUMBER(1,0) where 0 is false and everything else is true. Closes #908 Original pull request #983
1 parent 8652418 commit 4c68944

File tree

14 files changed

+156
-24
lines changed

14 files changed

+156
-24
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public JdbcCustomConversions jdbcCustomConversions() {
133133
}
134134
}
135135

136-
private List<?> userConverters() {
136+
protected List<?> userConverters() {
137137
return Collections.emptyList();
138138
}
139139

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -402,11 +402,15 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, i
402402
}
403403

404404
if (comparator == Comparator.IS_TRUE) {
405-
return column.isEqualTo(SQL.literalOf(true));
405+
406+
Expression bind = bindBoolean(column, parameterSource, true);
407+
return column.isEqualTo(bind);
406408
}
407409

408410
if (comparator == Comparator.IS_FALSE) {
409-
return column.isEqualTo(SQL.literalOf(false));
411+
412+
Expression bind = bindBoolean(column, parameterSource, false);
413+
return column.isEqualTo(bind);
410414
}
411415

412416
Expression columnExpression = column;
@@ -495,6 +499,12 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, i
495499
}
496500
}
497501

502+
private Expression bindBoolean(Column column, MapSqlParameterSource parameterSource, boolean value) {
503+
504+
Object converted = converter.writeValue(value, ClassTypeInformation.OBJECT);
505+
return bind(converted, Types.BIT, parameterSource, column.getName().getReference());
506+
}
507+
498508
Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIdentifier key) {
499509
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter);
500510
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+17
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,19 @@ void intervalCalculation() {
501501
repository.updateWithIntervalCalculation(23L, LocalDateTime.now());
502502
}
503503

504+
@Test // #908
505+
void derivedQueryWithBooleanLiteralFindsCorrectValues() {
506+
507+
repository.save(createDummyEntity());
508+
DummyEntity entity = createDummyEntity();
509+
entity.flag = true;
510+
entity = repository.save(entity);
511+
512+
List<DummyEntity> result = repository.findByFlagTrue();
513+
514+
assertThat(result).extracting(e -> e.idProp).containsExactly(entity.idProp);
515+
}
516+
504517
private Instant createDummyBeforeAndAfterNow() {
505518

506519
Instant now = Instant.now();
@@ -570,6 +583,8 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
570583
@Modifying
571584
@Query("UPDATE dummy_entity SET point_in_time = :start - interval '30 minutes' WHERE id_prop = :id")
572585
void updateWithIntervalCalculation(@Param("id") Long id, @Param("start") LocalDateTime start);
586+
587+
List<DummyEntity> findByFlagTrue();
573588
}
574589

575590
@Configuration
@@ -616,10 +631,12 @@ public void onApplicationEvent(AbstractRelationalEvent<?> event) {
616631
@Data
617632
@NoArgsConstructor
618633
static class DummyEntity {
634+
619635
String name;
620636
Instant pointInTime;
621637
OffsetDateTime offsetDateTime;
622638
@Id private Long idProp;
639+
boolean flag;
623640

624641
public DummyEntity(String name) {
625642
this.name = name;

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java

+79-7
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.config;
1717

18+
import static java.util.Arrays.*;
1819
import static org.assertj.core.api.Assertions.*;
1920
import static org.mockito.Mockito.*;
2021

21-
import java.util.Arrays;
22-
import java.util.Collections;
22+
import java.util.Collection;
2323
import java.util.List;
24+
import java.util.Optional;
2425
import java.util.function.Consumer;
2526

2627
import org.junit.jupiter.api.Test;
@@ -29,14 +30,17 @@
2930
import org.springframework.context.annotation.Bean;
3031
import org.springframework.context.annotation.Configuration;
3132
import org.springframework.core.convert.converter.Converter;
33+
import org.springframework.data.convert.ReadingConverter;
3234
import org.springframework.data.convert.WritingConverter;
3335
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
3436
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
3537
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3638
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
3739
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
3840
import org.springframework.data.relational.core.dialect.Dialect;
39-
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
41+
import org.springframework.data.relational.core.dialect.LimitClause;
42+
import org.springframework.data.relational.core.dialect.LockClause;
43+
import org.springframework.data.relational.core.sql.render.SelectRenderContext;
4044
import org.springframework.jdbc.core.JdbcOperations;
4145
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4246
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -53,7 +57,7 @@ void configuresInfrastructureComponents() {
5357

5458
assertApplicationContext(context -> {
5559

56-
List<Class<?>> expectedBeanTypes = Arrays.asList(DataAccessStrategy.class, //
60+
List<Class<?>> expectedBeanTypes = asList(DataAccessStrategy.class, //
5761
JdbcMappingContext.class, //
5862
JdbcConverter.class, //
5963
JdbcCustomConversions.class, //
@@ -70,11 +74,26 @@ void configuresInfrastructureComponents() {
7074
void registersSimpleTypesFromCustomConversions() {
7175

7276
assertApplicationContext(context -> {
77+
7378
JdbcMappingContext mappingContext = context.getBean(JdbcMappingContext.class);
7479
assertThat( //
7580
mappingContext.getPersistentEntity(AbstractJdbcConfigurationUnderTest.Blah.class) //
7681
).describedAs("Blah should not be an entity, since there is a WritingConversion configured for it") //
7782
.isNull();
83+
84+
}, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class);
85+
}
86+
87+
@Test // #908
88+
void userProvidedConversionsOverwriteDialectSpecificConversions() {
89+
90+
assertApplicationContext(applicationContext -> {
91+
92+
Optional<Class<?>> customWriteTarget = applicationContext.getBean(JdbcCustomConversions.class)
93+
.getCustomWriteTarget(Boolean.class);
94+
95+
assertThat(customWriteTarget).contains(String.class);
96+
7897
}, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class);
7998
}
8099

@@ -106,12 +125,12 @@ static class AbstractJdbcConfigurationUnderTest extends AbstractJdbcConfiguratio
106125
@Override
107126
@Bean
108127
public Dialect jdbcDialect(NamedParameterJdbcOperations operations) {
109-
return HsqlDbDialect.INSTANCE;
128+
return new DummyDialect();
110129
}
111130

112131
@Override
113-
public JdbcCustomConversions jdbcCustomConversions() {
114-
return new JdbcCustomConversions(Collections.singletonList(Blah2BlubbConverter.INSTANCE));
132+
protected List<?> userConverters() {
133+
return asList(Blah2BlubbConverter.INSTANCE, BooleanToYnConverter.INSTANCE);
115134
}
116135

117136
@WritingConverter
@@ -127,6 +146,59 @@ public Blubb convert(Blah blah) {
127146
private static class Blah {}
128147

129148
private static class Blubb {}
149+
150+
private static class DummyDialect implements Dialect {
151+
@Override
152+
public LimitClause limit() {
153+
return null;
154+
}
155+
156+
@Override
157+
public LockClause lock() {
158+
return null;
159+
}
160+
161+
@Override
162+
public SelectRenderContext getSelectContext() {
163+
return null;
164+
}
165+
166+
@Override
167+
public Collection<Object> getConverters() {
168+
return asList(BooleanToNumberConverter.INSTANCE, NumberToBooleanConverter.INSTANCE);
169+
}
170+
}
171+
172+
@WritingConverter
173+
enum BooleanToNumberConverter implements Converter<Boolean, Number> {
174+
INSTANCE;
175+
176+
@Override
177+
public Number convert(Boolean source) {
178+
return source ? 1 : 0;
179+
}
180+
}
181+
182+
@ReadingConverter
183+
enum NumberToBooleanConverter implements Converter<Number, Boolean> {
184+
INSTANCE;
185+
186+
@Override
187+
public Boolean convert(Number source) {
188+
return source.intValue() == 0;
189+
}
190+
}
191+
192+
@WritingConverter
193+
enum BooleanToYnConverter implements Converter<Boolean, String> {
194+
INSTANCE;
195+
196+
@Override
197+
public String convert(Boolean source) {
198+
return source ? "Y" : "N";
199+
}
200+
}
201+
130202
}
131203

132204
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.List;
2828
import java.util.Properties;
2929

30+
import org.junit.jupiter.api.Disabled;
3031
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.extension.ExtendWith;
3233
import org.mockito.junit.jupiter.MockitoExtension;
@@ -457,7 +458,7 @@ public void createsQueryToFindAllEntitiesByBooleanAttributeTrue() throws Excepti
457458
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
458459
ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType);
459460

460-
assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = TRUE");
461+
assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = :active");
461462
}
462463

463464
@Test // DATAJDBC-318
@@ -468,7 +469,7 @@ public void createsQueryToFindAllEntitiesByBooleanAttributeFalse() throws Except
468469
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
469470
ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType);
470471

471-
assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = FALSE");
472+
assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = :active");
472473
}
473474

474475
@Test // DATAJDBC-318

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ CREATE TABLE dummy_entity
55
id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
66
NAME VARCHAR(100),
77
POINT_IN_TIME TIMESTAMP,
8-
OFFSET_DATE_TIME TIMESTAMP -- with time zone is only supported with z/OS
8+
OFFSET_DATE_TIME TIMESTAMP, -- with time zone is only supported with z/OS
9+
FLAG BOOLEAN
910
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ CREATE TABLE dummy_entity
33
id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
44
NAME VARCHAR(100),
55
POINT_IN_TIME TIMESTAMP,
6-
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE
6+
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
7+
FLAG BOOLEAN
78
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ CREATE TABLE dummy_entity
33
id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
44
NAME VARCHAR(100),
55
POINT_IN_TIME TIMESTAMP,
6-
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE
6+
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
7+
FLAG BOOLEAN
78
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ CREATE TABLE dummy_entity
33
id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY,
44
NAME VARCHAR(100),
55
POINT_IN_TIME TIMESTAMP(3),
6-
OFFSET_DATE_TIME TIMESTAMP(3)
6+
OFFSET_DATE_TIME TIMESTAMP(3),
7+
FLAG BOOLEAN
78
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ CREATE TABLE dummy_entity
44
id_Prop BIGINT IDENTITY PRIMARY KEY,
55
NAME VARCHAR(100),
66
POINT_IN_TIME DATETIME,
7-
OFFSET_DATE_TIME DATETIMEOFFSET
7+
OFFSET_DATE_TIME DATETIMEOFFSET,
8+
FLAG BIT
89
);
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
SET SQL_MODE='ALLOW_INVALID_DATES';
22

3-
CREATE TABLE dummy_entity
3+
CREATE TABLE DUMMY_ENTITY
44
(
5-
id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY,
5+
ID_PROP BIGINT AUTO_INCREMENT PRIMARY KEY,
66
NAME VARCHAR(100),
7-
POINT_IN_TIME TIMESTAMP(3) default null,
8-
OFFSET_DATE_TIME TIMESTAMP(3) default null
7+
POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL,
8+
OFFSET_DATE_TIME TIMESTAMP(3) DEFAULT NULL,
9+
FLAG BIT(1)
910
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ CREATE TABLE DUMMY_ENTITY
55
ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
66
NAME VARCHAR2(100),
77
POINT_IN_TIME TIMESTAMP,
8-
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE
8+
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
9+
FLAG NUMBER(1,0)
910
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ CREATE TABLE dummy_entity
44
id_Prop SERIAL PRIMARY KEY,
55
NAME VARCHAR(100),
66
POINT_IN_TIME TIMESTAMP,
7-
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE
7+
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
8+
FLAG BOOLEAN
89
);

spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
*/
1616
package org.springframework.data.relational.core.dialect;
1717

18+
import org.springframework.core.convert.converter.Converter;
19+
import org.springframework.data.convert.ReadingConverter;
20+
import org.springframework.data.convert.WritingConverter;
21+
1822
import java.util.Collection;
1923
import java.util.Collections;
2024

25+
import static java.util.Arrays.*;
26+
2127
/**
2228
* An SQL dialect for Oracle.
2329
*
@@ -47,7 +53,25 @@ public IdGeneration getIdGeneration() {
4753

4854
@Override
4955
public Collection<Object> getConverters() {
50-
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE);
56+
return asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE, BooleanToIntegerConverter.INSTANCE);
57+
}
58+
59+
@ReadingConverter
60+
enum NumberToBooleanConverter implements Converter<Number, Boolean> {
61+
INSTANCE;
62+
63+
@Override
64+
public Boolean convert(Number number) {
65+
return number.intValue() != 0;
66+
}
5167
}
68+
@WritingConverter
69+
enum BooleanToIntegerConverter implements Converter<Boolean, Integer> {
70+
INSTANCE;
5271

72+
@Override
73+
public Integer convert(Boolean bool) {
74+
return bool ? 1 : 0;
75+
}
76+
}
5377
}

0 commit comments

Comments
 (0)