DATAJDBC-374 - Add onEmpty attribute to Embedded annotation.

The onEmpty attribute allows to define if an embedded entity should be set to null or a default instance if all properties backing the entity are actually null.

    @Embedded(onEmpty = USE_NULL)
    EmbeddedEntity embeddedEntity;

Original pull request: #154.
This commit is contained in:
Christoph Strobl
2019-05-21 15:20:59 +02:00
committed by Jens Schauder
parent f69aa31958
commit 5830b83cf4
14 changed files with 104 additions and 37 deletions

View File

@@ -37,6 +37,8 @@ import org.springframework.data.mapping.context.MappingContext;
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;
@@ -375,6 +377,11 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
private Object readEmbeddedEntityFrom(@Nullable Object idValue, RelationalPersistentProperty property) {
ReadingContext<?> newContext = extendBy(property);
if(OnEmpty.USE_EMPTY.equals(property.findAnnotation(Embedded.class).onEmpty())) {
return newContext.createInstanceInternal(idValue);
}
return newContext.hasInstanceValues(idValue) ? newContext.createInstanceInternal(idValue) : null;
}

View File

@@ -23,6 +23,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.mapping.PersistentPropertyPath;
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;
@@ -217,7 +218,7 @@ public class PersistentPropertyPathExtensionUnitTests {
static class DummyEntity {
@Id Long entityId;
Second second;
@Embedded("sec") Second second2;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "sec") Second second2;
List<Second> secondList;
WithId withId;
}
@@ -225,7 +226,7 @@ public class PersistentPropertyPathExtensionUnitTests {
@SuppressWarnings("unused")
static class Second {
Third third;
@Embedded("thrd") Third third2;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "thrd") Third third2;
}
@SuppressWarnings("unused")
@@ -237,7 +238,7 @@ public class PersistentPropertyPathExtensionUnitTests {
static class WithId {
@Id Long withIdId;
Second second;
@Embedded("sec") Second second2;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "sec") Second second2;
}
}

View File

