From 4a2b19b8197a1c690bdfa20fb597a0d47786d47e Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 26 May 2021 06:27:41 +0200 Subject: [PATCH 1/5] 908-boolean-for-derived-queries - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index eeaa0b9e93..460f239464 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-908-boolean-for-derived-queries-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 03d6a5c2a0..a86a0a2724 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-908-boolean-for-derived-queries-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index af9ad0904e..f99d52e942 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.3.0-SNAPSHOT + 2.3.0-908-boolean-for-derived-queries-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-908-boolean-for-derived-queries-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 4e42a006ec..943d75d4f8 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.3.0-SNAPSHOT + 2.3.0-908-boolean-for-derived-queries-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-908-boolean-for-derived-queries-SNAPSHOT From ea1882d7428d375a6455bc72b76038645be2df76 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 26 May 2021 08:04:33 +0200 Subject: [PATCH 2/5] 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 --- .../jdbc/repository/query/QueryMapper.java | 14 ++++++++-- .../JdbcRepositoryIntegrationTests.java | 17 ++++++++++++ .../query/PartTreeJdbcQueryUnitTests.java | 5 ++-- .../JdbcRepositoryIntegrationTests-db2.sql | 3 ++- .../JdbcRepositoryIntegrationTests-h2.sql | 3 ++- .../JdbcRepositoryIntegrationTests-hsql.sql | 3 ++- ...JdbcRepositoryIntegrationTests-mariadb.sql | 3 ++- .../JdbcRepositoryIntegrationTests-mssql.sql | 3 ++- .../JdbcRepositoryIntegrationTests-mysql.sql | 9 ++++--- .../JdbcRepositoryIntegrationTests-oracle.sql | 3 ++- ...dbcRepositoryIntegrationTests-postgres.sql | 3 ++- .../core/dialect/OracleDialect.java | 26 ++++++++++++++++++- 12 files changed, 76 insertions(+), 16 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java index f8ee2d7b1e..328e088295 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java @@ -402,11 +402,15 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, i } 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) { - return column.isEqualTo(SQL.literalOf(false)); + + Expression bind = bindBoolean(column, parameterSource, false); + return column.isEqualTo(bind); } Expression columnExpression = column; @@ -495,6 +499,12 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, i } } + 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) { return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 54497a5d44..f3c30237dc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -492,6 +492,19 @@ public void pageQueryProjectionShouldReturnProjectedEntities() { assertThat(result.getContent().get(0).getName()).isEqualTo("Entity Name"); } + @Test // #908 + void derivedQueryWithBooleanLiteralFindsCorrectValues() { + + repository.save(createDummyEntity()); + DummyEntity entity = createDummyEntity(); + entity.flag = true; + entity = repository.save(entity); + + List result = repository.findByFlagTrue(); + + assertThat(result).extracting(e -> e.idProp).containsExactly(entity.idProp); + } + private Instant createDummyBeforeAndAfterNow() { Instant now = Instant.now(); @@ -557,6 +570,8 @@ interface DummyEntityRepository extends CrudRepository { @Query("SELECT * FROM DUMMY_ENTITY WHERE OFFSET_DATE_TIME > :threshhold") List findByOffsetDateTime(@Param("threshhold") OffsetDateTime threshhold); + + List findByFlagTrue(); } @Configuration @@ -603,10 +618,12 @@ public void onApplicationEvent(AbstractRelationalEvent event) { @Data @NoArgsConstructor static class DummyEntity { + String name; Instant pointInTime; OffsetDateTime offsetDateTime; @Id private Long idProp; + boolean flag; public DummyEntity(String name) { this.name = name; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index cd24ec3960..3416ae0bfa 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Properties; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -457,7 +458,7 @@ public void createsQueryToFindAllEntitiesByBooleanAttributeTrue() throws Excepti RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); 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 @@ -468,7 +469,7 @@ public void createsQueryToFindAllEntitiesByBooleanAttributeFalse() throws Except RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); 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 diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index daa415344a..d41d8accdd 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/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, NAME VARCHAR(100), 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 ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index b9b3101690..5a3f1654a2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/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, NAME VARCHAR(100), POINT_IN_TIME TIMESTAMP, - OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, + FLAG BOOLEAN ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index b9b3101690..5a3f1654a2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/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, NAME VARCHAR(100), POINT_IN_TIME TIMESTAMP, - OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, + FLAG BOOLEAN ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index f9b086443b..663446bdcc 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/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, NAME VARCHAR(100), POINT_IN_TIME TIMESTAMP(3), - OFFSET_DATE_TIME TIMESTAMP(3) + OFFSET_DATE_TIME TIMESTAMP(3), + FLAG BOOLEAN ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index c71942b4c1..f18b9da5cc 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/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, NAME VARCHAR(100), POINT_IN_TIME DATETIME, - OFFSET_DATE_TIME DATETIMEOFFSET + OFFSET_DATE_TIME DATETIMEOFFSET, + FLAG BIT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 9c4085b27a..60e23ca6bd 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1,9 +1,10 @@ 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), - POINT_IN_TIME TIMESTAMP(3) default null, - OFFSET_DATE_TIME TIMESTAMP(3) default null + POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL, + OFFSET_DATE_TIME TIMESTAMP(3) DEFAULT NULL, + FLAG BIT(1) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index a3d831346d..5a92e2a238 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/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, NAME VARCHAR2(100), POINT_IN_TIME TIMESTAMP, - OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, + FLAG NUMBER(1,0) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 5e670bfe77..05f4908e71 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/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, NAME VARCHAR(100), POINT_IN_TIME TIMESTAMP, - OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE + OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, + FLAG BOOLEAN ); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java index 76f0c72cda..90f7d466e7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java +++ b/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; +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.Collections; +import static java.util.Arrays.*; + /** * An SQL dialect for Oracle. * @@ -47,7 +53,25 @@ public IdGeneration getIdGeneration() { @Override public Collection getConverters() { - return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE); + return asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE, BooleanToIntegerConverter.INSTANCE); + } + + @ReadingConverter + enum NumberToBooleanConverter implements Converter { + INSTANCE; + + @Override + public Boolean convert(Number number) { + return number.intValue() != 0; + } } + @WritingConverter + enum BooleanToIntegerConverter implements Converter { + INSTANCE; + @Override + public Integer convert(Boolean bool) { + return bool ? 1 : 0; + } + } } From ec95d9fddc72e7f88d1936b1fe6b74e32013b3bd Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 2 Jun 2021 10:19:53 +0200 Subject: [PATCH 3/5] made userConverters protected and used it in test to register CustomConversions --- .../config/AbstractJdbcConfiguration.java | 2 +- ...ractJdbcConfigurationIntegrationTests.java | 79 ++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index be6398b3cf..7aca5b75f0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -133,7 +133,7 @@ public JdbcCustomConversions jdbcCustomConversions() { } } - private List userConverters() { + protected List userConverters() { return Collections.emptyList(); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java index 1ec72619c9..eb180b6666 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java @@ -15,10 +15,12 @@ */ package org.springframework.data.jdbc.repository.config; +import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -29,14 +31,19 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; 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.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -53,7 +60,7 @@ void configuresInfrastructureComponents() { assertApplicationContext(context -> { - List> expectedBeanTypes = Arrays.asList(DataAccessStrategy.class, // + List> expectedBeanTypes = asList(DataAccessStrategy.class, // JdbcMappingContext.class, // JdbcConverter.class, // JdbcCustomConversions.class, // @@ -106,12 +113,12 @@ static class AbstractJdbcConfigurationUnderTest extends AbstractJdbcConfiguratio @Override @Bean public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { - return HsqlDbDialect.INSTANCE; + return new DummyDialect(); } @Override - public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(Collections.singletonList(Blah2BlubbConverter.INSTANCE)); + protected List userConverters() { + return asList(Blah2BlubbConverter.INSTANCE); } @WritingConverter @@ -127,6 +134,70 @@ public Blubb convert(Blah blah) { private static class Blah {} 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 getConverters() { + return asList(BooleanToNumberConverter.INSTANCE); + } + } + + @WritingConverter + enum BooleanToNumberConverter implements Converter{ + INSTANCE; + + @Override + public Number convert(Boolean source) { + return source ? 1 : 0; + } + } + + @ReadingConverter + enum NumberToBooleanConverter implements Converter{ + INSTANCE; + + @Override + public Boolean convert(Number source) { + return source.intValue() == 0; + } + } + + + @WritingConverter + enum BooleanToYnConverter implements Converter{ + INSTANCE; + + @Override + public String convert(Boolean source) { + return source ? "Y" : "N"; + } + } + + @ReadingConverter + enum StringToBooleanConverter implements Converter{ + INSTANCE; + + @Override + public Boolean convert(String source) { + return source.equals("Y") ; + } + } + } } From 4106cda15037119c8f4fb5a065ee1a90a4352d57 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 2 Jun 2021 11:28:02 +0200 Subject: [PATCH 4/5] Ensure that user provided custom conversions overwrite Dialect conversions. --- ...ractJdbcConfigurationIntegrationTests.java | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java index eb180b6666..e2cad273d7 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java @@ -19,10 +19,9 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -37,10 +36,8 @@ import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; -import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; 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; @@ -77,11 +74,26 @@ void configuresInfrastructureComponents() { void registersSimpleTypesFromCustomConversions() { assertApplicationContext(context -> { + JdbcMappingContext mappingContext = context.getBean(JdbcMappingContext.class); assertThat( // mappingContext.getPersistentEntity(AbstractJdbcConfigurationUnderTest.Blah.class) // ).describedAs("Blah should not be an entity, since there is a WritingConversion configured for it") // .isNull(); + + }, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class); + } + + @Test // #908 + void userProvidedConversionsOverwriteDialectSpecificConversions() { + + assertApplicationContext(applicationContext -> { + + Optional> customWriteTarget = applicationContext.getBean(JdbcCustomConversions.class) + .getCustomWriteTarget(Boolean.class); + + assertThat(customWriteTarget).contains(String.class); + }, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class); } @@ -118,7 +130,7 @@ public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { @Override protected List userConverters() { - return asList(Blah2BlubbConverter.INSTANCE); + return asList(Blah2BlubbConverter.INSTANCE, BooleanToYnConverter.INSTANCE); } @WritingConverter @@ -153,12 +165,12 @@ public SelectRenderContext getSelectContext() { @Override public Collection getConverters() { - return asList(BooleanToNumberConverter.INSTANCE); + return asList(BooleanToNumberConverter.INSTANCE, NumberToBooleanConverter.INSTANCE); } } @WritingConverter - enum BooleanToNumberConverter implements Converter{ + enum BooleanToNumberConverter implements Converter { INSTANCE; @Override @@ -168,7 +180,7 @@ public Number convert(Boolean source) { } @ReadingConverter - enum NumberToBooleanConverter implements Converter{ + enum NumberToBooleanConverter implements Converter { INSTANCE; @Override @@ -177,9 +189,8 @@ public Boolean convert(Number source) { } } - @WritingConverter - enum BooleanToYnConverter implements Converter{ + enum BooleanToYnConverter implements Converter { INSTANCE; @Override @@ -188,16 +199,6 @@ public String convert(Boolean source) { } } - @ReadingConverter - enum StringToBooleanConverter implements Converter{ - INSTANCE; - - @Override - public Boolean convert(String source) { - return source.equals("Y") ; - } - } - } } From b32beaa1d62bba7358851dfd47f9263c6ee05255 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 2 Jun 2021 11:56:49 +0200 Subject: [PATCH 5/5] Improving the documentation. --- .../asciidoc/jdbc-custom-conversions.adoc | 26 ++++++++--- src/main/asciidoc/jdbc.adoc | 43 +------------------ 2 files changed, 22 insertions(+), 47 deletions(-) diff --git a/src/main/asciidoc/jdbc-custom-conversions.adoc b/src/main/asciidoc/jdbc-custom-conversions.adoc index ea4c5fd204..2a7a1b2f5f 100644 --- a/src/main/asciidoc/jdbc-custom-conversions.adoc +++ b/src/main/asciidoc/jdbc-custom-conversions.adoc @@ -1,4 +1,6 @@ [[jdbc.custom-converters]] +// for backward compatibility only: +[[jdbc.entity-persistence.custom-converters]] == Custom Conversions Spring Data JDBC allows registration of custom converters to influence how values are mapped in the database. @@ -55,12 +57,26 @@ class MyJdbcConfiguration extends AbstractJdbcConfiguration { // … - @Overwrite - @Bean - public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter())); - } + @Override + protected List userConverters() { + return Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter()); + } + } ---- +NOTE: In previous versions of Spring Data JDBC it was recommended to directly overwrite `AbstractJdbcConfiguration.jdbcCustomConversions()`. +This is no longer necessary or even recommended, since that method assembles conversions intended for all databases, conversions registered by the `Dialect` used and conversions registered by the user. +If you are migrating from an older version of Spring Data JDBC and have `AbstractJdbcConfiguration.jdbcCustomConversions()` overwritten conversions from your `Dialect` will not get registered. + +[[jdbc.custom-converters.jdbc-value]] +// for backward compatibility only: +[[jdbc.entity-persistence.custom-converters.jdbc-value]] +=== JdbcValue + +Value conversion uses `JdbcValue` to enrich values propagated to JDBC operations with a `java.sql.Types` type. +Register a custom write converter if you need to specify a JDBC-specific type instead of using type derivation. +This converter should convert the value to `JdbcValue` which has a field for the value and for the actual `JDBCType`. + + include::{spring-data-commons-docs}/custom-conversions.adoc[leveloffset=+3] diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index d55d0b1d11..ce7d4c2538 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -263,48 +263,7 @@ p1.bestFriend = AggregateReference.to(p2.id); ---- ==== -[[jdbc.entity-persistence.custom-converters]] -=== Custom converters - -Custom converters can be registered, for types that are not supported by default, by inheriting your configuration from `AbstractJdbcConfiguration` and overwriting the method `jdbcCustomConversions()`. - -==== -[source,java] ----- -@Configuration -class DataJdbcConfiguration extends AbstractJdbcConfiguration { - - @Override - public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(Collections.singletonList(TimestampTzToDateConverter.INSTANCE)); - } - - @ReadingConverter - enum TimestampTzToDateConverter implements Converter { - - INSTANCE; - - @Override - public Date convert(TIMESTAMPTZ source) { - //... - } - } -} ----- -==== - -The constructor of `JdbcCustomConversions` accepts a list of `org.springframework.core.convert.converter.Converter`. - -Converters should be annotated with `@ReadingConverter` or `@WritingConverter` in order to control their applicability to only reading from or to writing to the database. - -`TIMESTAMPTZ` in the example is a database specific data type that needs conversion into something more suitable for a domain model. - -[[jdbc.entity-persistence.custom-converters.jdbc-value]] -==== JdbcValue - -Value conversion uses `JdbcValue` to enrich values propagated to JDBC operations with a `java.sql.Types` type. -Register a custom write converter if you need to specify a JDBC-specific type instead of using type derivation. -This converter should convert the value to `JdbcValue` which has a field for the value and for the actual `JDBCType`. +* Types for which you registered suitable [[jdbc.custom-converters, custom conversions]]. [[jdbc.entity-persistence.naming-strategy]] === `NamingStrategy`