Skip to content

Commit 3a89ec0

Browse files
DATAJDBC-370 - Fix read/write of nulled embedded objects.
We now allow read and write of Objects annotated with Embedded that are actually null. When writing all contained fields will be nulled. Reading back the entity considers an embedded object to be null itself if all contained properties are null within the backing result. Relates to DATAJDBC-364 Original Pull Request: #151
1 parent 7f50c92 commit 3a89ec0

File tree

4 files changed

+205
-13
lines changed

4 files changed

+205
-13
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
*
5353
* @author Mark Paluch
5454
* @author Jens Schauder
55+
* @author Christoph Strobl
5556
* @see MappingContext
5657
* @see SimpleTypeHolder
5758
* @see CustomConversions
@@ -345,15 +346,40 @@ private Object readFrom(RelationalPersistentProperty property) {
345346

346347
Object value = getObjectFromResultSet(path.extendBy(property).getColumnAlias());
347348
return readValue(value, property.getTypeInformation());
348-
349349
}
350350

351-
@SuppressWarnings("unchecked")
351+
@Nullable
352352
private Object readEmbeddedEntityFrom(@Nullable Object idValue, RelationalPersistentProperty property) {
353353

354-
ReadingContext newContext = extendBy(property);
354+
ReadingContext<?> ctx = extendBy(property);
355+
return hasInstanceValues(property, ctx) ? ctx.createInstanceInternal(idValue) : null;
356+
}
355357

356-
return newContext.createInstanceInternal(idValue);
358+
private boolean hasInstanceValues(RelationalPersistentProperty property, ReadingContext<?> ctx) {
359+
360+
RelationalPersistentEntity<?> persistentEntity = getMappingContext()
361+
.getPersistentEntity(property.getTypeInformation());
362+
363+
PersistentPropertyPathExtension extension = ctx.path;
364+
365+
for (RelationalPersistentProperty embeddedProperty : persistentEntity) {
366+
367+
if (embeddedProperty.isQualified() || embeddedProperty.isReference()) {
368+
return true;
369+
}
370+
371+
try {
372+
if (ctx.getObjectFromResultSet(extension.extendBy(embeddedProperty).getColumnName()) != null) {
373+
return true;
374+
}
375+
} catch (MappingException e) {
376+
if (ctx.getObjectFromResultSet(extension.extendBy(embeddedProperty).getReverseColumnNameAlias()) != null) {
377+
return true;
378+
}
379+
}
380+
}
381+
382+
return false;
357383
}
358384

359385
@Nullable

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
* @author Mark Paluch
5555
* @author Thomas Lang
5656
* @author Bastian Wilhelm
57+
* @author Christoph Strobl
5758
* @since 1.1
5859
*/
5960
public class DefaultDataAccessStrategy implements DataAccessStrategy {
@@ -313,7 +314,8 @@ private <S, T> MapSqlParameterSource getParameterSource(S instance, RelationalPe
313314

314315
MapSqlParameterSource parameters = new MapSqlParameterSource();
315316

316-
PersistentPropertyAccessor<S> propertyAccessor = persistentEntity.getPropertyAccessor(instance);
317+
PersistentPropertyAccessor<S> propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance)
318+
: NoValuePropertyAccessor.instance();
317319

318320
persistentEntity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> {
319321

@@ -480,4 +482,33 @@ static Predicate<RelationalPersistentProperty> includeAll() {
480482
return it -> false;
481483
}
482484
}
485+
486+
/**
487+
* A {@link PersistentPropertyAccessor} implementation always returning null
488+
*
489+
* @param <T>
490+
*/
491+
static class NoValuePropertyAccessor<T> implements PersistentPropertyAccessor<T> {
492+
493+
private static final NoValuePropertyAccessor INSTANCE = new NoValuePropertyAccessor();
494+
495+
static <T> NoValuePropertyAccessor<T> instance() {
496+
return INSTANCE;
497+
}
498+
499+
@Override
500+
public void setProperty(PersistentProperty<?> property, Object value) {
501+
throw new UnsupportedOperationException("Cannot set value on 'null' target object.");
502+
}
503+
504+
@Override
505+
public Object getProperty(PersistentProperty<?> property) {
506+
return null;
507+
}
508+
509+
@Override
510+
public T getBean() {
511+
return null;
512+
}
513+
}
483514
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/EntityRowMapperUnitTests.java

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
import static java.util.Arrays.*;
1919
import static java.util.Collections.*;
2020
import static org.assertj.core.api.Assertions.*;
21-
import static org.mockito.ArgumentMatchers.*;
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.ArgumentMatchers.eq;
2223
import static org.mockito.Mockito.*;
2324

2425
import lombok.AllArgsConstructor;
26+
import lombok.EqualsAndHashCode;
27+
import lombok.NoArgsConstructor;
2528
import lombok.RequiredArgsConstructor;
29+
import lombok.SneakyThrows;
2630
import lombok.Value;
2731
import lombok.experimental.Wither;
2832

@@ -301,6 +305,101 @@ public void simpleImmutableEmbeddedGetsProperlyExtracted() throws SQLException {
301305
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue("ru'Ha'"));
302306
}
303307