@@ -54,6 +54,7 @@ import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.relational.core.mapping.NamingStrategy;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -295,13 +296,13 @@ public class EntityRowMapperUnitTests {
}
@Test // DATAJDBC-370
public void simpleImmutableEmbeddedGetsProperlyExtracted() throws SQLException {
public void simpleNullableImmutableEmbeddedGetsProperlyExtracted() throws SQLException {
ResultSet rs = mockResultSet(asList("id", "value"), //
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "ru'Ha'");
rs.next();
WithImmutableValue extracted = createRowMapper(WithImmutableValue.class).mapRow(rs, 1);
WithNullableEmbeddedImmutableValue extracted = createRowMapper(WithNullableEmbeddedImmutableValue.class).mapRow(rs, 1);
assertThat(extracted) //
.isNotNull() //
@@ -309,6 +310,21 @@ public class EntityRowMapperUnitTests {
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue("ru'Ha'"));
}
@Test // DATAJDBC-374
public void simpleEmptyImmutableEmbeddedGetsProperlyExtracted() throws SQLException {
ResultSet rs = mockResultSet(asList("id", "value"), //
ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
rs.next();
WithEmptyEmbeddedImmutableValue extracted = createRowMapper(WithEmptyEmbeddedImmutableValue.class).mapRow(rs, 1);
assertThat(extracted) //
.isNotNull() //
.extracting(e -> e.id, e -> e.embeddedImmutableValue) //
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, new ImmutableValue(null));
}
@Test // DATAJDBC-370
@SneakyThrows
public void simplePrimitiveImmutableEmbeddedGetsProperlyExtracted() {
@@ -317,7 +333,7 @@ public class EntityRowMapperUnitTests {
ID_FOR_ENTITY_NOT_REFERENCING_MAP, 24);
rs.next();
WithPrimitiveImmutableValue extracted = createRowMapper(WithPrimitiveImmutableValue.class).mapRow(rs, 1);
WithEmbeddedPrimitiveImmutableValue extracted = createRowMapper(WithEmbeddedPrimitiveImmutableValue.class).mapRow(rs, 1);
assertThat(extracted) //
.isNotNull() //
@@ -332,7 +348,7 @@ public class EntityRowMapperUnitTests {
ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
rs.next();
WithImmutableValue extracted = createRowMapper(WithImmutableValue.class).mapRow(rs, 1);
WithNullableEmbeddedImmutableValue extracted = createRowMapper(WithNullableEmbeddedImmutableValue.class).mapRow(rs, 1);
assertThat(extracted) //
.isNotNull() //
@@ -380,7 +396,7 @@ public class EntityRowMapperUnitTests {
ID_FOR_ENTITY_NOT_REFERENCING_MAP, null);
rs.next();
WithPrimitiveImmutableValue extracted = createRowMapper(WithPrimitiveImmutableValue.class).mapRow(rs, 1);
WithEmbeddedPrimitiveImmutableValue extracted = createRowMapper(WithEmbeddedPrimitiveImmutableValue.class).mapRow(rs, 1);
assertThat(extracted) //
.isNotNull() //
@@ -465,7 +481,7 @@ public class EntityRowMapperUnitTests {
@Id Long id;
String name;
@Embedded("prefix_") Trivial children;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_") Trivial children;
}
private static class DontUseSetter {
@@ -533,16 +549,22 @@ public class EntityRowMapperUnitTests {
NoIdChain3 chain3;
}
static class WithImmutableValue {
static class WithNullableEmbeddedImmutableValue {
@Id Long id;
@Embedded ImmutableValue embeddedImmutableValue;
@Embedded(onEmpty = OnEmpty.USE_NULL) ImmutableValue embeddedImmutableValue;
}
static class WithPrimitiveImmutableValue {
static class WithEmptyEmbeddedImmutableValue {
@Id Long id;
@Embedded ImmutablePrimitiveValue embeddedImmutablePrimitiveValue;
@Embedded(onEmpty = OnEmpty.USE_EMPTY) ImmutableValue embeddedImmutableValue;
}
static class WithEmbeddedPrimitiveImmutableValue {
@Id Long id;
@Embedded(onEmpty = OnEmpty.USE_NULL) ImmutablePrimitiveValue embeddedImmutablePrimitiveValue;
}
@Value
@@ -559,13 +581,13 @@ public class EntityRowMapperUnitTests {
@Id Long id;
String level0;
@Embedded("level1_") EmbeddedWithEmbedded level1;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "level1_") EmbeddedWithEmbedded level1;
}
static class EmbeddedWithEmbedded {
Object value;
@Embedded("level2_") ImmutableValue level2;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "level2_") ImmutableValue level2;
}
// Infrastructure for assertions and constructing mocks

View File

@@ -27,6 +27,7 @@ import org.springframework.data.jdbc.core.PropertyPathTestingUtils;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.relational.core.mapping.Column;
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.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -245,16 +246,16 @@ public class SqlGeneratorEmbeddedUnitTests {
@Column("id1") @Id Long id;
@Embedded("prefix_") CascadedEmbedded prefixedEmbeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_") CascadedEmbedded prefixedEmbeddable;
@Embedded CascadedEmbedded embeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL) CascadedEmbedded embeddable;
}
@SuppressWarnings("unused")
static class CascadedEmbedded {
String test;
@Embedded("prefix2_") Embeddable prefixedEmbeddable;
@Embedded Embeddable embeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix2_") Embeddable prefixedEmbeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL) Embeddable embeddable;
}
@SuppressWarnings("unused")
@@ -268,7 +269,7 @@ public class SqlGeneratorEmbeddedUnitTests {
@Id Long id;
@Embedded("prefix_") EmbeddedWithReference embedded;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_") EmbeddedWithReference embedded;
}
static class EmbeddedWithReference {

View File

@@ -33,6 +33,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -99,7 +100,7 @@ public class JdbcRepositoryEmbeddedImmutableIntegrationTests {
@Id Long id;
@Embedded("prefix_") Embeddable prefixedEmbeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_") Embeddable prefixedEmbeddable;
}
@Value

View File

@@ -31,6 +31,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -250,16 +251,16 @@ public class JdbcRepositoryEmbeddedIntegrationTests {
@Id Long id;
@Embedded("prefix_") CascadedEmbeddable prefixedEmbeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_") CascadedEmbeddable prefixedEmbeddable;
@Embedded CascadedEmbeddable embeddable;
@Embedded(onEmpty = OnEmpty.USE_NULL) CascadedEmbeddable embeddable;
}
@Data
static class CascadedEmbeddable {
String test;
@Embedded("prefix2_")
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix2_")
Embeddable embeddable;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -246,7 +247,7 @@ public class JdbcRepositoryEmbeddedNotInAggregateRootIntegrationTests {
String test;
@Embedded("prefix_")
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_")
Embeddable embeddable;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -249,7 +250,7 @@ public class JdbcRepositoryEmbeddedWithCollectionIntegrationTests {
String test;
@Embedded("prefix_")
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_")
Embeddable embeddable;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@@ -236,7 +237,7 @@ public class JdbcRepositoryEmbeddedWithReferenceIntegrationTests {
String test;
@Embedded("prefix_")
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_")
Embeddable embeddable;
}

View File

@@ -84,7 +84,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
this.isEmbedded = Lazy.of(() -> Optional.ofNullable(findAnnotation(Embedded.class)).isPresent());
this.embeddedPrefix = Lazy.of(() -> Optional.ofNullable(findAnnotation(Embedded.class)) //
.map(Embedded::value) //
.map(Embedded::prefix) //
.orElse(""));
this.columnName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Column.class)) //

View File

@@ -23,15 +23,38 @@ import java.lang.annotation.Target;
/**
* The annotation to configure a value object as embedded in the current table.
* <p />
* Depending on the {@link OnEmpty value} of {@link #onEmpty()} the property is set to {@literal null} or an empty
* instance in the case all embedded values are {@literal null} when reading from the result set.
*
* @author Bastian Wilhelm
* @author Christoph Strobl
* @since 1.1
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
public @interface Embedded {
/**
* @return prefix for columns in the embedded value object. Default is an empty String
*/
String value() default "";
/**
* Set the load strategy for the embedded object if all contained fields yield {@literal null} values.
*
* @return never {@link} null.
*/
OnEmpty onEmpty();
/**
* @return prefix for columns in the embedded value object. An empty {@link String} by default.
*/
String prefix() default "";
/**
* Load strategy to be used {@link Embedded#onEmpty()}.
*
* @author Christoph Strobl
* @since 1.1
*/
enum OnEmpty {
USE_NULL, USE_EMPTY
}
}

View File

@@ -39,6 +39,7 @@ import org.springframework.data.relational.core.conversion.DbAction.Insert;
import org.springframework.data.relational.core.conversion.DbAction.InsertRoot;
import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable;
@@ -588,7 +589,7 @@ public class RelationalEntityWriterUnitTests {
static class EmbeddedReferenceEntity {
@Id final Long id;
@Embedded("prefix_") Element other;
@Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix_") Element other;
}
@RequiredArgsConstructor

View File

@@ -30,6 +30,7 @@ import org.assertj.core.api.SoftAssertions;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
/**
* Unit tests for the {@link BasicRelationalPersistentProperty}.
@@ -188,10 +189,10 @@ public class BasicRelationalPersistentPropertyUnitTests {
private @Column("dummy_name") String name;
// DATAJDBC-111
private @Embedded EmbeddableEntity embeddableEntity;
private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity;
// DATAJDBC-111
private @Embedded("prefix") EmbeddableEntity prefixedEmbeddableEntity;
private @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix") EmbeddableEntity prefixedEmbeddableEntity;
@Column("dummy_last_updated_at")
public LocalDateTime getLocalDateTime() {

View File

@@ -288,14 +288,19 @@ Embedded entities are used to have value objects in your java data model, even i
In the following example you see, that `MyEntity` is mapped with the `@Embedded` annotation.
The consequence of this is, that in the database a table `my_entity` with the two columns `id` and `name` (from the `EmbeddedEntity` class) is expected.
However, if the `name` column is actually `null` within the result set, the entire property `embeddedEntity` will be set to null according to the `onEmpty` of `@Embedded`, which ``null``s objects when all nested properties are `null`. +
Opposite to this behavior `USE_EMPTY` tries to create a new instance using either a default constructor or one that accepts nullable parameter values from the result set.
.Sample Code of embedding objects
====
[source, java]
----
public class MyEntity {
@Id
Integer id;
@Embedded
@Embedded(onEmpty = USE_NULL) <1>
EmbeddedEntity embeddedEntity;
}
@@ -303,9 +308,10 @@ public class EmbeddedEntity {
String name;
}
----
<1> ``Null``s `embeddedEntity` if `name` in `null`. Use `USE_EMPTY` to instanciate `embeddedEntity` with a potential `null` value for the `name` property.
====
If you need a value object multiple times in an entity, this can be achieved with the optional `value` element of the `@Embedded` annotation.
If you need a value object multiple times in an entity, this can be achieved with the optional `prefix` element of the `@Embedded` annotation.
This element represents a prefix and is prepend for each column name in the embedded object.
[[jdbc.entity-persistence.state-detection-strategies]]