Skip to content

Commit 9ce9694

Browse files
committed
Fix regression of loading empty arrays.
Closes #1826 See #1812
1 parent 448b6fd commit 9ce9694

File tree

3 files changed

+107
-12
lines changed

3 files changed

+107
-12
lines changed

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

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
import org.apache.commons.logging.Log;
2828
import org.apache.commons.logging.LogFactory;
29-
3029
import org.springframework.context.ApplicationContextAware;
3130
import org.springframework.core.convert.ConverterNotFoundException;
3231
import org.springframework.core.convert.converter.Converter;
@@ -81,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
8180
* {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)}
8281
* (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types.
8382
*
84-
* @param context must not be {@literal null}.
83+
* @param context must not be {@literal null}.
8584
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
8685
*/
8786
public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) {
@@ -99,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r
9998
/**
10099
* Creates a new {@link MappingJdbcConverter} given {@link MappingContext}.
101100
*
102-
* @param context must not be {@literal null}.
101+
* @param context must not be {@literal null}.
103102
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
104-
* @param typeFactory must not be {@literal null}
103+
* @param typeFactory must not be {@literal null}
105104
*/
106105
public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
107-
CustomConversions conversions, JdbcTypeFactory typeFactory) {
106+
CustomConversions conversions, JdbcTypeFactory typeFactory) {
108107

109108
super(context, conversions);
110109

@@ -330,7 +329,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu
330329
private final Identifier identifier;
331330

332331
private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor,
333-
ResolvingConversionContext context, Identifier identifier) {
332+
ResolvingConversionContext context, Identifier identifier) {
334333

335334
AggregatePath path = context.aggregatePath();
336335

@@ -339,15 +338,15 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele
339338
this.context = context;
340339
this.identifier = path.isEntity()
341340
? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(),
342-
property -> delegate.getValue(path.append(property)))
341+
property -> delegate.getValue(path.append(property)))
343342
: identifier;
344343
}
345344

