Browse Source

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
pull/989/head
Jens Schauder 5 years ago committed by Mark Paluch
parent
commit
4c6894464e
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 2
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java
  2. 14
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java
  3. 17
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  4. 86
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java
  5. 5
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java
  6. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql
  7. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql
  8. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql
  9. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql
  10. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql
  11. 9
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql
  12. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql
  13. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql
  14. 26
      spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java

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

@ -133,7 +133,7 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware {
} }
} }
private List<?> userConverters() { protected List<?> userConverters() {
return Collections.emptyList(); return Collections.emptyList();
} }

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

@ -402,11 +402,15 @@ class QueryMapper {
} }
if (comparator == Comparator.IS_TRUE) { if (comparator == Comparator.IS_TRUE) {
return column.isEqualTo(SQL.literalOf(true));
Expression bind = bindBoolean(column, parameterSource, true);
return column.isEqualTo(bind);
} }
if (comparator == Comparator.IS_FALSE) { if (comparator == Comparator.IS_FALSE) {
return column.isEqualTo(SQL.literalOf(false));
Expression bind = bindBoolean(column, parameterSource, false);
return column.isEqualTo(bind);
} }
Expression columnExpression = column; Expression columnExpression = column;
@ -495,6 +499,12 @@ class QueryMapper {
} }
} }
private Expression bindBoolean(Column column, MapSqlParameterSource parameterSource, boolean value) {
Object converted = converter.writeValue(value, ClassTypeInformation.OBJECT);
return bind(converted, Types.BIT, parameterSource, column.getName().getReference());
}
Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIdentifier key) { Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIdentifier key) {
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter); return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter);
} }

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

@ -501,6 +501,19 @@ public class JdbcRepositoryIntegrationTests {
repository.updateWithIntervalCalculation(23L, LocalDateTime.now()); repository.updateWithIntervalCalculation(23L, LocalDateTime.now());
} }
@Test // #908
void derivedQueryWithBooleanLiteralFindsCorrectValues() {
repository.save(createDummyEntity());
DummyEntity entity = createDummyEntity();
entity.flag = true;
entity = repository.save(entity);
List<DummyEntity> result = repository.findByFlagTrue();
assertThat(result).extracting(e -> e.idProp).containsExactly(entity.idProp);
}
private Instant createDummyBeforeAndAfterNow() { private Instant createDummyBeforeAndAfterNow() {
Instant now = Instant.now(); Instant now = Instant.now();
@ -570,6 +583,8 @@ public class JdbcRepositoryIntegrationTests {
@Modifying @Modifying
@Query("UPDATE dummy_entity SET point_in_time = :start - interval '30 minutes' WHERE id_prop = :id") @Query("UPDATE dummy_entity SET point_in_time = :start - interval '30 minutes' WHERE id_prop = :id")
void updateWithIntervalCalculation(@Param("id") Long id, @Param("start") LocalDateTime start); void updateWithIntervalCalculation(@Param("id") Long id, @Param("start") LocalDateTime start);
List<DummyEntity> findByFlagTrue();
} }
@Configuration @Configuration
@ -616,10 +631,12 @@ public class JdbcRepositoryIntegrationTests {
@Data @Data
@NoArgsConstructor @NoArgsConstructor
static class DummyEntity { static class DummyEntity {
String name; String name;
Instant pointInTime; Instant pointInTime;
OffsetDateTime offsetDateTime; OffsetDateTime offsetDateTime;
@Id private Long idProp; @Id private Long idProp;
boolean flag;
public DummyEntity(String name) { public DummyEntity(String name) {
this.name = name; this.name = name;

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

@ -15,12 +15,13 @@
*/ */
package org.springframework.data.jdbc.repository.config; package org.springframework.data.jdbc.repository.config;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.util.Arrays; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -29,6 +30,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
@ -36,7 +38,9 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.HsqlDbDialect; import org.springframework.data.relational.core.dialect.LimitClause;
import org.springframework.data.relational.core.dialect.LockClause;
import org.springframework.data.relational.core.sql.render.SelectRenderContext;
import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@ -53,7 +57,7 @@ public class AbstractJdbcConfigurationIntegrationTests {
assertApplicationContext(context -> { assertApplicationContext(context -> {
List<Class<?>> expectedBeanTypes = Arrays.asList(DataAccessStrategy.class, // List<Class<?>> expectedBeanTypes = asList(DataAccessStrategy.class, //
JdbcMappingContext.class, // JdbcMappingContext.class, //
JdbcConverter.class, // JdbcConverter.class, //
JdbcCustomConversions.class, // JdbcCustomConversions.class, //
@ -70,11 +74,26 @@ public class AbstractJdbcConfigurationIntegrationTests {
void registersSimpleTypesFromCustomConversions() { void registersSimpleTypesFromCustomConversions() {
assertApplicationContext(context -> { assertApplicationContext(context -> {
JdbcMappingContext mappingContext = context.getBean(JdbcMappingContext.class); JdbcMappingContext mappingContext = context.getBean(JdbcMappingContext.class);
assertThat( // assertThat( //
mappingContext.getPersistentEntity(AbstractJdbcConfigurationUnderTest.Blah.class) // mappingContext.getPersistentEntity(AbstractJdbcConfigurationUnderTest.Blah.class) //
).describedAs("Blah should not be an entity, since there is a WritingConversion configured for it") // ).describedAs("Blah should not be an entity, since there is a WritingConversion configured for it") //
.isNull(); .isNull();
}, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class);
}
@Test // #908
void userProvidedConversionsOverwriteDialectSpecificConversions() {
assertApplicationContext(applicationContext -> {
Optional<Class<?>> customWriteTarget = applicationContext.getBean(JdbcCustomConversions.class)
.getCustomWriteTarget(Boolean.class);
assertThat(customWriteTarget).contains(String.class);
}, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class); }, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class);
} }
@ -106,12 +125,12 @@ public class AbstractJdbcConfigurationIntegrationTests {
@Override @Override
@Bean @Bean
public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { public Dialect jdbcDialect(NamedParameterJdbcOperations operations) {
return HsqlDbDialect.INSTANCE; return new DummyDialect();
} }
@Override @Override
public JdbcCustomConversions jdbcCustomConversions() { protected List<?> userConverters() {
return new JdbcCustomConversions(Collections.singletonList(Blah2BlubbConverter.INSTANCE)); return asList(Blah2BlubbConverter.INSTANCE, BooleanToYnConverter.INSTANCE);
} }
@WritingConverter @WritingConverter
@ -127,6 +146,59 @@ public class AbstractJdbcConfigurationIntegrationTests {
private static class Blah {} private static class Blah {}
private static class Blubb {} private static class Blubb {}
private static class DummyDialect implements Dialect {
@Override
public LimitClause limit() {
return null;
}
@Override
public LockClause lock() {
return null;
}
@Override
public SelectRenderContext getSelectContext() {
return null;
}
@Override
public Collection<Object> getConverters() {
return asList(BooleanToNumberConverter.INSTANCE, NumberToBooleanConverter.INSTANCE);
}
}
@WritingConverter
enum BooleanToNumberConverter implements Converter<Boolean, Number> {
INSTANCE;
@Override
public Number convert(Boolean source) {
return source ? 1 : 0;
}
}
@ReadingConverter
enum NumberToBooleanConverter implements Converter<Number, Boolean> {
INSTANCE;
@Override
public Boolean convert(Number source) {
return source.intValue() == 0;
}
}
@WritingConverter
enum BooleanToYnConverter implements Converter<Boolean, String> {
INSTANCE;
@Override
public String convert(Boolean source) {
return source ? "Y" : "N";
}
}
} }
} }

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

@ -27,6 +27,7 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
@ -457,7 +458,7 @@ public class PartTreeJdbcQueryUnitTests {
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType);
assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = TRUE"); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = :active");
} }
@Test // DATAJDBC-318 @Test // DATAJDBC-318
@ -468,7 +469,7 @@ public class PartTreeJdbcQueryUnitTests {
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType);
assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = FALSE"); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = :active");
} }
@Test // DATAJDBC-318 @Test // DATAJDBC-318

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

@ -5,5 +5,6 @@ CREATE TABLE dummy_entity
id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP -- with time zone is only supported with z/OS OFFSET_DATE_TIME TIMESTAMP, -- with time zone is only supported with z/OS
FLAG BOOLEAN
); );

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

@ -3,5 +3,6 @@ CREATE TABLE dummy_entity
id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG BOOLEAN
); );

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

@ -3,5 +3,6 @@ CREATE TABLE dummy_entity
id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG BOOLEAN
); );

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

