DATAJDBC-341 - Reworked the changes.

Turns out this was a little more involved than expected.
This modifies the implementation to not use exceptions for flow control.

Properties that get set via setter, wither or field access get not invoked for missing columns.
When properties get referenced in constructors a missing column results in an exception.

As a side effect we now access the `ResultSet` by index.
Depending on how the driver is implemented this might improve the performance a little.

Original pull request: #201.
This commit is contained in:
Jens Schauder
2020-03-17 13:53:09 +01:00
committed by Mark Paluch
parent c2062dbb9d
commit 95ca73df2f
11 changed files with 644 additions and 120 deletions

View File

@@ -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> T mapRow(RelationalPersistentEntity<T> entity, ResultSet resultSet, Object key) {
public <T> T mapRow(RelationalPersistentEntity<T> entity, ResultSetWrapper resultSet, Object key) {
return new ReadingContext<T>(
new PersistentPropertyPathExtension(
getMappingContext(), entity),
@@ -330,7 +330,8 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
}
@Override
public <T> T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key) {
public <T> T mapRow(PersistentPropertyPathExtension path, ResultSetWrapper resultSet, Identifier identifier,
Object key) {
return new ReadingContext<T>(path, resultSet, identifier, key).mapRow();
}
@@ -338,14 +339,17 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
private final RelationalPersistentEntity<T> 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<RelationalPersistentProperty> propertyValueProvider;
private final PropertyValueProvider<RelationalPersistentProperty> backReferencePropertyValueProvider;
@SuppressWarnings("unchecked")
private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSet resultSet, Identifier identifier,
private ReadingContext(PersistentPropertyPathExtension rootPath, ResultSetWrapper resultSet, Identifier identifier,
Object key) {
RelationalPersistentEntity<T> entity = (RelationalPersistentEntity<T>) 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<T> entity, ResultSet resultSet,
private ReadingContext(RelationalPersistentEntity<T> 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 <S> ReadingContext<S> 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);
}

View File

@@ -64,8 +64,11 @@ public class EntityRowMapper<T> implements RowMapper<T> {
@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);
}
}

View File

@@ -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<RelationalPersistentProperty> {
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> T getPropertyValue(RelationalPersistentProperty property) {
return (T)resultSet.getObject(basePath.extendBy(property).getReverseColumnNameAlias().getReference(identifierProcessing));
}
}

View File

@@ -54,7 +54,7 @@ public interface JdbcConverter extends RelationalConverter {
* @param <T>
* @return
*/
<T> T mapRow(RelationalPersistentEntity<T> entity, ResultSet resultSet, Object key);
<T> T mapRow(RelationalPersistentEntity<T> 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 <T>
* @return
*/
<T> T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key);
<T> 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

View File

@@ -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<RelationalPersistentProperty> {
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> T getPropertyValue(RelationalPersistentProperty property) {
String columnName = basePath.extendBy(property).getColumnAlias().getReference(identifierProcessing);
return (T) resultSet.getObject(columnName);
}
}

View File

@@ -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<T> implements RowMapper<Map.Entry<Object, T>> {
}
private T mapEntity(ResultSet resultSet, Object key) {
return converter.mapRow(path, resultSet, identifier, key);
return converter.mapRow(path, new ResultSetWrapper(resultSet), identifier, key);
}
}

View File

@@ -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<String, Integer> indexLookUp;
ResultSetWrapper(ResultSet resultSet) {
this.resultSet = resultSet;
indexLookUp = indexColumns();
}
private Map<String, Integer> indexColumns() {
try {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
Map<String, Integer> 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
}
}

View File

@@ -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<Map<String, Object>> result = convertValues(columns, values);
return mock(ResultSet.class, new ResultSetAnswer(result));
return mock(ResultSet.class, new ResultSetAnswer(columns, result));
}
private static List<Map<String, Object>> convertValues(List<String> 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<Object> {
private List<String> names;
private final List<Map<String, Object>> values;
private int index = -1;
public ResultSetAnswer(List<String> names, List<Map<String, Object>> 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<Map<String, Object>> 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> T unwrap(Class<T> aClass) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> aClass) throws SQLException {
return false;
}
}
}
private interface SetValue<T> {

View File

@@ -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<DummyEntity> after(@Param("threshhold")Instant threshhold);
@Query("SELECT id_Prop from dummy_entity where id_Prop = :id")
DummyEntity withMissingColumn(@Param("id")Long id);
}
@Data

View File

@@ -440,6 +440,14 @@ public interface UserRepository extends CrudRepository<User, Long> {
----
====
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 8s 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.

View File

@@ -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