346345
/**
347346
* Conditionally append the identifier if the entity has an identifier property.
348347
*/
349348
static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity<?> entity,
350-
Function<RelationalPersistentProperty, Object> getter) {
349+
Function<RelationalPersistentProperty, Object> getter) {
351350

352351
if (entity.hasIdProperty()) {
353352

@@ -422,7 +421,7 @@ public <T> T getPropertyValue(RelationalPersistentProperty property) {
422421
@Override
423422
public boolean hasValue(RelationalPersistentProperty property) {
424423

425-
if ((property.isCollectionLike() && property.isEntity())|| property.isMap()) {
424+
if ((property.isCollectionLike() && property.isEntity()) || property.isMap()) {
426425
// attempt relation fetch
427426
return true;
428427
}
@@ -445,12 +444,38 @@ public boolean hasValue(RelationalPersistentProperty property) {
445444
return delegate.hasValue(aggregatePath);
446445
}
447446

447+
@Override
448+
public boolean hasNonEmptyValue(RelationalPersistentProperty property) {
449+
450+
if ((property.isCollectionLike() && property.isEntity()) || property.isMap()) {
451+
// attempt relation fetch
452+
return true;
453+
}
454+
455+
AggregatePath aggregatePath = context.aggregatePath();
456+
457+
if (property.isEntity()) {
458+
459+
RelationalPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(property);
460+
if (entity.hasIdProperty()) {
461+
462+
RelationalPersistentProperty referenceId = entity.getRequiredIdProperty();
463+
AggregatePath toUse = aggregatePath.append(referenceId);
464+
return delegate.hasValue(toUse);
465+
}
466+
467+
return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias());
468+
}
469+
470+
return delegate.hasNonEmptyValue(aggregatePath);
471+
}
472+
448473
@Override
449474
public RelationalPropertyValueProvider withContext(ConversionContext context) {
450475

451476
return context == this.context ? this
452477
: new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor,
453-
(ResolvingConversionContext) context, identifier);
478+
(ResolvingConversionContext) context, identifier);
454479
}
455480
}
456481

@@ -462,7 +487,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
462487
* @param identifier
463488
*/
464489
private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath,
465-
Identifier identifier) implements ConversionContext {
490+
Identifier identifier) implements ConversionContext {
466491

467492
@Override
468493
public <S> S convert(Object source, TypeInformation<? extends S> typeHint) {

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,26 @@ void saveAndLoadAnEntityWithArray() {
681681
assertThat(reloaded.digits).isEqualTo(new String[] { "one", "two", "three" });
682682
}
683683

684+
@Test // GH-1826
685+
@EnabledOnFeature(SUPPORTS_ARRAYS)
686+
void saveAndLoadAnEntityWithEmptyArray() {
687+
688+
ArrayOwner arrayOwner = new ArrayOwner();
689+
arrayOwner.digits = new String[] { };
690+
691+
ArrayOwner saved = template.save(arrayOwner);
692+
693+
assertThat(saved.id).isNotNull();
694+
695+
ArrayOwner reloaded = template.findById(saved.id, ArrayOwner.class);
696+
697+
assertThat(reloaded).isNotNull();
698+
assertThat(reloaded.id).isEqualTo(saved.id);
699+
assertThat(reloaded.digits) //
700+
.isNotNull() //
701+
.isEmpty();
702+
}
703+
684704
@Test // DATAJDBC-259, DATAJDBC-512
685705
@EnabledOnFeature(SUPPORTS_MULTIDIMENSIONAL_ARRAYS)
686706
void saveAndLoadAnEntityWithMultidimensionalArray() {
@@ -718,6 +738,23 @@ void saveAndLoadAnEntityWithList() {
718738
assertThat(reloaded.digits).isEqualTo(asList("one", "two", "three"));
719739
}
720740

741+
@Test // DATAJDBC-1826
742+
@EnabledOnFeature(SUPPORTS_ARRAYS)
743+
void saveAndLoadAnEntityWithEmptyList() {
744+
745+
ListOwner arrayOwner = new ListOwner();
746+
747+
ListOwner saved = template.save(arrayOwner);
748+
749+
assertThat(saved.id).isNotNull();
750+
751+
ListOwner reloaded = template.findById(saved.id, ListOwner.class);
752+
753+
assertThat(reloaded).isNotNull();
754+
assertThat(reloaded.id).isEqualTo(saved.id);
755+
assertThat(reloaded.digits).isNotNull().isEmpty();
756+
}
757+
721758
@Test // GH-1033
722759
@EnabledOnFeature(SUPPORTS_ARRAYS)
723760
void saveAndLoadAnEntityWithListOfDouble() {

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,11 @@ public boolean hasValue(RelationalPersistentProperty property) {
466466
return withContext(context.forProperty(property)).hasValue(property);
467467
}
468468

469+
@Override
470+
public boolean hasNonEmptyValue(RelationalPersistentProperty property) {
471+
return withContext(context.forProperty(property)).hasNonEmptyValue(property);
472+
}
473+
469474
@SuppressWarnings("unchecked")
470475
@Nullable
471476
@Override
@@ -541,6 +546,7 @@ private void readProperties(ConversionContext context, RelationalPersistentEntit
541546
continue;
542547
}
543548

549+
// this hasValue should actually check against null
544550
if (!valueProviderToUse.hasValue(property)) {
545551
continue;
546552
}
@@ -584,7 +590,7 @@ private boolean shouldReadEmbeddable(ConversionContext context, RelationalPersis
584590
return true;
585591
}
586592

587-
} else if (contextual.hasValue(persistentProperty)) {
593+
} else if (contextual.hasNonEmptyValue(persistentProperty)) {
588594
return true;
589595
}
590596
}
@@ -1027,6 +1033,13 @@ protected interface RelationalPropertyValueProvider extends PropertyValueProvide
10271033
*/
10281034
boolean hasValue(RelationalPersistentProperty property);
10291035

1036+
/**
1037+
* Determine whether there is a non empty value for the given {@link RelationalPersistentProperty}.
1038+
*
1039+
* @param property the property to check for whether a value is present.
1040+
*/
1041+
boolean hasNonEmptyValue(RelationalPersistentProperty property);
1042+
10301043
/**
10311044
* Contextualize this property value provider.
10321045
*
@@ -1048,6 +1061,8 @@ protected interface AggregatePathValueProvider extends RelationalPropertyValuePr
10481061
*/
10491062
boolean hasValue(AggregatePath path);
10501063

1064+
boolean hasNonEmptyValue(AggregatePath aggregatePath);
1065+
10511066
/**
10521067
* Determine whether there is a value for the given {@link SqlIdentifier}.
10531068
*
@@ -1129,6 +1144,11 @@ public boolean hasValue(RelationalPersistentProperty property) {
11291144
return accessor.hasValue(property);
11301145
}
11311146

1147+
@Override
1148+
public boolean hasNonEmptyValue(RelationalPersistentProperty property) {
1149+
return hasValue(property);
1150+
}
1151+
11321152
@Nullable
11331153
@Override
11341154
public Object getValue(AggregatePath path) {
@@ -1144,6 +1164,7 @@ public Object getValue(AggregatePath path) {
11441164

11451165
@Override
11461166
public boolean hasValue(AggregatePath path) {
1167+
11471168
Object value = document.get(path.getColumnInfo().alias().getReference());
11481169

11491170
if (value == null) {
@@ -1154,6 +1175,18 @@ public boolean hasValue(AggregatePath path) {
11541175
return true;
11551176
}
11561177

1178+
return true;
1179+
}
1180+
1181+
@Override
1182+
public boolean hasNonEmptyValue(AggregatePath path) {
1183+
1184+
if (!hasValue(path)) {
1185+
return false;
1186+
}
1187+
1188+
Object value = document.get(path.getColumnInfo().alias().getReference());
1189+
11571190
if (value instanceof Collection<?> || value.getClass().isArray()) {
11581191
return !ObjectUtils.isEmpty(value);
11591192
}

0 commit comments

Comments
 (0)