diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index e2aa77bf..a7d1bf84 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.jdbc.core.convert.ResultSetWrapper.SpecialColumnValue; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.MappingException; @@ -34,11 +35,10 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; -import org.springframework.data.relational.core.mapping.Embedded; -import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -146,7 +146,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return getColumnType(referencedEntity.getRequiredIdProperty()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.convert.JdbcConverter#getSqlType(org.springframework.data.relational.core.mapping.RelationalPersistentProperty) */ @@ -155,7 +155,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return JdbcUtil.sqlTypeFor(getColumnType(property)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.convert.JdbcConverter#getColumnType(org.springframework.data.relational.core.mapping.RelationalPersistentProperty) */ @@ -199,8 +199,8 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc @Nullable public Object readValue(@Nullable Object value, TypeInformation type) { - if (null == value) { - return null; + if (value == null || value == SpecialColumnValue.NO_SUCH_COLUMN) { + return value; } if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { @@ -322,7 +322,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc @SuppressWarnings("unchecked") @Override - public T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key) { + public T mapRow(RelationalPersistentEntity entity, ResultSetWrapper resultSet, Object key) { return new ReadingContext( new PersistentPropertyPathExtension( getMappingContext(), entity), @@ -330,7 +330,8 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc } @Override - public T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key) { + public T mapRow(PersistentPropertyPathExtension path, ResultSetWrapper resultSet, Identifier identifier, + Object key) { return new ReadingContext(path, resultSet, identifier, key).mapRow(); } @@ -338,14 +339,17 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private final RelationalPersistentEntity entity; - private final ResultSet resultSet; + private final ResultSetWrapper resultSet; private final PersistentPropertyPathExtension rootPath; private final PersistentPropertyPathExtension path; private final Identifier identifier; private final Object key; + private final PropertyValueProvider propertyValueProvider; + private final PropertyValueProvider backReferencePropertyValueProvider; + @SuppressWarnings("unchecked") - private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSet resultSet, Identifier identifier, + private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSetWrapper resultSet, Identifier identifier, Object key) { RelationalPersistentEntity entity = (RelationalPersistentEntity) rootPath.getLeafEntity(); @@ -360,9 +364,12 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc this.entity); this.identifier = identifier; this.key = key; + propertyValueProvider = new JdbcPropertyValueProvider(identifierProcessing, path, resultSet); + backReferencePropertyValueProvider = new JdbcBackReferencePropertyValueProvider(identifierProcessing, path, + resultSet); } - private ReadingContext(RelationalPersistentEntity entity, ResultSet resultSet, + private ReadingContext(RelationalPersistentEntity entity, ResultSetWrapper resultSet, PersistentPropertyPathExtension rootPath, PersistentPropertyPathExtension path, Identifier identifier, Object key) { @@ -372,6 +379,10 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc this.path = path; this.identifier = identifier; this.key = key; + + propertyValueProvider = new JdbcPropertyValueProvider(identifierProcessing, path, resultSet); + backReferencePropertyValueProvider = new JdbcBackReferencePropertyValueProvider(identifierProcessing, path, + resultSet); } private ReadingContext extendBy(RelationalPersistentProperty property) { @@ -401,33 +412,9 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc continue; } - // check if property is in the result set - // if not - leave it out - // DATAJDBC-341 - if (property.isEntity() || property.isEmbedded()) { - propertyAccessor.setProperty(property, readOrLoadProperty(idValue, property)); - } else { - try { - if (resultSet.findColumn(property.getColumnName().getReference(identifierProcessing)) > 0) { - propertyAccessor.setProperty(property, readOrLoadProperty(idValue, property)); - } else { - try { - propertyAccessor.setProperty(property, readOrLoadProperty(idValue, property)); - } catch (Exception exception) { - LOG.info( - "The result set is not corresponding to the target entity. Left out properties will be set to standard values (NULL for reference types, 0 for primitives."); - } - } - } catch (SQLException e) { - String columnAlias = path.extendBy(property).getColumnAlias().getReference(identifierProcessing); - try { - if (resultSet.findColumn(columnAlias) > 0) { - propertyAccessor.setProperty(property, readOrLoadProperty(idValue, property)); - } - } catch (SQLException ex) { - LOG.info(String.format("Cannot find column named %s within the result set!", property.getColumnName())); - } - } + Object value = readOrLoadProperty(idValue, property); + if (value != SpecialColumnValue.NO_SUCH_COLUMN) { + propertyAccessor.setProperty(property, value); } } @@ -476,12 +463,12 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc private Object readFrom(RelationalPersistentProperty property) { if (property.isEntity()) { - return readEntityFrom(property, path); + return readEntityFrom(property); } - Object value = getObjectFromResultSet( - path.extendBy(property).getColumnAlias().getReference(identifierProcessing)); - return readValue(value, property.getTypeInformation()); + Object value = propertyValueProvider.getPropertyValue(property); + return value == SpecialColumnValue.NO_SUCH_COLUMN ? SpecialColumnValue.NO_SUCH_COLUMN + : readValue(value, property.getTypeInformation()); } @Nullable @@ -497,7 +484,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc } private boolean shouldCreateEmptyEmbeddedInstance(RelationalPersistentProperty property) { - return OnEmpty.USE_EMPTY.equals(property.findAnnotation(Embedded.class).onEmpty()); + return property.shouldCreateEmptyEmbedded(); } private boolean hasInstanceValues(@Nullable Object idValue) { @@ -513,7 +500,8 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return true; } - if (readOrLoadProperty(idValue, embeddedProperty) != null) { + Object value = readOrLoadProperty(idValue, embeddedProperty); + if (value != null && value != SpecialColumnValue.NO_SUCH_COLUMN) { return true; } } @@ -523,7 +511,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc @Nullable @SuppressWarnings("unchecked") - private Object readEntityFrom(RelationalPersistentProperty property, PersistentPropertyPathExtension path) { + private Object readEntityFrom(RelationalPersistentProperty property) { ReadingContext newContext = extendBy(property); RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(property.getActualType()); @@ -534,8 +522,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc if (idProperty != null) { idValue = newContext.readFrom(idProperty); } else { - idValue = newContext.getObjectFromResultSet( - path.extendBy(property).getReverseColumnNameAlias().getReference(identifierProcessing)); + idValue = backReferencePropertyValueProvider.getPropertyValue(property); } if (idValue == null) { @@ -545,17 +532,6 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc return newContext.createInstanceInternal(idValue); } - @Nullable - private Object getObjectFromResultSet(String backreferenceName) { - - try { - return resultSet.getObject(backreferenceName); - } catch (SQLException o_O) { - LOG.info(String.format("Could not read value %s from result set! Use null as value.", backreferenceName), o_O); - return null; - } - } - private T createInstanceInternal(@Nullable Object idValue) { T instance = createInstance(entity, parameter -> { @@ -566,7 +542,16 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameterName); - return readOrLoadProperty(idValue, property); + Object value = readOrLoadProperty(idValue, property); + + if (value == SpecialColumnValue.NO_SUCH_COLUMN) { + + throw new MappingException( + String.format("Couldn't create instance of type %s with id. No column found for argument named '%s'.", + entity.getType(), parameterName)); + } + + return value; }); return populateProperties(instance, idValue); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java index 27785ee1..80a37309 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java @@ -64,8 +64,11 @@ public class EntityRowMapper implements RowMapper { @Override public T mapRow(ResultSet resultSet, int rowNumber) { - return path == null ? converter.mapRow(entity, resultSet, rowNumber) - : converter.mapRow(path, resultSet, identifier, rowNumber); + ResultSetWrapper wrappedResultSet = new ResultSetWrapper(resultSet); + + return path == null // + ? converter.mapRow(entity, wrappedResultSet, rowNumber) // + : converter.mapRow(path, wrappedResultSet, identifier, rowNumber); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java new file mode 100644 index 00000000..54f5627e --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * {@link PropertyValueProvider} obtaining values from a {@link ResultSetWrapper}. + * For a given id property it provides the value in the resultset under which other entities refer back to it. + * + * + * @author Jens Schauder + * @since 2.0 + */ +class JdbcBackReferencePropertyValueProvider implements PropertyValueProvider { + + private final IdentifierProcessing identifierProcessing; + private final PersistentPropertyPathExtension basePath; + private final ResultSetWrapper resultSet; + + + /** + * @param identifierProcessing used for converting the {@link org.springframework.data.relational.core.sql.SqlIdentifier} from a property to a column label + * @param basePath path from the aggregate root relative to which all properties get resolved. + * @param resultSet the {@link ResultSetWrapper} from which to obtain the actual values. + */ + JdbcBackReferencePropertyValueProvider(IdentifierProcessing identifierProcessing, PersistentPropertyPathExtension basePath, ResultSetWrapper resultSet) { + + this.resultSet = resultSet; + this.basePath = basePath; + this.identifierProcessing = identifierProcessing; + } + + @Override + public T getPropertyValue(RelationalPersistentProperty property) { + return (T)resultSet.getObject(basePath.extendBy(property).getReverseColumnNameAlias().getReference(identifierProcessing)); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java index 55a3e35d..cc773dec 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java @@ -54,7 +54,7 @@ public interface JdbcConverter extends RelationalConverter { * @param * @return */ - T mapRow(RelationalPersistentEntity entity, ResultSet resultSet, Object key); + T mapRow(RelationalPersistentEntity entity, ResultSetWrapper resultSet, Object key); /** * Read the current row from {@link ResultSet} to an {@link PersistentPropertyPathExtension#getActualType() entity}. @@ -66,7 +66,7 @@ public interface JdbcConverter extends RelationalConverter { * @param * @return */ - T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key); + T mapRow(PersistentPropertyPathExtension path, ResultSetWrapper resultSet, Identifier identifier, Object key); /** * The type to be used to store this property in the database. Multidimensional arrays are unwrapped to reflect a diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcPropertyValueProvider.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcPropertyValueProvider.java new file mode 100644 index 00000000..a979becf --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcPropertyValueProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.IdentifierProcessing; + +/** + * {@link PropertyValueProvider} obtaining values from a {@link ResultSetWrapper}. + * + * @author Jens Schauder + * @since 2.0 + */ +class JdbcPropertyValueProvider implements PropertyValueProvider { + + private final IdentifierProcessing identifierProcessing; + private final PersistentPropertyPathExtension basePath; + private final ResultSetWrapper resultSet; + + /** + * @param identifierProcessing used for converting the {@link org.springframework.data.relational.core.sql.SqlIdentifier} from a property to a column label + * @param basePath path from the aggregate root relative to which all properties get resolved. + * @param resultSet the {@link ResultSetWrapper} from which to obtain the actual values. + */ + JdbcPropertyValueProvider(IdentifierProcessing identifierProcessing, PersistentPropertyPathExtension basePath, + ResultSetWrapper resultSet) { + + this.resultSet = resultSet; + this.basePath = basePath; + this.identifierProcessing = identifierProcessing; + } + + @Override + public T getPropertyValue(RelationalPersistentProperty property) { + + String columnName = basePath.extendBy(property).getColumnAlias().getReference(identifierProcessing); + return (T) resultSet.getObject(columnName); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java index 4fa888b6..4309482c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java @@ -23,9 +23,9 @@ import java.util.HashMap; import java.util.Map; import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; -import org.springframework.data.relational.domain.Identifier; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.domain.Identifier; import org.springframework.jdbc.core.RowMapper; import org.springframework.lang.NonNull; @@ -54,6 +54,6 @@ class MapEntityRowMapper implements RowMapper> { } private T mapEntity(ResultSet resultSet, Object key) { - return converter.mapRow(path, resultSet, identifier, key); + return converter.mapRow(path, new ResultSetWrapper(resultSet), identifier, key); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetWrapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetWrapper.java new file mode 100644 index 00000000..7529e724 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ResultSetWrapper.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.convert; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.mapping.MappingException; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedCaseInsensitiveMap; + +/** + * Wraps a {@link java.sql.ResultSet} in order to provide fast lookup of columns by name, including for missing columns. + * + * @author Jens Schauder + */ +class ResultSetWrapper { + + private static final Logger LOG = LoggerFactory.getLogger(ResultSetWrapper.class); + + private final ResultSet resultSet; + + private final Map indexLookUp; + + ResultSetWrapper(ResultSet resultSet) { + + this.resultSet = resultSet; + indexLookUp = indexColumns(); + } + + private Map indexColumns() { + + try { + + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + Map index = new LinkedCaseInsensitiveMap<>(columnCount); + + for (int i = 1; i <= columnCount; i++) { + + String label = metaData.getColumnLabel(i); + if (index.containsKey(label)) { + LOG.warn("We encountered a ResutSet with multiple columns associated with the same label {}.", label); + continue; + } + index.put(label, i); + + } + + return index; + } catch (SQLException se) { + throw new MappingException("Failure while accessing metadata."); + } + + } + + @Nullable + Object getObject(String columnName) { + + try { + + int column = findColumnIndex(columnName); + + return column > 0 ? resultSet.getObject(column) : SpecialColumnValue.NO_SUCH_COLUMN; + } catch (SQLException o_O) { + throw new MappingException(String.format("Could not read value %s from result set!", columnName), o_O); + } + } + + private int findColumnIndex(String columnName) { + return indexLookUp.getOrDefault(columnName, -1); + } + + enum SpecialColumnValue { + NO_SUCH_COLUMN + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java index 47aaa70d..f335fdf3 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java @@ -26,19 +26,24 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.Value; import lombok.With; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.naming.OperationNotSupportedException; +import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; import org.junit.Test; import org.mockito.ArgumentMatchers; @@ -48,6 +53,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; @@ -83,35 +89,6 @@ public class EntityRowMapperUnitTests { } }; - @Test // DATAJDBC-341 - public void mapNotNeededValueTypePropertiesToNull() throws SQLException { - ResultSet rs = mockResultSet(singletonList("id"), // - ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha"); - rs.next(); - Trivial extracted = createRowMapper(Trivial.class).mapRow(rs, 1); - - assertThat(extracted) // - .isNotNull() // - .extracting(e -> e.id, e -> e.name) // - .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); - - } - - @Test // DATAJDBC-341 - public void mapNotNeededPrimitiveTypePropertiesToNull() throws SQLException { - ResultSet rs = mockResultSet(singletonList("id"), // - ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha"); - rs.next(); - TrivialMapPropertiesToNullIfNotNeeded extracted = createRowMapper(TrivialMapPropertiesToNullIfNotNeeded.class) - .mapRow(rs, 1); - - assertThat(extracted) // - .isNotNull() // - .extracting(e -> e.id, e -> e.age) // - .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, 0); - - } - @Test // DATAJDBC-113 public void simpleEntitiesGetProperlyExtracted() throws SQLException { @@ -356,8 +333,8 @@ public class EntityRowMapperUnitTests { @Test // DATAJDBC-370 public void simpleNullableImmutableEmbeddedGetsProperlyExtracted() throws SQLException { - ResultSet rs = mockResultSet(asList("ID", "VALUE"), // - ID_FOR_ENTITY_NOT_REFERENCING_MAP, "ru'Ha'"); + ResultSet rs = mockResultSet(asList("ID", "VALUE", "NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "ru'Ha'", "Alfred"); rs.next(); WithNullableEmbeddedImmutableValue extracted = createRowMapper(WithNullableEmbeddedImmutableValue.class) // @@ -366,14 +343,14 @@ public class EntityRowMapperUnitTests { assertThat(extracted) // .isNotNull() // .extracting(e -> e.id, e -> e.embeddedImmutableValue) // - .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue("ru'Ha'")); + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue("ru'Ha'", "Alfred")); } @Test // DATAJDBC-374 public void simpleEmptyImmutableEmbeddedGetsProperlyExtracted() throws SQLException { - ResultSet rs = mockResultSet(asList("ID", "VALUE"), // - ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + ResultSet rs = mockResultSet(asList("ID", "VALUE", "NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, null, null); rs.next(); WithEmptyEmbeddedImmutableValue extracted = createRowMapper(WithEmptyEmbeddedImmutableValue.class).mapRow(rs, 1); @@ -381,12 +358,11 @@ public class EntityRowMapperUnitTests { assertThat(extracted) // .isNotNull() // .extracting(e -> e.id, e -> e.embeddedImmutableValue) // - .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue(null)); + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue(null, null)); } @Test // DATAJDBC-370 - @SneakyThrows - public void simplePrimitiveImmutableEmbeddedGetsProperlyExtracted() { + public void simplePrimitiveImmutableEmbeddedGetsProperlyExtracted() throws SQLException { ResultSet rs = mockResultSet(asList("ID", "VALUE"), // ID_FOR_ENTITY_NOT_REFERENCING_MAP, 24); @@ -404,8 +380,8 @@ public class EntityRowMapperUnitTests { @Test // DATAJDBC-370 public void simpleImmutableEmbeddedShouldBeNullIfAllOfTheEmbeddableAreNull() throws SQLException { - ResultSet rs = mockResultSet(asList("ID", "VALUE"), // - ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + ResultSet rs = mockResultSet(asList("ID", "VALUE", "NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, null, null); rs.next(); WithNullableEmbeddedImmutableValue extracted = createRowMapper(WithNullableEmbeddedImmutableValue.class) // @@ -418,8 +394,7 @@ public class EntityRowMapperUnitTests { } @Test // DATAJDBC-370 - @SneakyThrows - public void embeddedShouldBeNullWhenFieldsAreNull() { + public void embeddedShouldBeNullWhenFieldsAreNull() throws SQLException { ResultSet rs = mockResultSet(asList("ID", "NAME", "PREFIX_ID", "PREFIX_NAME"), // ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", null, null); @@ -434,8 +409,7 @@ public class EntityRowMapperUnitTests { } @Test // DATAJDBC-370 - @SneakyThrows - public void embeddedShouldNotBeNullWhenAtLeastOneFieldIsNotNull() { + public void embeddedShouldNotBeNullWhenAtLeastOneFieldIsNotNull() throws SQLException { ResultSet rs = mockResultSet(asList("ID", "NAME", "PREFIX_ID", "PREFIX_NAME"), // ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24, null); @@ -450,8 +424,7 @@ public class EntityRowMapperUnitTests { } @Test // DATAJDBC-370 - @SneakyThrows - public void primitiveEmbeddedShouldBeNullWhenNoValuePresent() { + public void primitiveEmbeddedShouldBeNullWhenNoValuePresent() throws SQLException { ResultSet rs = mockResultSet(asList("ID", "VALUE"), // ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); @@ -467,11 +440,10 @@ public class EntityRowMapperUnitTests { } @Test // DATAJDBC-370 - @SneakyThrows - public void deepNestedEmbeddable() { + public void deepNestedEmbeddable() throws SQLException { - ResultSet rs = mockResultSet(asList("ID", "LEVEL0", "LEVEL1_VALUE", "LEVEL1_LEVEL2_VALUE"), // - ID_FOR_ENTITY_NOT_REFERENCING_MAP, "0", "1", "2"); + ResultSet rs = mockResultSet(asList("ID", "LEVEL0", "LEVEL1_VALUE", "LEVEL1_LEVEL2_VALUE", "LEVEL1_LEVEL2_NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "0", "1", "2", "Rumpelstilzchen"); rs.next(); WithDeepNestedEmbeddable extracted = createRowMapper(WithDeepNestedEmbeddable.class).mapRow(rs, 1); @@ -482,6 +454,212 @@ public class EntityRowMapperUnitTests { .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "0", "1", "2"); } + @Test // DATAJDBC-341 + public void missingValueForObjectGetsMappedToZero() throws SQLException { + + ResultSet rs = mockResultSet(singletonList("id"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP); + rs.next(); + Trivial extracted = createRowMapper(Trivial.class).mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.name) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + + } + + @Test // DATAJDBC-341 + public void missingValueForConstructorArgCausesException() throws SQLException { + + ResultSet rs = mockResultSet(singletonList("id"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP); + rs.next(); + + assertThatThrownBy(() -> createRowMapper(TrivialImmutable.class).mapRow(rs, 1)) // + .hasMessageContaining("TrivialImmutable") // + .hasMessageContaining("name"); + + } + + @Test // DATAJDBC-341 + public void missingColumnForPrimitiveGetsMappedToZero() throws SQLException { + + ResultSet rs = mockResultSet(singletonList("id"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP); + rs.next(); + TrivialMapPropertiesToNullIfNotNeeded extracted = createRowMapper(TrivialMapPropertiesToNullIfNotNeeded.class) + .mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.age) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, 0); + + } + + @Test // DATAJDBC-341 + public void columnNamesAreCaseInsensitive() throws SQLException { + + ResultSet rs = mockResultSet(asList("id", "name"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha"); + rs.next(); + + Trivial extracted = createRowMapper(Trivial.class).mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.name) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha"); + } + + @Test // DATAJDBC-341 + public void immutableEmbeddedWithAllColumnsMissingShouldBeNull() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP); + rs.next(); + + WithNullableEmbeddedImmutableValue extracted = createRowMapper(WithNullableEmbeddedImmutableValue.class) // + .mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.embeddedImmutableValue) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + } + + @Test // DATAJDBC-341 + public void immutableEmbeddedWithSomeColumnsMissingShouldThrowAnException() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "VALUE"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "some value"); + rs.next(); + + Assertions.assertThatThrownBy( // + () -> createRowMapper(WithNullableEmbeddedImmutableValue.class).mapRow(rs, 1) // + ).isInstanceOf(MappingException.class) // + .hasMessageContaining("'name'"); + } + + @Test // DATAJDBC-341 + public void immutableEmbeddedWithSomeColumnsMissingAndSomeNullShouldBeNull() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "VALUE"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + rs.next(); + + WithNullableEmbeddedImmutableValue extracted = createRowMapper(WithNullableEmbeddedImmutableValue.class) // + .mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.embeddedImmutableValue) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + } + + @Test // DATAJDBC-341 + public void embeddedShouldBeNullWhenAllFieldsAreMissing() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha"); + rs.next(); + + EmbeddedEntity extracted = createRowMapper(EmbeddedEntity.class).mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.name, e -> e.children) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", null); + } + + @Test // DATAJDBC-341 + public void missingColumnsInEmbeddedShouldBeUnset() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "NAME", "PREFIX_ID"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24); + rs.next(); + + EmbeddedEntity extracted = createRowMapper(EmbeddedEntity.class).mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.name, e -> e.children) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", new Trivial(24L, null)); + } + + @Test // DATAJDBC-341 + public void primitiveEmbeddedShouldBeNullWhenAllColumnsAreMissing() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP); + rs.next(); + + WithEmbeddedPrimitiveImmutableValue extracted = createRowMapper(WithEmbeddedPrimitiveImmutableValue.class) + .mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.embeddedImmutablePrimitiveValue) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null); + } + + @Test // DATAJDBC-341 + public void oneToOneWithMissingColumnResultsInNullProperty() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "NAME", "CHILD_ID"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24L); + rs.next(); + + OneToOne extracted = createRowMapper(OneToOne.class).mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.name, e -> e.child.id, e -> e.child.name) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24L, null); + } + + @Test // DATAJDBC-341 + public void immutableOneToOneWithMissingColumnResultsInException() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "NAME", "CHILD_ID"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24L); + rs.next(); + + Assertions.assertThatThrownBy( // + () -> createRowMapper(OneToOneImmutable.class).mapRow(rs, 1) // + ).isInstanceOf(MappingException.class) // + .hasMessageContaining("'name'"); + } + + @Test // DATAJDBC-341 + public void oneToOneWithMissingIdColumnResultsInNullProperty() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "NAME", "CHILD_NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", "Alfred"); + rs.next(); + + OneToOne extracted = createRowMapper(OneToOne.class).mapRow(rs, 1); + + assertThat(extracted) // + .isNotNull() // + .extracting(e -> e.id, e -> e.name, e -> e.child.id, e -> e.child.name) // + .containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", null, "Alfred"); + } + + @Test // DATAJDBC-341 + public void immutableOneToOneWithIdMissingColumnResultsInNullReference() throws SQLException { + + ResultSet rs = mockResultSet(asList("ID", "NAME", "CHILD_NAME"), // + ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", "Alfred"); + rs.next(); + + Assertions.assertThatThrownBy( // + () -> createRowMapper(OneToOneImmutable.class).mapRow(rs, 1) // + ).isInstanceOf(MappingException.class) // + .hasMessageContaining("'id'"); + } + // Model classes to be used in tests @With @@ -665,6 +843,7 @@ public class EntityRowMapperUnitTests { @Value static class ImmutableValue { Object value; + String name; } @Value @@ -757,7 +936,7 @@ public class EntityRowMapperUnitTests { List> result = convertValues(columns, values); - return mock(ResultSet.class, new ResultSetAnswer(result)); + return mock(ResultSet.class, new ResultSetAnswer(columns, result)); } private static List> convertValues(List columns, Object[] values) { @@ -778,12 +957,18 @@ public class EntityRowMapperUnitTests { return result; } - @RequiredArgsConstructor - private static class ResultSetAnswer implements Answer { + private static class ResultSetAnswer implements Answer { + private List names; private final List> values; private int index = -1; + public ResultSetAnswer(List names, List> values) { + + this.names = names; + this.values = values; + } + @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -791,7 +976,10 @@ public class EntityRowMapperUnitTests { case "next": return next(); case "getObject": - return getObject(invocation.getArgument(0)); + + Object argument = invocation.getArgument(0); + String name = argument instanceof Integer ? names.get(((Integer) argument) - 1) : (String) argument; + return getObject(name); case "isAfterLast": return isAfterLast(); case "isBeforeFirst": @@ -802,12 +990,16 @@ public class EntityRowMapperUnitTests { return this.toString(); case "findColumn": return isThereAColumnNamed(invocation.getArgument(0)); + case "getMetaData": + ResultSetMetaData metaData = new MockedMetaData(); + return metaData; default: throw new OperationNotSupportedException(invocation.getMethod().getName()); } } private int isThereAColumnNamed(String name) { + Optional> first = values.stream().filter(s -> s.equals(name)).findFirst(); return (first.isPresent()) ? 1 : 0; } @@ -836,6 +1028,123 @@ public class EntityRowMapperUnitTests { index++; return index < values.size(); } + + private class MockedMetaData implements ResultSetMetaData { + @Override + public int getColumnCount() throws SQLException { + return values.get(index).size(); + } + + @Override + public boolean isAutoIncrement(int i) throws SQLException { + return false; + } + + @Override + public boolean isCaseSensitive(int i) throws SQLException { + return false; + } + + @Override + public boolean isSearchable(int i) throws SQLException { + return false; + } + + @Override + public boolean isCurrency(int i) throws SQLException { + return false; + } + + @Override + public int isNullable(int i) throws SQLException { + return 0; + } + + @Override + public boolean isSigned(int i) throws SQLException { + return false; + } + + @Override + public int getColumnDisplaySize(int i) throws SQLException { + return 0; + } + + @Override + public String getColumnLabel(int i) throws SQLException { + return names.get(i - 1); + } + + @Override + public String getColumnName(int i) throws SQLException { + return null; + } + + @Override + public String getSchemaName(int i) throws SQLException { + return null; + } + + @Override + public int getPrecision(int i) throws SQLException { + return 0; + } + + @Override + public int getScale(int i) throws SQLException { + return 0; + } + + @Override + public String getTableName(int i) throws SQLException { + return null; + } + + @Override + public String getCatalogName(int i) throws SQLException { + return null; + } + + @Override + public int getColumnType(int i) throws SQLException { + return 0; + } + + @Override + public String getColumnTypeName(int i) throws SQLException { + return null; + } + + @Override + public boolean isReadOnly(int i) throws SQLException { + return false; + } + + @Override + public boolean isWritable(int i) throws SQLException { + return false; + } + + @Override + public boolean isDefinitelyWritable(int i) throws SQLException { + return false; + } + + @Override + public String getColumnClassName(int i) throws SQLException { + return null; + } + + @Override + public T unwrap(Class aClass) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class aClass) throws SQLException { + return false; + } + } } private interface SetValue { 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 b4c984d8..0bb48460 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 @@ -290,6 +290,18 @@ public class JdbcRepositoryIntegrationTests { assertThat(repository.findAllByNamedQuery()).hasSize(1); } + @Test // DATAJDBC-341 + public void findWithMissingQuery() { + + DummyEntity dummy = repository.save(createDummyEntity()); + + DummyEntity loaded = repository.withMissingColumn(dummy.idProp); + + assertThat(loaded.idProp).isEqualTo(dummy.idProp); + assertThat(loaded.name).isNull(); + assertThat(loaded.pointInTime).isNull(); + } + private static DummyEntity createDummyEntity() { DummyEntity entity = new DummyEntity(); @@ -305,6 +317,9 @@ public class JdbcRepositoryIntegrationTests { @Query("SELECT * FROM DUMMY_ENTITY WHERE POINT_IN_TIME > :threshhold") List after(@Param("threshhold")Instant threshhold); + @Query("SELECT id_Prop from dummy_entity where id_Prop = :id") + DummyEntity withMissingColumn(@Param("id")Long id); + } @Data diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index b0e070a9..a411cecc 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -440,6 +440,14 @@ public interface UserRepository extends CrudRepository { ---- ==== +For converting the query result into entities the same `RowMapper` is used by default as for the queries Spring Data JDBC generates itself. +The query you provide must match the format the `RowMapper` expects. +Columns for all properties that are used in the constructor of an entity must be provided. +Columns for properties that get set via setter, wither or field access are optional. +Properties that don't have a matching column will not get set. +The query is used for populating the aggregate root, embedded entities and one to one relationships including arrays of primitive types which get stored and loaded as SQL-array-types. +For maps, lists, sets and arrays of entities separate queries get generated. + NOTE: Spring fully supports Java 8’s parameter name discovery based on the `-parameters` compiler flag. By using this flag in your build as an alternative to debug information, you can omit the `@Param` annotation for named parameters. NOTE: Spring Data JDBC supports only named parameters. diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 21ec6ecb..d54f35ba 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -10,6 +10,7 @@ This section covers the significant changes for each version. * Support for `PagingAndSortingRepository`. * Full Support for H2. * All SQL identifiers know get quoted by default. +* Missing column no longer cause exceptions as long as they are not required by the persitence constructor. [[new-features.1-1-0]] == What's New in Spring Data JDBC 1.1