@ -3,5 +3,6 @@ CREATE TABLE dummy_entity
id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME TIMESTAMP(3), POINT_IN_TIME TIMESTAMP(3),
OFFSET_DATE_TIME TIMESTAMP(3) OFFSET_DATE_TIME TIMESTAMP(3),
FLAG BOOLEAN
); );

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

@ -4,5 +4,6 @@ CREATE TABLE dummy_entity
id_Prop BIGINT IDENTITY PRIMARY KEY, id_Prop BIGINT IDENTITY PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME DATETIME, POINT_IN_TIME DATETIME,
OFFSET_DATE_TIME DATETIMEOFFSET OFFSET_DATE_TIME DATETIMEOFFSET,
FLAG BIT
); );

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

@ -1,9 +1,10 @@
SET SQL_MODE='ALLOW_INVALID_DATES'; SET SQL_MODE='ALLOW_INVALID_DATES';
CREATE TABLE dummy_entity CREATE TABLE DUMMY_ENTITY
( (
id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, ID_PROP BIGINT AUTO_INCREMENT PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME TIMESTAMP(3) default null, POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL,
OFFSET_DATE_TIME TIMESTAMP(3) default null OFFSET_DATE_TIME TIMESTAMP(3) DEFAULT NULL,
FLAG BIT(1)
); );

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

@ -5,5 +5,6 @@ CREATE TABLE DUMMY_ENTITY
ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
NAME VARCHAR2(100), NAME VARCHAR2(100),
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG NUMBER(1,0)
); );

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

@ -4,5 +4,6 @@ CREATE TABLE dummy_entity
id_Prop SERIAL PRIMARY KEY, id_Prop SERIAL PRIMARY KEY,
NAME VARCHAR(100), NAME VARCHAR(100),
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG BOOLEAN
); );

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

@ -15,9 +15,15 @@
*/ */
package org.springframework.data.relational.core.dialect; package org.springframework.data.relational.core.dialect;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import static java.util.Arrays.*;
/** /**
* An SQL dialect for Oracle. * An SQL dialect for Oracle.
* *
@ -47,7 +53,25 @@ public class OracleDialect extends AnsiDialect {
@Override @Override
public Collection<Object> getConverters() { public Collection<Object> getConverters() {
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); return asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE, BooleanToIntegerConverter.INSTANCE);
}
@ReadingConverter
enum NumberToBooleanConverter implements Converter<Number, Boolean> {
INSTANCE;
@Override
public Boolean convert(Number number) {
return number.intValue() != 0;
}
} }
@WritingConverter
enum BooleanToIntegerConverter implements Converter<Boolean, Integer> {
INSTANCE;
@Override
public Integer convert(Boolean bool) {
return bool ? 1 : 0;
}
}
} }

Loading…
Cancel
Save