DATACASS-167 - Polishing.

Add value shortcut to Embedded.Nullable and Embedded.Empty for improved annotation usage. Move isEntity check into PersistentProperty.isEmbedded as both checks are issued always together.

Tweak documentation.

Original pull request: #173.
This commit is contained in:
Mark Paluch
2020-04-06 09:54:26 +02:00
parent a59d012fb9
commit ba7b725202
11 changed files with 83 additions and 75 deletions

View File

@@ -434,7 +434,6 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
Assert.notNull(source, "Value must not be null");
// TODO
Class<?> beanClassLoaderClass = transformClassToBeanClassLoaderClass(source.getClass());
CassandraPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(beanClassLoaderClass);
@@ -499,7 +498,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
continue;
}
if (value != null && property.isEmbedded() && property.isEntity()) {
if (value != null && property.isEmbedded()) {
if (log.isDebugEnabled()) {
log.debug("Mapping embedded property [{}] - [{}]", property.getRequiredColumnName(), value);
@@ -653,7 +652,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
log.debug("Adding udt.value [{}] - [{}]", property.getRequiredColumnName(), value);
}
if (property.isEmbedded() && property.isEntity()) {
if (property.isEmbedded()) {
if (log.isDebugEnabled()) {
log.debug("Mapping embedded property [{}] - [{}]", property.getRequiredColumnName(), value);
@@ -918,8 +917,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
}
/**
* Retrieve the value to read for the given {@link CassandraPersistentProperty} from
* {@link BasicCassandraRowValueProvider} and perform optionally a conversion of collection element types.
* Retrieve the value to read for the given {@link CassandraPersistentProperty} from {@link CassandraValueProvider}
* and perform optionally a conversion of collection element types.
*
* @param valueProvider the row.
* @param property the property.
@@ -934,7 +933,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
return doReadEntity(keyEntity, valueProvider);
}
if (property.isEntity() && property.isEmbedded()) {
if (property.isEmbedded()) {
CassandraPersistentEntity<?> targetEntity = embeddedEntityOperations.getEntity(property);
return isNullEmbedded(targetEntity, property, valueProvider) ? null : doReadEntity(targetEntity, valueProvider);
@@ -959,7 +958,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
private boolean isNullEmbedded(CassandraPersistentEntity<?> entity, CassandraPersistentProperty property,
CassandraValueProvider valueProvider) {
if (OnEmpty.USE_EMPTY.equals(property.findAnnotation(Embedded.class).onEmpty())) {
if (OnEmpty.USE_EMPTY.equals(property.getRequiredAnnotation(Embedded.class).onEmpty())) {
return false;
}

View File

@@ -535,7 +535,7 @@ public class QueryMapper {
leafProperty = parentEntity.getPersistentProperty(p.getName());
parentEntity = null;
}
if (p.isEmbedded() && p.isEntity()) {
if (p.isEmbedded()) {
embedded = true;
parentEntity = new EmbeddedEntityOperations(mappingContext).getEntity(p);
}

View File

@@ -158,7 +158,7 @@ public class SchemaFactory {
primaryKeyProperty.getPrimaryKeyOrdering());
}
}
} else if (property.isEmbedded() && property.isEntity()) {
} else if (property.isEmbedded()) {
CassandraPersistentEntity<?> embeddedEntity = embeddedEntityOperations.getEntity(property);
@@ -167,7 +167,6 @@ public class SchemaFactory {
DataType dataType = getDataType(embeddedProperty);
specification.column(embeddedProperty.getRequiredColumnName(), dataType);
}
} else {
DataType type = UserTypeUtil.potentiallyFreeze(getDataType(property));
@@ -248,7 +247,7 @@ public class SchemaFactory {
if (property.isCompositePrimaryKey()) {
indexes.addAll(getCreateIndexSpecificationsFor(mappingContext.getRequiredPersistentEntity(property)));
}
if (property.isEmbedded() && property.isEntity()) {
if (property.isEmbedded()) {
if (property.isAnnotationPresent(Indexed.class)) {
Indexed indexed = property.findAnnotation(Indexed.class);
@@ -281,7 +280,7 @@ public class SchemaFactory {
for (CassandraPersistentProperty property : entity) {
if (property.isEmbedded() && property.isEntity()) {
if (property.isEmbedded()) {
CassandraPersistentEntity<?> embeddedEntity = embeddedEntityOperations.getEntity(property);
for (CassandraPersistentProperty embeddedProperty : embeddedEntity) {

View File

@@ -163,7 +163,7 @@ public interface CassandraPersistentProperty
* @since 3.0
*/
default boolean isEmbedded() {
return findAnnotation(Embedded.class) != null;
return findAnnotation(Embedded.class) != null && isEntity();
}
/**

View File

@@ -26,13 +26,19 @@ import javax.annotation.meta.When;
import org.springframework.core.annotation.AliasFor;
/**
* 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 Christoph Strobl
* @since 3.0
*/
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Target(value = { ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
public @interface Embedded {
/**
* Set the load strategy for the embedded object if all contained fields yield {@literal null} values.
* <p />
@@ -51,7 +57,6 @@ public @interface Embedded {
* Load strategy to be used {@link Embedded#onEmpty()}.
*
* @author Christoph Strobl
* @since 1.1
*/
enum OnEmpty {
USE_NULL, USE_EMPTY
@@ -60,27 +65,17 @@ public @interface Embedded {
/**
* Shortcut for a nullable embedded property.
*
* <pre>
* <code>
* &#64;Embedded.Nullable
* private Address address;
* </code>
* <pre class="code">
* &#64;Embedded.Nullable private Address address;
* </pre>
*
* as alternative to the more verbose
*
* <pre>
* <code>
*
* &#64;Embedded(onEmpty = USE_NULL)
* &#64;javax.annotation.Nonnull(when = When.MAYBE)
* private Address address;
*
* </code>
* <pre class="code">
* &#64;Embedded(onEmpty = USE_NULL) &#64;javax.annotation.Nonnull(when = When.MAYBE) private Address address;
* </pre>
*
* @author Christoph Strobl
* @since 3.0
* @see Embedded#onEmpty()
*/
@Embedded(onEmpty = OnEmpty.USE_NULL)
@@ -95,32 +90,28 @@ public @interface Embedded {
*/
@AliasFor(annotation = Embedded.class, attribute = "prefix")
String prefix() default "";
/**
* @return value for columns in the embedded value object. An empty {@link String} by default.
*/
@AliasFor(annotation = Embedded.class, attribute = "prefix")
String value() default "";
}
/**
* Shortcut for an empty embedded property.
*
* <pre>
* <code>
* &#64;Embedded.Empty
* private Address address;
* </code>
* <pre class="code">
* &#64;Embedded.Empty private Address address;
* </pre>
*
* as alternative to the more verbose
*
* <pre>
* <code>
*
* &#64;Embedded(onEmpty = USE_EMPTY)
* &#64;javax.annotation.Nonnull(when = When.NEVER)
* private Address address;
*
* </code>
* <pre class="code">
* &#64;Embedded(onEmpty = USE_EMPTY) &#64;javax.annotation.Nonnull(when = When.NEVER) private Address address;
* </pre>
*
* @author Christoph Strobl
* @since 3.0
* @see Embedded#onEmpty()
*/
@Embedded(onEmpty = OnEmpty.USE_EMPTY)
@@ -135,5 +126,11 @@ public @interface Embedded {
*/
@AliasFor(annotation = Embedded.class, attribute = "prefix")
String prefix() default "";
/**
* @return value for columns in the embedded value object. An empty {@link String} by default.
*/
@AliasFor(annotation = Embedded.class, attribute = "prefix")
String value() default "";
}
}

View File

@@ -29,23 +29,27 @@ import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.mapping.Embedded.Nullable;
import org.springframework.data.mapping.*;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
/**
* Support methods to obtain {@link PersistentProperty} and {@link PersistentEntity} for embedded properties.
*
* @author Christoph Strobl
* @since 3.0
* @see Embedded
*/
public class EmbeddedEntityOperations {
private final MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext;
public EmbeddedEntityOperations(MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext) {
public EmbeddedEntityOperations(
MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
@@ -73,7 +77,7 @@ public class EmbeddedEntityOperations {
static class PrefixedCassandraPersistentEntity<T> implements CassandraPersistentEntity<T> {
private final String prefix;
private CassandraPersistentEntity<T> delegate;
private final CassandraPersistentEntity<T> delegate;
public PrefixedCassandraPersistentEntity(String prefix, CassandraPersistentEntity<T> delegate) {
@@ -169,7 +173,10 @@ public class EmbeddedEntityOperations {
@Override
@org.springframework.lang.Nullable
public CassandraPersistentProperty getPersistentProperty(String name) {
return new PrefixedCassandraPersistentProperty(prefix, delegate.getPersistentProperty(name));
CassandraPersistentProperty property = delegate.getPersistentProperty(name);
return property == null ? null : new PrefixedCassandraPersistentProperty(prefix, property);
}
@Override
@@ -180,7 +187,10 @@ public class EmbeddedEntityOperations {
@Override
@org.springframework.lang.Nullable
public CassandraPersistentProperty getPersistentProperty(Class<? extends Annotation> annotationType) {
return new PrefixedCassandraPersistentProperty(prefix, delegate.getPersistentProperty(annotationType));
CassandraPersistentProperty property = delegate.getPersistentProperty(annotationType);
return property == null ? null : new PrefixedCassandraPersistentProperty(prefix, property);
}
@Override
@@ -337,7 +347,6 @@ public class EmbeddedEntityOperations {
}
@Override
@org.springframework.lang.Nullable
public CqlIdentifier getColumnName() {
return CqlIdentifier.fromInternal(prefix + delegate.getColumnName().asInternal());
}

View File

@@ -1272,7 +1272,7 @@ public class MappingCassandraConverterUnitTests {
String id;
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
@Embedded.Nullable("prefix") EmbeddedWithSimpleTypes nested;
}
@ToString

View File

@@ -337,8 +337,7 @@ public class UpdateMapperUnitTests {
@Data
static class EmbeddedWithSimpleTypes {
@Indexed // single property index (IndexSpecificationFactory) | sassi index etc. :'(
String firstname;
@Indexed String firstname;
Integer age;
}

View File

@@ -70,11 +70,6 @@ public class SchemaTestUtils {
potentiallyCreateTableFor(persistentEntity, operations, schemaFactory);
}
public static void potentiallyCreateUdtFor(Class<?> entityType, CassandraOperations operations) {
potentiallyCreateUdtFor(operations.getConverter().getMappingContext().getRequiredPersistentEntity(entityType),
operations, new SchemaFactory(operations.getConverter()));
}
private static void potentiallyCreateTableFor(CassandraPersistentEntity<?> persistentEntity,
CassandraOperations operations, SchemaFactory schemaFactory) {
@@ -103,15 +98,18 @@ public class SchemaTestUtils {
} else {
for (CassandraPersistentProperty property : persistentEntity) {
if (property.isEntity()) {
if (property.isEmbedded()) {
potentiallyCreateUdtFor(
new EmbeddedEntityOperations(operations.getConverter().getMappingContext()).getEntity(property),
operations, schemaFactory);
} else {
potentiallyCreateUdtFor(operations.getConverter().getMappingContext().getRequiredPersistentEntity(property),
operations, schemaFactory);
}
if (!property.isEntity()) {
continue;
}
if (property.isEmbedded()) {
potentiallyCreateUdtFor(
new EmbeddedEntityOperations(operations.getConverter().getMappingContext()).getEntity(property),
operations, schemaFactory);
} else {
potentiallyCreateUdtFor(operations.getConverter().getMappingContext().getRequiredPersistentEntity(property),
operations, schemaFactory);
}
}
}

View File

@@ -9,6 +9,7 @@ This chapter summarizes changes and new features for each release.
* Upgrade to Cassandra Driver version 4. See the <<cassandra.migration.2.x-to-3.x,2.x to 3.x migration guide for details>>.
* Support for `NamingStrategy`.
* Support for frozen collections and UDT columns in schema creation.
* Support for <<mapping.embedded-entities,`@Embedded` properties>>.
[[new-features.2-2-0]]
== What's new in Spring Data for Apache Cassandra 2.2

View File

@@ -347,13 +347,14 @@ public class LoginEvent {
[[mapping.embedded-entities]]
=== Embedded Entity Support
Embedded entities are used to have value objects in your java data model who's properties are flattened out into the table.
Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the table.
In the following example you see, that `User.name` is annotated with `@Embedded`.
The consequence of this is the properties of `UserName` are folded into the `user` table which consists of 3 columns (`user_id`, `firstname`, `lastname`).
The consequence of this is that all properties of `UserName` are folded into the `user` table which consists of 3 columns (`user_id`, `firstname`, `lastname`).
[WARNING]
[NOTE]
====
Embedded entities may only contain simple property types. It is not possible to nest an embedded entity into another embedded one.
Embedded entities may only contain simple property types.
It is not possible to nest an embedded entity into another embedded one.
====
However, if the `firstname` and `lastname` column values are actually `null` within the result set, the entire property `name` will be set to `null` according to the `onEmpty` of `@Embedded`, which ``null``s objects when all nested properties are `null`. +
@@ -377,17 +378,19 @@ public class UserName {
private String lastname;
}
----
<1> ``Null``s `embeddedEntity` if `name` in `null`. Use `USE_EMPTY` to instantiate `embeddedEntity` with a potential `null` value for the `name` property.
<1> Property is `null` if `firstname` and `lastname` are `null`.
Use `onEmpty=USE_EMPTY` to instantiate `UserName` with a potential `null` value for its properties.
====
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.
You can embed a value object multiple times in an entity by using the optional `prefix` element of the `@Embedded` annotation.
This element represents a prefix and is prepended to each column name in the embedded object.
Note that properties will overwrite each other if multiple properties render to the same column name.
[TIP]
====
Make use of the shortcuts `@Embedded.Nullable` & `@Embedded.Empty` for `@Embedded(onEmpty = USE_NULL)` and `@Embedded(onEmpty = USE_EMPTY)` to reduce verbosity and simultaneously set JSR-305 `@javax.annotation.Nonnull` accordingly.
Make use of the shortcuts `@Embedded.Nullable` and `@Embedded.Empty` for `@Embedded(onEmpty = USE_NULL)` and `@Embedded(onEmpty = USE_EMPTY)` to reduce verbosity and simultaneously set JSR-305 `@javax.annotation.Nonnull` accordingly.
[source, java]
[source,java]
----
public class MyEntity {
@@ -428,6 +431,9 @@ In order to reference a property of a given `Row`/`UdtValue`/`TupleValue` one ha
Entity-bound insert and update statements do not include this property.
* `@Column`: Applied at the field level.
Describes the column name as it is represented in the Cassandra table, thus letting the name differ from the field name of the class.
* `@Embedded`: Applied at the field level.
Enables embedded object usage for types mapped to a table or a user-defined type.
Properties of the embedded object are flattened into the structure of its parent.
* `@Indexed`: Applied at the field level.
Describes the index to be created at session initialization.
* `@SASI`: Applied at the field level.