Skip to content

Commit 925c0e1

Browse files
authored
fix: create a datastore property to skip insertion if the value is null (#3611)
* fix: add a property to convert empty value * change property name * format * add unit test * change docs * add tests * add docs * update action * change to default method * change default to false
1 parent daec34d commit 925c0e1

File tree

10 files changed

+118
-10
lines changed

10 files changed

+118
-10
lines changed

docs/src/main/asciidoc/datastore.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ The following configuration options are available:
7979
| `spring.cloud.gcp.datastore.emulator.consistency` | The https://cloud.google.com/sdk/gcloud/reference/beta/emulators/datastore/start?#--consistency[consistency] to use for the Datastore Emulator instance | No | `0.9`
8080
| `spring.cloud.gcp.datastore.emulator.store-on-disk` | Configures whether or not the emulator should persist any data to disk. | No | `true`
8181
| `spring.cloud.gcp.datastore.emulator.data-dir` | The directory to be used to store/retrieve data/config for an emulator run. | No | The default value is `<USER_CONFIG_DIR>/emulators/datastore`. See the https://cloud.google.com/sdk/gcloud/reference/beta/emulators/datastore/start[gcloud documentation] for finding your `USER_CONFIG_DIR`.
82+
| `spring.cloud.gcp.datastore.skip-null-value` | Whether skip inserting `null` values. | No | The default value is `false`.
83+
If configured to `true`, `null` value will not be inserted into the datastore.
8284

8385
|===
8486

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/GcpDatastoreAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ public ReadWriteConversions datastoreReadWriteConversions(
153153

154154
@Bean
155155
@ConditionalOnMissingBean
156-
public DatastoreMappingContext datastoreMappingContext() {
157-
return new DatastoreMappingContext();
156+
public DatastoreMappingContext datastoreMappingContext(GcpDatastoreProperties gcpDatastoreProperties) {
157+
return new DatastoreMappingContext(gcpDatastoreProperties.isSkipNullValue());
158158
}
159159

160160
@Bean

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/GcpDatastoreProperties.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public class GcpDatastoreProperties implements CredentialsSupplier {
4646

4747
private String namespace;
4848

49+
/** Whether skip the insertion if the value is null */
50+
private boolean skipNullValue;
51+
4952
@Override
5053
public Credentials getCredentials() {
5154
return this.credentials;
@@ -86,4 +89,12 @@ public String getHost() {
8689
public void setHost(String host) {
8790
this.host = host;
8891
}
92+
93+
public boolean isSkipNullValue() {
94+
return skipNullValue;
95+
}
96+
97+
public void setSkipNullValue(boolean skipNullValue) {
98+
this.skipNullValue = skipNullValue;
99+
}
89100
}

spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/datastore/GcpDatastoreAutoConfigurationTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.google.cloud.spring.core.GcpProjectIdProvider;
3333
import com.google.cloud.spring.data.datastore.core.DatastoreOperations;
3434
import com.google.cloud.spring.data.datastore.core.DatastoreTransactionManager;
35+
import com.google.cloud.spring.data.datastore.core.mapping.DatastoreMappingContext;
3536
import java.util.function.Supplier;
3637
import org.junit.jupiter.api.Test;
3738
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -177,6 +178,30 @@ void testDatastoreHealthIndicatorNotCreated() {
177178
.isThrownBy(() -> context.getBean(DatastoreHealthIndicator.class)));
178179
}
179180

181+
@Test
182+
void testSkipNullValueDefaultToFalse() {
183+
this.contextRunner.run(
184+
context -> {
185+
DatastoreMappingContext datastoreMappingContext =
186+
context.getBean(DatastoreMappingContext.class);
187+
assertThat(datastoreMappingContext).isNotNull();
188+
assertThat(datastoreMappingContext.isSkipNullValue()).isFalse();
189+
});
190+
}
191+
192+
@Test
193+
void testSkipNullValueSetToTrue() {
194+
this.contextRunner
195+
.withPropertyValues("spring.cloud.gcp.datastore.skip-null-value=true")
196+
.run(
197+
context -> {
198+
DatastoreMappingContext datastoreMappingContext =
199+
context.getBean(DatastoreMappingContext.class);
200+
assertThat(datastoreMappingContext).isNotNull();
201+
assertThat(datastoreMappingContext.isSkipNullValue()).isTrue();
202+
});
203+
}
204+
180205
private Datastore getDatastoreBean(ApplicationContext context) {
181206
return (Datastore)
182207
((Supplier)

spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/core/convert/DefaultDatastoreEntityConverter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spring.data.datastore.core.convert;
1818

19+
import static com.google.cloud.datastore.ValueType.NULL;
1920
import static com.google.cloud.spring.data.datastore.core.mapping.EmbeddedType.NOT_EMBEDDED;
2021

2122
import com.google.cloud.datastore.BaseEntity;
@@ -243,7 +244,9 @@ public void write(Object source, @NonNull BaseEntity.Builder sink) {
243244
if (persistentProperty.isUnindexed()) {
244245
convertedVal = setExcludeFromIndexes(convertedVal);
245246
}
246-
sink.set(persistentProperty.getFieldName(), convertedVal);
247+
if (!(persistentProperty.isSkipNullValue() && convertedVal.getType().equals(NULL))) {
248+
sink.set(persistentProperty.getFieldName(), convertedVal);
249+
}
247250
} catch (DatastoreDataException ex) {
248251
throw new DatastoreDataException(
249252
"Unable to write "

spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/core/convert/TwoStepsConversions.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,23 +244,23 @@ private <T> T convertOnReadSingle(Object val, TypeInformation<?> targetTypeInfor
244244
}
245245

246246
@Override
247-
public Value convertOnWrite(Object proppertyVal, DatastorePersistentProperty persistentProperty) {
247+
public Value convertOnWrite(Object propertyVal, DatastorePersistentProperty persistentProperty) {
248248
return convertOnWrite(
249-
proppertyVal,
249+
propertyVal,
250250
persistentProperty.getEmbeddedType(),
251251
persistentProperty.getFieldName(),
252252
persistentProperty.getTypeInformation());
253253
}
254254

255255
private Value convertOnWrite(
256-
Object proppertyVal,
256+
Object propertyVal,
257257
EmbeddedType embeddedType,
258258
String fieldName,
259259
TypeInformation typeInformation) {
260-
Object val = proppertyVal;
260+
Object val = propertyVal;
261261

262262
Function<Object, Value> writeConverter = this::convertOnWriteSingle;
263-
if (proppertyVal != null) {
263+
if (propertyVal != null) {
264264
writeConverter = switch (embeddedType) {
265265
case EMBEDDED_MAP -> x -> convertOnWriteSingleEmbeddedMap(x, fieldName,
266266
typeInformation.getMapValueType());

spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/core/mapping/DatastoreMappingContext.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,15 @@ public class DatastoreMappingContext
5252
// Kind and that are subclasses of the given class.
5353
private static final Map<Class, Set<Class>> discriminationFamilies = new ConcurrentHashMap<>();
5454

55+
private final boolean skipNullValue;
56+
5557
public DatastoreMappingContext() {
58+
this(false);
59+
}
60+
61+
public DatastoreMappingContext(boolean skipNullValue) {
5662
this.setSimpleTypeHolder(DatastoreNativeTypes.HOLDER);
63+
this.skipNullValue = skipNullValue;
5764
}
5865

5966
@Override
@@ -118,7 +125,7 @@ protected <T> DatastorePersistentEntity<?> createPersistentEntity(
118125
protected DatastorePersistentProperty createPersistentProperty(
119126
Property property, DatastorePersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
120127
return new DatastorePersistentPropertyImpl(
121-
property, owner, simpleTypeHolder, FIELD_NAMING_STRATEGY);
128+
property, owner, simpleTypeHolder, FIELD_NAMING_STRATEGY, skipNullValue);
122129
}
123130

124131
/**
@@ -139,4 +146,8 @@ public DatastorePersistentEntity<?> getDatastorePersistentEntity(Class<?> entity
139146
"Unable to find a DatastorePersistentEntity for: " + entityClass);
140147
}
141148
}
149+
150+
public boolean isSkipNullValue() {
151+
return skipNullValue;
152+
}
142153
}

spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/core/mapping/DatastorePersistentProperty.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,13 @@ public interface DatastorePersistentProperty
6969
* @return {@code true} if the property is lazily-fetched. {@code false} otherwise.
7070
*/
7171
boolean isLazyLoaded();
72+
73+
/**
74+
* Return whether to skip null value, i.e., skip insertion if value is null.
75+
*
76+
* @return {@code true} if the null value is skipped. {@code false} otherwise.
77+
*/
78+
default boolean isSkipNullValue() {
79+
return false;
80+
}
7281
}

spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/core/mapping/DatastorePersistentPropertyImpl.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class DatastorePersistentPropertyImpl
3838

3939
private final FieldNamingStrategy fieldNamingStrategy;
4040

41+
private final boolean isSkipNullValue;
42+
4143
/**
4244
* Constructor.
4345
*
@@ -50,12 +52,14 @@ public class DatastorePersistentPropertyImpl
5052
Property property,
5153
PersistentEntity<?, DatastorePersistentProperty> owner,
5254
SimpleTypeHolder simpleTypeHolder,
53-
FieldNamingStrategy fieldNamingStrategy) {
55+
FieldNamingStrategy fieldNamingStrategy,
56+
boolean isSkipNullValue) {
5457
super(property, owner, simpleTypeHolder);
5558
this.fieldNamingStrategy =
5659
(fieldNamingStrategy != null)
5760
? fieldNamingStrategy
5861
: PropertyNameFieldNamingStrategy.INSTANCE;
62+
this.isSkipNullValue = isSkipNullValue;
5963
verify();
6064
}
6165

@@ -131,4 +135,9 @@ private String getAnnotatedFieldName() {
131135
public boolean isLazyLoaded() {
132136
return findAnnotation(LazyReference.class) != null;
133137
}
138+
139+
@Override
140+
public boolean isSkipNullValue() {
141+
return isSkipNullValue;
142+
}
134143
}

spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/core/convert/DefaultDatastoreEntityConverterTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.cloud.datastore.BaseEntity;
2424
import com.google.cloud.datastore.Blob;
2525
import com.google.cloud.datastore.Datastore;
26+
import com.google.cloud.datastore.DatastoreException;
2627
import com.google.cloud.datastore.Entity;
2728
import com.google.cloud.datastore.EntityValue;
2829
import com.google.cloud.datastore.FullEntity;
@@ -353,6 +354,43 @@ void writeNullTest() {
353354
assertThat(baseEntity).as("validate null field").isEqualTo(new NullValue());
354355
}
355356

357+
@Test
358+
void testWriteEmptyValueSkipped() {
359+
DatastoreMappingContext context = new DatastoreMappingContext(true);
360+
DatastoreEntityConverter skipEmptyValueConverter =
361+
new DefaultDatastoreEntityConverter(
362+
new DatastoreMappingContext(true),
363+
new TwoStepsConversions(
364+
new DatastoreCustomConversions(
365+
Collections.singletonList(
366+
new Converter<HashMap, String>() {
367+
@Nullable
368+
@Override
369+
public String convert(HashMap source) {
370+
return "Map was converted to String";
371+
}
372+
})),
373+
null,
374+
context));
375+
byte[] bytes = {1, 2, 3};
376+
TestDatastoreItem item = new TestDatastoreItem();
377+
item.setStringField(null);
378+
item.setBoolField(true);
379+
item.setDoubleField(3.1415D);
380+
item.setLongField(123L);
381+
item.setLatLngField(LatLng.of(10, 20));
382+
item.setTimestampField(Timestamp.ofTimeSecondsAndNanos(30, 40));
383+
item.setBlobField(Blob.copyFrom(bytes));
384+
385+
Entity.Builder builder = getEntityBuilder();
386+
skipEmptyValueConverter.write(item, builder);
387+
388+
Entity entity = builder.build();
389+
assertThatThrownBy(() -> entity.getValue("stringField"))
390+
.isInstanceOf(DatastoreException.class)
391+
.hasMessage("No such property stringField");
392+
}
393+
356394
@Test
357395
void testUnsupportedTypeWriteException() {
358396

0 commit comments

Comments
 (0)