308+
@Test // DATAJDBC-370
309+
@SneakyThrows
310+
public void simplePrimitiveImmutableEmbeddedGetsProperlyExtracted() {
311+
312+
ResultSet rs = mockResultSet(asList("id", "value"), //
313+
ID_FOR_ENTITY_NOT_REFERENCING_MAP, 24);
314+
rs.next();
315+
316+
WithPrimitiveImmutableValue extracted = createRowMapper(WithPrimitiveImmutableValue.class).mapRow(rs, 1);
317+
318+
assertThat(extracted) //
319+
.isNotNull() //
320+
.extracting(e -> e.id, e -> e.embeddedImmutablePrimitiveValue) //
321+
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutablePrimitiveValue(24));
322+
}
323+
324+
@Test // DATAJDBC-370
325+
public void simpleImmutableEmbeddedShouldBeNullIfAllOfTheEmbeddableAreNull() throws SQLException {
326+
327+
ResultSet rs = mockResultSet(asList("id", "value"), //
328+
ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
329+
rs.next();
330+
331+
WithImmutableValue extracted = createRowMapper(WithImmutableValue.class).mapRow(rs, 1);
332+
333+
assertThat(extracted) //
334+
.isNotNull() //
335+
.extracting(e -> e.id, e -> e.embeddedImmutableValue) //
336+
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
337+
}
338+
339+
@Test // DATAJDBC-370
340+
@SneakyThrows
341+
public void embeddedShouldBeNullWhenFieldsAreNull() {
342+
343+
ResultSet rs = mockResultSet(asList("id", "name", "prefix_id", "prefix_name"), //
344+
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", null, null);
345+
rs.next();
346+
347+
EmbeddedEntity extracted = createRowMapper(EmbeddedEntity.class).mapRow(rs, 1);
348+
349+
assertThat(extracted) //
350+
.isNotNull() //
351+
.extracting(e -> e.id, e -> e.name, e -> e.children) //
352+
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", null);
353+
}
354+
355+
@Test // DATAJDBC-370
356+
@SneakyThrows
357+
public void embeddedShouldNotBeNullWhenAtLeastOneFieldIsNotNull() {
358+
359+
ResultSet rs = mockResultSet(asList("id", "name", "prefix_id", "prefix_name"), //
360+
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24, null);
361+
rs.next();
362+
363+
EmbeddedEntity extracted = createRowMapper(EmbeddedEntity.class).mapRow(rs, 1);
364+
365+
assertThat(extracted) //
366+
.isNotNull() //
367+
.extracting(e -> e.id, e -> e.name, e -> e.children) //
368+
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", new Trivial(24L, null));
369+
}
370+
371+
@Test // DATAJDBC-370
372+
@SneakyThrows
373+
public void primitiveEmbeddedShouldBeNullWhenNoValuePresent() {
374+
375+
ResultSet rs = mockResultSet(asList("id", "value"), //
376+
ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
377+
rs.next();
378+
379+
WithPrimitiveImmutableValue extracted = createRowMapper(WithPrimitiveImmutableValue.class).mapRow(rs, 1);
380+
381+
assertThat(extracted) //
382+
.isNotNull() //
383+
.extracting(e -> e.id, e -> e.embeddedImmutablePrimitiveValue) //
384+
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
385+
}
386+
387+
@Test // DATAJDBC-370
388+
@SneakyThrows
389+
public void deepNestedEmbeddable() {
390+
391+
ResultSet rs = mockResultSet(asList("id", "level0", "level1_value", "level1_level2_value"), //
392+
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "0", "1", "2");
393+
rs.next();
394+
395+
WithDeepNestedEmbeddable extracted = createRowMapper(WithDeepNestedEmbeddable.class).mapRow(rs, 1);
396+
397+
assertThat(extracted) //
398+
.isNotNull() //
399+
.extracting(e -> e.id, e -> extracted.level0, e -> e.level1.value, e -> e.level1.level2.value) //
400+
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "0", "1", "2");
401+
}
402+
304403
// Model classes to be used in tests
305404

306405
@Wither
@@ -311,6 +410,9 @@ static class TrivialImmutable {
311410
private final String name;
312411
}
313412

413+
@EqualsAndHashCode
414+
@NoArgsConstructor
415+
@AllArgsConstructor
314416
static class Trivial {
315417

316418
@Id Long id;
@@ -432,11 +534,35 @@ static class WithImmutableValue {
432534
@Embedded ImmutableValue embeddedImmutableValue;
433535
}
434536

537+
static class WithPrimitiveImmutableValue {
538+
539+
@Id Long id;
540+
@Embedded ImmutablePrimitiveValue embeddedImmutablePrimitiveValue;
541+
}
542+
435543
@Value
436544
static class ImmutableValue {
437545
Object value;
438546
}
439547

548+
@Value
549+
static class ImmutablePrimitiveValue {
550+
int value;
551+
}
552+
553+
static class WithDeepNestedEmbeddable {
554+
555+
@Id Long id;
556+
String level0;
557+
@Embedded("level1_") EmbeddedWithEmbedded level1;
558+
}
559+
560+
static class EmbeddedWithEmbedded {
561+
562+
Object value;
563+
@Embedded("level2_") ImmutableValue level2;
564+
}
565+
440566
// Infrastructure for assertions and constructing mocks
441567

442568
private <T> FixtureBuilder<T> buildFixture() {
@@ -454,22 +580,23 @@ private <T> EntityRowMapper<T> createRowMapper(Class<T> type, NamingStrategy nam
454580
DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class);
455581

456582
// the ID of the entity is used to determine what kind of ResultSet is needed for subsequent selects.
457-
doReturn(new HashSet<>(asList(new Trivial(), new Trivial()))).when(accessStrategy)
583+
doReturn(new HashSet<>(asList(new Trivial(1L, "one"), new Trivial(2L, "two")))).when(accessStrategy)
458584
.findAllByProperty(eq(ID_FOR_ENTITY_NOT_REFERENCING_MAP), any(RelationalPersistentProperty.class));
459585

460586
doReturn(new HashSet<>(asList( //
461-
new SimpleEntry<>("one", new Trivial()), //
462-
new SimpleEntry<>("two", new Trivial()) //
587+
new SimpleEntry<>("one", new Trivial(1L, "one")), //
588+
new SimpleEntry<>("two", new Trivial(2L, "two")) //
463589
))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_MAP),
464590
any(RelationalPersistentProperty.class));
465591

466592
doReturn(new HashSet<>(asList( //
467-
new SimpleEntry<>(1, new Trivial()), //
468-
new SimpleEntry<>(2, new Trivial()) //
593+
new SimpleEntry<>(1, new Trivial(1L, "one")), //
594+
new SimpleEntry<>(2, new Trivial(2L, "tow")) //
469595
))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_LIST),
470596
any(RelationalPersistentProperty.class));
471597

472-
JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions());
598+
JdbcConverter converter = new BasicJdbcConverter(context, new JdbcCustomConversions(),
599+
JdbcTypeFactory.unsupported());
473600

474601
return new EntityRowMapper<>( //
475602
(RelationalPersistentEntity<T>) context.getRequiredPersistentEntity(type), //

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* Very simple use cases for creation and usage of JdbcRepositories with test {@link Embedded} annotation in Entities.
4545
*
4646
* @author Bastian Wilhelm
47+
* @author Christoph Strobl
4748
*/
4849
@ContextConfiguration
4950
@Transactional
@@ -209,6 +210,14 @@ public void deleteAll() {
209210
assertThat(repository.findAll()).isEmpty();
210211
}
211212

213+
@Test // DATAJDBC-370
214+
public void saveWithNullValueEmbeddable() {
215+
216+
DummyEntity entity = repository.save(new DummyEntity());
217+
218+
assertThat(JdbcTestUtils.countRowsInTableWhere((JdbcTemplate) template.getJdbcOperations(), "dummy_entity",
219+
"id = " + entity.getId())).isEqualTo(1);
220+
}
212221

213222
private static DummyEntity createDummyEntity() {
214223
DummyEntity entity = new DummyEntity();
@@ -222,7 +231,6 @@ private static DummyEntity createDummyEntity() {
222231

223232
entity.setPrefixedEmbeddable(prefixedCascadedEmbeddable);
224233

225-
226234
final CascadedEmbeddable cascadedEmbeddable = new CascadedEmbeddable();
227235
cascadedEmbeddable.setTest("c2");
228236

0 commit comments

Comments
 (0)