DATACASS-167 - Add support for embedded entities.

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 that all properties of `UserName` are folded into the `user` table which consists of 3 columns (user_id, firstname, lastname).

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 nulls objects when all nested properties are null.

Opposite to this behaviour USE_EMPTY tries to create a new instance using either a default constructor or one that accepts nullable parameter values from the result set.

public class User {

    @PrimaryKey("user_id")
    private String userId;

    @Embedded(onEmpty = USE_NULL)
    UserName name;
}

public class UserName {
    private String firstname;
    private String lastname;
}

Original pull request: #173.
This commit is contained in:
Christoph Strobl
2020-03-30 16:18:27 +02:00
committed by Mark Paluch
parent b2031724ff
commit a59d012fb9
16 changed files with 1714 additions and 42 deletions

View File

@@ -42,6 +42,7 @@ import org.springframework.util.StringUtils;
* index-annotated {@link CassandraPersistentProperty properties}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
* @see Indexed
* @see SASI
@@ -124,7 +125,7 @@ class IndexSpecificationFactory {
return indexes;
}
private static CreateIndexSpecification createIndexSpecification(Indexed annotation,
static CreateIndexSpecification createIndexSpecification(Indexed annotation,
CassandraPersistentProperty property) {
CreateIndexSpecification index;

View File

@@ -27,7 +27,6 @@ import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
@@ -41,6 +40,9 @@ import org.springframework.data.cassandra.core.mapping.BasicMapId;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.Embedded.OnEmpty;
import org.springframework.data.cassandra.core.mapping.EmbeddedEntityOperations;
import org.springframework.data.cassandra.core.mapping.MapId;
import org.springframework.data.cassandra.core.mapping.MapIdentifiable;
import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
@@ -83,6 +85,7 @@ import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
* @author Mark Paluch
* @author Antoine Toulme
* @author John Blum
* @author Christoph Strobl
*/
public class MappingCassandraConverter extends AbstractCassandraConverter
implements ApplicationContextAware, BeanClassLoaderAware {
@@ -100,6 +103,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
private SpELContext spELContext;
private final DefaultColumnTypeResolver cassandraTypeResolver;
private final EmbeddedEntityOperations embeddedEntityOperations;
/**
* Create a new {@link MappingCassandraConverter} with a {@link CassandraMappingContext}.
@@ -117,6 +121,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
userTypeName -> getUserTypeResolver().resolveType(userTypeName), this::getCodecRegistry,
this::getCustomConversions);
this.setCustomConversions(conversions);
this.embeddedEntityOperations = new EmbeddedEntityOperations(mappingContext);
}
/**
@@ -137,6 +142,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
userTypeName -> getUserTypeResolver().resolveType(userTypeName), this::getCodecRegistry,
this::getCustomConversions);
this.setCustomConversions(mappingContext.getCustomConversions());
this.embeddedEntityOperations = new EmbeddedEntityOperations(mappingContext);
}
private static ConversionService newConversionService() {
@@ -404,7 +410,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
return;
}
if (property.isCompositePrimaryKey() || valueProvider.hasProperty(property)) {
if (property.isCompositePrimaryKey() || valueProvider.hasProperty(property) || property.isEmbedded()) {
propertyAccessor.setProperty(property, getReadValue(valueProvider, property));
}
}
@@ -493,11 +499,21 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
continue;
}
if (log.isDebugEnabled()) {
log.debug("Adding map.entry [{}] - [{}]", property.getRequiredColumnName(), value);
}
if (value != null && property.isEmbedded() && property.isEntity()) {
sink.put(property.getRequiredColumnName(), value);
if (log.isDebugEnabled()) {
log.debug("Mapping embedded property [{}] - [{}]", property.getRequiredColumnName(), value);
}
write(value, sink, embeddedEntityOperations.getEntity(property));
} else {
if (log.isDebugEnabled()) {
log.debug("Adding map.entry [{}] - [{}]", property.getRequiredColumnName(), value);
}
sink.put(property.getRequiredColumnName(), value);
}
}
}
@@ -625,6 +641,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
continue;
}
// value resolution
Object value = getWriteValue(property, propertyAccessor);
if (log.isDebugEnabled()) {
@@ -636,6 +653,23 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
log.debug("Adding udt.value [{}] - [{}]", property.getRequiredColumnName(), value);
}
if (property.isEmbedded() && property.isEntity()) {
if (log.isDebugEnabled()) {
log.debug("Mapping embedded property [{}] - [{}]", property.getRequiredColumnName(), value);
}
if (value == null) {
continue;
}
CassandraPersistentEntity<?> targetEntity = embeddedEntityOperations.getEntity(property);
writeUDTValue(new ConvertingPropertyAccessor<>(targetEntity.getPropertyAccessor(value), getConversionService()),
udtValue, targetEntity);
continue;
}
TypeCodec<Object> typeCodec = getCodec(property);
udtValue.set(property.getRequiredColumnName().toString(), value, typeCodec);
@@ -900,6 +934,12 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
return doReadEntity(keyEntity, valueProvider);
}
if (property.isEntity() && property.isEmbedded()) {
CassandraPersistentEntity<?> targetEntity = embeddedEntityOperations.getEntity(property);
return isNullEmbedded(targetEntity, property, valueProvider) ? null : doReadEntity(targetEntity, valueProvider);
}
if (!valueProvider.hasProperty(property)) {
return null;
}
@@ -908,6 +948,31 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
return value == null ? null : convertReadValue(value, property.getTypeInformation());
}
/**
* @param entity the property domain type
* @param property the current property annotated with {@link Embedded}.
* @param valueProvider
* @return {@literal true} if the property represents a {@link Embedded.Nullable nullable embedded} entity where all
* values obtainable from the given {@link CassandraValueProvider} are {@literal null}.
* @since 3.0
*/
private boolean isNullEmbedded(CassandraPersistentEntity<?> entity, CassandraPersistentProperty property,
CassandraValueProvider valueProvider) {
if (OnEmpty.USE_EMPTY.equals(property.findAnnotation(Embedded.class).onEmpty())) {
return false;
}
for (CassandraPersistentProperty embeddedProperty : entity) {
if (valueProvider.hasProperty(embeddedProperty) && valueProvider.getPropertyValue(embeddedProperty) != null) {
return false;
}
}
return true;
}
@Nullable
@SuppressWarnings("unchecked")
private Object convertReadValue(Object value, TypeInformation<?> typeInformation) {
@@ -1123,5 +1188,4 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
return parent.getSource();
}
}
}

View File

@@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.mapping.EmbeddedEntityOperations;
import org.springframework.data.cassandra.core.query.ColumnName;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Columns.ColumnSelector;
@@ -53,6 +54,7 @@ import com.datastax.oss.driver.api.core.CqlIdentifier;
* Map {@link org.springframework.data.cassandra.core.query.Query} to CQL-specific data types.
*
* @author Mark Paluch
* @author Christoph Strobl
* @see ColumnName
* @see Columns
* @see Criteria
@@ -517,10 +519,29 @@ public class QueryMapper {
@Override
public ColumnName getMappedKey() {
return path.map(PersistentPropertyPath::getLeafProperty) //
.map(CassandraPersistentProperty::getColumnName) //
.map(ColumnName::from) //
.orElse(name);
if (!path.isPresent()) {
return name;
}
boolean embedded = false;
CassandraPersistentEntity<?> parentEntity = null;
CassandraPersistentProperty leafProperty = null;
for (CassandraPersistentProperty p : path.get()) {
leafProperty = p;
if (embedded) {
embedded = false;
leafProperty = parentEntity.getPersistentProperty(p.getName());
parentEntity = null;
}
if (p.isEmbedded() && p.isEntity()) {
embedded = true;
parentEntity = new EmbeddedEntityOperations(mappingContext).getEntity(p);
}
}
return ColumnName.from(leafProperty.getColumnName());
}
}
}

View File

@@ -26,6 +26,8 @@ import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecifica
import org.springframework.data.cassandra.core.cql.keyspace.CreateUserTypeSpecification;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.mapping.EmbeddedEntityOperations;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mapping.MappingException;
@@ -43,6 +45,7 @@ import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
* Factory for Cassandra Schema objects such as user-defined types, tables and indexes.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 3.0
* @see CreateUserTypeSpecification
* @see CreateTableSpecification
@@ -55,6 +58,8 @@ public class SchemaFactory {
private final ColumnTypeResolver typeResolver;
private final EmbeddedEntityOperations embeddedEntityOperations;
/**
* Creates a new {@link SchemaFactory} given {@link CassandraConverter}.
*
@@ -67,6 +72,7 @@ public class SchemaFactory {
this.mappingContext = converter.getMappingContext();
this.typeResolver = new DefaultColumnTypeResolver(mappingContext, ShallowUserTypeResolver.INSTANCE,
converter::getCodecRegistry, converter::getCustomConversions);
this.embeddedEntityOperations = new EmbeddedEntityOperations(this.mappingContext);
}
/**
@@ -88,6 +94,7 @@ public class SchemaFactory {
this.mappingContext = mappingContext;
this.typeResolver = new DefaultColumnTypeResolver(mappingContext, ShallowUserTypeResolver.INSTANCE,
() -> codecRegistry, () -> customConversions);
this.embeddedEntityOperations = new EmbeddedEntityOperations(this.mappingContext);
}
/**
@@ -151,6 +158,16 @@ public class SchemaFactory {
primaryKeyProperty.getPrimaryKeyOrdering());
}
}
} else if (property.isEmbedded() && property.isEntity()) {
CassandraPersistentEntity<?> embeddedEntity = embeddedEntityOperations.getEntity(property);
for (CassandraPersistentProperty embeddedProperty : embeddedEntity) {
DataType dataType = getDataType(embeddedProperty);
specification.column(embeddedProperty.getRequiredColumnName(), dataType);
}
} else {
DataType type = UserTypeUtil.potentiallyFreeze(getDataType(property));
@@ -230,6 +247,17 @@ public class SchemaFactory {
for (CassandraPersistentProperty property : entity) {
if (property.isCompositePrimaryKey()) {
indexes.addAll(getCreateIndexSpecificationsFor(mappingContext.getRequiredPersistentEntity(property)));
}
if (property.isEmbedded() && property.isEntity()) {
if (property.isAnnotationPresent(Indexed.class)) {
Indexed indexed = property.findAnnotation(Indexed.class);
for (CassandraPersistentProperty embeddedProperty : embeddedEntityOperations.getEntity(property)) {
indexes.add(IndexSpecificationFactory.createIndexSpecification(indexed, embeddedProperty));
}
} else {
indexes.addAll(getCreateIndexSpecificationsFor(embeddedEntityOperations.getEntity(property)));
}
} else {
indexes.addAll(IndexSpecificationFactory.createIndexSpecifications(property));
}
@@ -252,8 +280,19 @@ public class SchemaFactory {
CreateUserTypeSpecification specification = CreateUserTypeSpecification.createType(entity.getTableName());
for (CassandraPersistentProperty property : entity) {
// Use frozen literal to not resolve types from Cassandra; At this stage, they might be not created yet.
specification.field(property.getRequiredColumnName(), UserTypeUtil.potentiallyFreeze(getDataType(property)));
if (property.isEmbedded() && property.isEntity()) {
CassandraPersistentEntity<?> embeddedEntity = embeddedEntityOperations.getEntity(property);
for (CassandraPersistentProperty embeddedProperty : embeddedEntity) {
DataType dataType = getDataType(embeddedProperty);
specification.field(embeddedProperty.getRequiredColumnName(), dataType);
}
} else {
// Use frozen literal to not resolve types from Cassandra; At this stage, they might be not created yet.
specification.field(property.getRequiredColumnName(), UserTypeUtil.potentiallyFreeze(getDataType(property)));
}
}
if (specification.getFields().isEmpty()) {

View File

@@ -34,6 +34,7 @@ import com.datastax.oss.driver.api.core.CqlIdentifier;
* @author David T. Webb
* @author Mark Paluch
* @author John Blum
* @author Christoph Strobl
*/
public interface CassandraPersistentProperty
extends PersistentProperty<CassandraPersistentProperty>, ApplicationContextAware {
@@ -157,6 +158,14 @@ public interface CassandraPersistentProperty
*/
boolean isPrimaryKeyColumn();
/**
* @return {@literal true} if the property should be embedded.
* @since 3.0
*/
default boolean isEmbedded() {
return findAnnotation(Embedded.class) != null;
}
/**
* Find an {@link AnnotatedType} by {@code annotationType} derived from the property type. Annotated type is looked up
* by introspecting property field/accessors. Collection/Map-like types are introspected for type annotations within

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.cassandra.core.mapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.annotation.meta.When;
import org.springframework.core.annotation.AliasFor;
/**
* @author Christoph Strobl
* @since 3.0
*/
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.ANNOTATION_TYPE })
public @interface Embedded {
/**
* Set the load strategy for the embedded object if all contained fields yield {@literal null} values.
* <p />
* {@link Nullable @Embedded.Nullable} and {@link Empty @Embedded.Empty} offer shortcuts for this.
*
* @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
}
/**
* Shortcut for a nullable embedded property.
*
* <pre>
* <code>
* &#64;Embedded.Nullable
* private Address address;
* </code>
* </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>
*
* @author Christoph Strobl
* @since 3.0
* @see Embedded#onEmpty()
*/
@Embedded(onEmpty = OnEmpty.USE_NULL)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
@javax.annotation.Nonnull(when = When.MAYBE)
@interface Nullable {
/**
* @return prefix for columns in the embedded value object. An empty {@link String} by default.
*/
@AliasFor(annotation = Embedded.class, attribute = "prefix")
String prefix() default "";
}
/**
* Shortcut for an empty embedded property.
*
* <pre>
* <code>
* &#64;Embedded.Empty
* private Address address;
* </code>
* </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>
*
* @author Christoph Strobl
* @since 3.0
* @see Embedded#onEmpty()
*/
@Embedded(onEmpty = OnEmpty.USE_EMPTY)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
@javax.annotation.Nonnull(when = When.NEVER)
@interface Empty {
/**
* @return prefix for columns in the embedded value object. An empty {@link String} by default.
*/
@AliasFor(annotation = Embedded.class, attribute = "prefix")
String prefix() default "";
}
}

View File

@@ -0,0 +1,610 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.cassandra.core.mapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
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.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
/**
* @author Christoph Strobl
* @since 3.0
*/
public class EmbeddedEntityOperations {
private final MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext;
public EmbeddedEntityOperations(MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
public CassandraPersistentEntity<?> getEntity(CassandraPersistentProperty property) {
return withPrefix(getPrefix(property), mappingContext.getPersistentEntity(property));
}
static <T> CassandraPersistentEntity<T> withPrefix(@org.springframework.lang.Nullable String prefix,
CassandraPersistentEntity<T> source) {
if (!StringUtils.hasText(prefix)) {
return source;
}
return new PrefixedCassandraPersistentEntity<>(prefix, source);
}
@Nullable
static String getPrefix(CassandraPersistentProperty property) {
Embedded embedded = property.findAnnotation(Embedded.class);
return embedded != null ? embedded.prefix() : null;
}
static class PrefixedCassandraPersistentEntity<T> implements CassandraPersistentEntity<T> {
private final String prefix;
private CassandraPersistentEntity<T> delegate;
public PrefixedCassandraPersistentEntity(String prefix, CassandraPersistentEntity<T> delegate) {
this.prefix = prefix;
this.delegate = delegate;
}
@Override
public boolean isCompositePrimaryKey() {
return delegate.isCompositePrimaryKey();
}
@Override
@Deprecated
public void setForceQuote(boolean forceQuote) {
delegate.setForceQuote(forceQuote);
}
@Override
public CqlIdentifier getTableName() {
return delegate.getTableName();
}
@Override
@Deprecated
public void setTableName(org.springframework.data.cassandra.core.cql.CqlIdentifier tableName) {
delegate.setTableName(tableName);
}
@Override
public void setTableName(CqlIdentifier tableName) {
delegate.setTableName(tableName);
}
@Override
public boolean isTupleType() {
return delegate.isTupleType();
}
@Override
public boolean isUserDefinedType() {
return delegate.isUserDefinedType();
}
@Override
public String getName() {
return delegate.getName();
}
@Override
@org.springframework.lang.Nullable
public PreferredConstructor<T, CassandraPersistentProperty> getPersistenceConstructor() {
return delegate.getPersistenceConstructor();
}
@Override
public boolean isConstructorArgument(PersistentProperty<?> property) {
return delegate.isConstructorArgument(property);
}
@Override
public boolean isIdProperty(PersistentProperty<?> property) {
return delegate.isIdProperty(property);
}
@Override
public boolean isVersionProperty(PersistentProperty<?> property) {
return delegate.isVersionProperty(property);
}
@Override
@org.springframework.lang.Nullable
public CassandraPersistentProperty getIdProperty() {
return delegate.getIdProperty();
}
@Override
public CassandraPersistentProperty getRequiredIdProperty() {
return delegate.getRequiredIdProperty();
}
@Override
@org.springframework.lang.Nullable
public CassandraPersistentProperty getVersionProperty() {
return delegate.getVersionProperty();
}
@Override
public CassandraPersistentProperty getRequiredVersionProperty() {
return delegate.getRequiredVersionProperty();
}
@Override
@org.springframework.lang.Nullable
public CassandraPersistentProperty getPersistentProperty(String name) {
return new PrefixedCassandraPersistentProperty(prefix, delegate.getPersistentProperty(name));
}
@Override
public CassandraPersistentProperty getRequiredPersistentProperty(String name) {
return new PrefixedCassandraPersistentProperty(prefix, delegate.getRequiredPersistentProperty(name));
}
@Override
@org.springframework.lang.Nullable
public CassandraPersistentProperty getPersistentProperty(Class<? extends Annotation> annotationType) {
return new PrefixedCassandraPersistentProperty(prefix, delegate.getPersistentProperty(annotationType));
}
@Override
public Iterable<CassandraPersistentProperty> getPersistentProperties(Class<? extends Annotation> annotationType) {
return delegate.getPersistentProperties(annotationType);
}
@Override
public boolean hasIdProperty() {
return delegate.hasIdProperty();
}
@Override
public boolean hasVersionProperty() {
return delegate.hasVersionProperty();
}
@Override
public Class<T> getType() {
return delegate.getType();
}
@Override
public Alias getTypeAlias() {
return delegate.getTypeAlias();
}
@Override
public TypeInformation<T> getTypeInformation() {
return delegate.getTypeInformation();
}
@Override
public void doWithProperties(PropertyHandler<CassandraPersistentProperty> handler) {
delegate.doWithProperties((PropertyHandler<CassandraPersistentProperty>) property -> {
handler.doWithPersistentProperty(wrap(property));
});
}
@Override
public void doWithProperties(SimplePropertyHandler handler) {
delegate.doWithProperties((SimplePropertyHandler) property -> {
if (property instanceof CassandraPersistentProperty) {
handler.doWithPersistentProperty(wrap((CassandraPersistentProperty) property));
} else {
handler.doWithPersistentProperty(property);
}
});
delegate.doWithProperties(handler);
}
@Override
public void doWithAssociations(AssociationHandler<CassandraPersistentProperty> handler) {
delegate.doWithAssociations(handler);
}
@Override
public void doWithAssociations(SimpleAssociationHandler handler) {
delegate.doWithAssociations(handler);
}
@Override
@org.springframework.lang.Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
return delegate.findAnnotation(annotationType);
}
@Override
public <A extends Annotation> A getRequiredAnnotation(Class<A> annotationType) throws IllegalStateException {
return delegate.getRequiredAnnotation(annotationType);
}
@Override
public <A extends Annotation> boolean isAnnotationPresent(Class<A> annotationType) {
return delegate.isAnnotationPresent(annotationType);
}
@Override
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
return delegate.getPropertyAccessor(bean);
}
@Override
public <B> PersistentPropertyPathAccessor<B> getPropertyPathAccessor(B bean) {
return delegate.getPropertyPathAccessor(bean);
}
@Override
public IdentifierAccessor getIdentifierAccessor(Object bean) {
return delegate.getIdentifierAccessor(bean);
}
@Override
public boolean isNew(Object bean) {
return delegate.isNew(bean);
}
@Override
public boolean isImmutable() {
return delegate.isImmutable();
}
@Override
public boolean requiresPropertyPopulation() {
return delegate.requiresPropertyPopulation();
}
@NotNull
@Override
public Iterator<CassandraPersistentProperty> iterator() {
List<CassandraPersistentProperty> target = new ArrayList<>();
delegate.iterator().forEachRemaining(it -> target.add(wrap(it)));
return target.iterator();
}
@Override
public void forEach(Consumer<? super CassandraPersistentProperty> action) {
delegate.forEach(it -> action.accept(wrap(it)));
}
@Override
public Spliterator<CassandraPersistentProperty> spliterator() {
return delegate.spliterator();
}
private PrefixedCassandraPersistentProperty wrap(CassandraPersistentProperty source) {
return new PrefixedCassandraPersistentProperty(prefix, source);
}
}
static class PrefixedCassandraPersistentProperty implements CassandraPersistentProperty {
private final String prefix;
private final CassandraPersistentProperty delegate;
public PrefixedCassandraPersistentProperty(String prefix, CassandraPersistentProperty delegate) {
this.prefix = prefix;
this.delegate = delegate;
}
@Override
@Deprecated
public void setColumnName(org.springframework.data.cassandra.core.cql.CqlIdentifier columnName) {
delegate.setColumnName(columnName);
}
@Override
public void setColumnName(CqlIdentifier columnName) {
delegate.setColumnName(columnName);
}
@Override
@org.springframework.lang.Nullable
public CqlIdentifier getColumnName() {
return CqlIdentifier.fromInternal(prefix + delegate.getColumnName().asInternal());
}
@Override
@Deprecated
public void setForceQuote(boolean forceQuote) {
delegate.setForceQuote(forceQuote);
}
@Override
@org.springframework.lang.Nullable
public Integer getOrdinal() {
return delegate.getOrdinal();
}
@Override
public int getRequiredOrdinal() {
return delegate.getRequiredOrdinal();
}
@Override
@org.springframework.lang.Nullable
public Ordering getPrimaryKeyOrdering() {
return delegate.getPrimaryKeyOrdering();
}
@Override
public boolean isClusterKeyColumn() {
return delegate.isClusterKeyColumn();
}
@Override
public boolean isCompositePrimaryKey() {
return delegate.isCompositePrimaryKey();
}
@Override
public boolean isMapLike() {
return delegate.isMapLike();
}
@Override
public boolean isPartitionKeyColumn() {
return delegate.isPartitionKeyColumn();
}
@Override
public boolean isPrimaryKeyColumn() {
return delegate.isPrimaryKeyColumn();
}
@Override
public boolean isEmbedded() {
return delegate.isEmbedded();
}
@Override
@org.springframework.lang.Nullable
public AnnotatedType findAnnotatedType(Class<? extends Annotation> annotationType) {
return delegate.findAnnotatedType(annotationType);
}
@Override
public PersistentEntity<?, CassandraPersistentProperty> getOwner() {
return delegate.getOwner();
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public Class<?> getType() {
return delegate.getType();
}
@Override
public TypeInformation<?> getTypeInformation() {
return delegate.getTypeInformation();
}
@Override
public Iterable<? extends TypeInformation<?>> getPersistentEntityTypes() {
return delegate.getPersistentEntityTypes();
}
@Override
@org.springframework.lang.Nullable
public Method getGetter() {
return delegate.getGetter();
}
@Override
public Method getRequiredGetter() {
return delegate.getRequiredGetter();
}
@Override
@org.springframework.lang.Nullable
public Method getSetter() {
return delegate.getSetter();
}
@Override
public Method getRequiredSetter() {
return delegate.getRequiredSetter();
}
@Override
@org.springframework.lang.Nullable
public Method getWither() {
return delegate.getWither();
}
@Override
public Method getRequiredWither() {
return delegate.getRequiredWither();
}
@Override
@org.springframework.lang.Nullable
public Field getField() {
return delegate.getField();
}
@Override
public Field getRequiredField() {
return delegate.getRequiredField();
}
@Override
@org.springframework.lang.Nullable
public String getSpelExpression() {
return delegate.getSpelExpression();
}
@Override
@org.springframework.lang.Nullable
public Association<CassandraPersistentProperty> getAssociation() {
return delegate.getAssociation();
}
@Override
public Association<CassandraPersistentProperty> getRequiredAssociation() {
return delegate.getRequiredAssociation();
}
@Override
public boolean isEntity() {
return delegate.isEntity();
}
@Override
public boolean isIdProperty() {
return delegate.isIdProperty();
}
@Override
public boolean isVersionProperty() {
return delegate.isVersionProperty();
}
@Override
public boolean isCollectionLike() {
return delegate.isCollectionLike();
}
@Override
public boolean isMap() {
return delegate.isMap();
}
@Override
public boolean isArray() {
return delegate.isArray();
}
@Override
public boolean isTransient() {
return delegate.isTransient();
}
@Override
public boolean isWritable() {
return delegate.isWritable();
}
@Override
public boolean isImmutable() {
return delegate.isImmutable();
}
@Override
public boolean isAssociation() {
return delegate.isAssociation();
}
@Override
@org.springframework.lang.Nullable
public Class<?> getComponentType() {
return delegate.getComponentType();
}
@Override
public Class<?> getRawType() {
return delegate.getRawType();
}
@Override
@org.springframework.lang.Nullable
public Class<?> getMapValueType() {
return delegate.getMapValueType();
}
@Override
public Class<?> getActualType() {
return delegate.getActualType();
}
@Override
@org.springframework.lang.Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
return delegate.findAnnotation(annotationType);
}
@Override
public <A extends Annotation> A getRequiredAnnotation(Class<A> annotationType) throws IllegalStateException {
return delegate.getRequiredAnnotation(annotationType);
}
@Override
@org.springframework.lang.Nullable
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
return delegate.findPropertyOrOwnerAnnotation(annotationType);
}
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return delegate.isAnnotationPresent(annotationType);
}
@Override
public boolean usePropertyAccess() {
return delegate.usePropertyAccess();
}
@Override
public boolean hasActualTypeAnnotation(Class<? extends Annotation> annotationType) {
return delegate.hasActualTypeAnnotation(annotationType);
}
@Override
@org.springframework.lang.Nullable
public Class<?> getAssociationTargetType() {
return delegate.getAssociationTargetType();
}
@Override
public <T> PersistentPropertyAccessor<T> getAccessorForOwner(T owner) {
return delegate.getAccessorForOwner(owner);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
delegate.setApplicationContext(applicationContext);
}
}
}

View File

@@ -37,15 +37,17 @@ import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
import org.springframework.data.cassandra.core.cql.CqlTemplate;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.BasicMapId;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import org.springframework.data.cassandra.core.query.CassandraPageRequest;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Query;
@@ -60,12 +62,14 @@ import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.util.Version;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.uuid.Uuids;
/**
* Integration tests for {@link CassandraTemplate}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest {
@@ -79,6 +83,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI
public void setUp() {
MappingCassandraConverter converter = new MappingCassandraConverter();
converter.setUserTypeResolver(new SimpleUserTypeResolver(session, CqlIdentifier.fromCql(keyspace)));
converter.afterPropertiesSet();
cassandraVersion = CassandraVersion.get(session);
@@ -90,10 +95,20 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI
SchemaTestUtils.potentiallyCreateTableFor(BookReference.class, template);
SchemaTestUtils.potentiallyCreateTableFor(TimeClass.class, template);
SchemaTestUtils.potentiallyCreateTableFor(TypeWithCompositeKey.class, template);
SchemaTestUtils.potentiallyCreateTableFor(WithNullableEmbeddedType.class, template);
SchemaTestUtils.potentiallyCreateTableFor(WithEmptyEmbeddedType.class, template);
SchemaTestUtils.potentiallyCreateTableFor(WithPrefixedNullableEmbeddedType.class, template);
SchemaTestUtils.createTableAndTypes(OuterWithNullableEmbeddedType.class, template);
SchemaTestUtils.createTableAndTypes(OuterWithPrefixedNullableEmbeddedType.class, template);
SchemaTestUtils.truncate(User.class, template);
SchemaTestUtils.truncate(UserToken.class, template);
SchemaTestUtils.truncate(BookReference.class, template);
SchemaTestUtils.truncate(TypeWithCompositeKey.class, template);
SchemaTestUtils.truncate(WithNullableEmbeddedType.class, template);
SchemaTestUtils.truncate(WithEmptyEmbeddedType.class, template);
SchemaTestUtils.truncate(WithPrefixedNullableEmbeddedType.class, template);
SchemaTestUtils.truncate(OuterWithNullableEmbeddedType.class, template);
SchemaTestUtils.truncate(OuterWithPrefixedNullableEmbeddedType.class, template);
}
@Test // DATACASS-343
@@ -556,6 +571,119 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI
assertThat(iterations).isEqualTo(10);
}
@Test // DATACASS-167
public void shouldSaveAndReadPrefixedEmbeddedCorrectly() {
WithPrefixedNullableEmbeddedType entity = new WithPrefixedNullableEmbeddedType();
entity.id = "id-1";
entity.nested = new EmbeddedWithSimpleTypes();
entity.nested.firstname = "fn";
entity.nested.age = 30;
template.insert(WithPrefixedNullableEmbeddedType.class).one(entity);
WithPrefixedNullableEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")),
WithPrefixedNullableEmbeddedType.class);
assertThat(target).isEqualTo(entity);
}
@Test // DATACASS-167
public void shouldSaveAndReadEmbeddedCorrectly() {
WithNullableEmbeddedType entity = new WithNullableEmbeddedType();
entity.id = "id-1";
entity.nested = new EmbeddedWithSimpleTypes();
entity.nested.firstname = "fn";
entity.nested.age = 30;
template.insert(WithNullableEmbeddedType.class).one(entity);
WithNullableEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")),
WithNullableEmbeddedType.class);
assertThat(target).isEqualTo(entity);
}
@Test // DATACASS-167
public void shouldSaveAndReadNullableEmbeddedCorrectly() {
WithNullableEmbeddedType entity = new WithNullableEmbeddedType();
entity.id = "id-1";
entity.nested = null;
template.insert(WithNullableEmbeddedType.class).one(entity);
WithNullableEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")),
WithNullableEmbeddedType.class);
assertThat(target.id).isEqualTo("id-1");
assertThat(target.nested).isNull();
}
@Test // DATACASS-167
public void shouldSaveAndReadEmptyEmbeddedCorrectly() {
WithEmptyEmbeddedType entity = new WithEmptyEmbeddedType();
entity.id = "id-1";
entity.nested = null;
template.insert(WithEmptyEmbeddedType.class).one(entity);
WithEmptyEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")), WithEmptyEmbeddedType.class);
assertThat(target.id).isEqualTo("id-1");
assertThat(target.nested).isNotNull();
}
@Test // DATACASS-167
public void shouldSaveAndReadEmbeddedUDTCorrectly() {
OuterWithNullableEmbeddedType entity = new OuterWithNullableEmbeddedType();
entity.id = "id-1";
entity.udtValue = new UDTWithNullableEmbeddedType();
entity.udtValue.value = "value-1";
entity.udtValue.nested = new EmbeddedWithSimpleTypes();
entity.udtValue.nested.firstname = "fn";
entity.udtValue.nested.age = 30;
template.insert(OuterWithNullableEmbeddedType.class).one(entity);
OuterWithNullableEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")),
OuterWithNullableEmbeddedType.class);
assertThat(target).isEqualTo(entity);
}
@Test // DATACASS-167
public void shouldSaveAndReadPrefixedUdtEmbeddedCorrectly() {
OuterWithPrefixedNullableEmbeddedType entity = new OuterWithPrefixedNullableEmbeddedType();
entity.id = "id-1";
entity.udtValue = new UDTWithPrefixedNullableEmbeddedType();
entity.udtValue.value = "value-1";
entity.udtValue.nested = new EmbeddedWithSimpleTypes();
entity.udtValue.nested.firstname = "fn";
entity.udtValue.nested.age = 30;
template.insert(OuterWithPrefixedNullableEmbeddedType.class).one(entity);
OuterWithPrefixedNullableEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")),
OuterWithPrefixedNullableEmbeddedType.class);
assertThat(target).isEqualTo(entity);
}
@Test // DATACASS-167
public void shouldSaveAndReadNullEmbeddedUDTCorrectly() {
OuterWithNullableEmbeddedType entity = new OuterWithNullableEmbeddedType();
entity.id = "id-1";
entity.udtValue = new UDTWithNullableEmbeddedType();
entity.udtValue.value = "value-1";
entity.udtValue.nested = null;
template.insert(OuterWithNullableEmbeddedType.class).one(entity);
OuterWithNullableEmbeddedType target = template.selectOne(Query.query(where("id").is("id-1")),
OuterWithNullableEmbeddedType.class);
assertThat(target).isEqualTo(entity);
}
@Data
static class TimeClass {
@@ -579,4 +707,69 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI
@PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED) String lastname;
}
@Data
static class WithNullableEmbeddedType {
@Id String id;
@Embedded.Nullable EmbeddedWithSimpleTypes nested;
}
@Data
static class WithPrefixedNullableEmbeddedType {
@Id String id;
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
}
@Data
static class WithEmptyEmbeddedType {
@Id String id;
@Embedded.Empty EmbeddedWithSimpleTypes nested;
}
@Data
static class EmbeddedWithSimpleTypes {
String firstname;
Integer age;
}
@Data
static class OuterWithNullableEmbeddedType {
@Id String id;
UDTWithNullableEmbeddedType udtValue;
}
@Data
static class OuterWithPrefixedNullableEmbeddedType {
@Id String id;
UDTWithPrefixedNullableEmbeddedType udtValue;
}
@UserDefinedType
@Data
static class UDTWithNullableEmbeddedType {
String value;
@Embedded.Nullable EmbeddedWithSimpleTypes nested;
}
@UserDefinedType
@Data
static class UDTWithPrefixedNullableEmbeddedType {
String value;
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
}
}

View File

@@ -34,9 +34,10 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
import org.springframework.data.cassandra.support.UserDefinedTypeBuilder;
@@ -51,6 +52,7 @@ import com.datastax.oss.driver.api.core.type.DataTypes;
* Unit tests for UDT through {@link MappingCassandraConverter}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.Silent.class) // there are some unused stubbings in RowMockUtil but they're used in other
public class MappingCassandraConverterUDTUnitTests {
@@ -61,6 +63,12 @@ public class MappingCassandraConverterUDTUnitTests {
.withField("name", DataTypes.TEXT).withField("displayname", DataTypes.TEXT).build();
com.datastax.oss.driver.api.core.type.UserDefinedType currency = UserDefinedTypeBuilder.forName("mycurrency")
.withField("currency", DataTypes.TEXT).build();
com.datastax.oss.driver.api.core.type.UserDefinedType withnullableembeddedtype = UserDefinedTypeBuilder
.forName("withnullableembeddedtype").withField("value", DataTypes.TEXT).withField("firstname", DataTypes.TEXT)
.withField("age", DataTypes.INT).build();
com.datastax.oss.driver.api.core.type.UserDefinedType withprefixednullableembeddedtype = UserDefinedTypeBuilder
.forName("withnullableembeddedtype").withField("value", DataTypes.TEXT)
.withField("prefixfirstname", DataTypes.TEXT).withField("prefixage", DataTypes.INT).build();
Row rowMock;
@@ -78,6 +86,10 @@ public class MappingCassandraConverterUDTUnitTests {
when(userTypeResolver.resolveType(CqlIdentifier.fromCql("manufacturer"))).thenReturn(manufacturer);
when(userTypeResolver.resolveType(CqlIdentifier.fromCql("currency"))).thenReturn(currency);
when(userTypeResolver.resolveType(CqlIdentifier.fromCql("withnullableembeddedtype")))
.thenReturn(withnullableembeddedtype);
when(userTypeResolver.resolveType(CqlIdentifier.fromCql("withprefixednullableembeddedtype")))
.thenReturn(withprefixednullableembeddedtype);
}
@Test // DATACASS-487, DATACASS-623
@@ -127,6 +139,99 @@ public class MappingCassandraConverterUDTUnitTests {
.contains("{currency:'EUR'}", "{currency:'USD'}");
}
@Test // DATACASS-167
public void writeFlattensEmbeddedType() {
OuterWithNullableEmbeddedType entity = new OuterWithNullableEmbeddedType();
entity.id = "id-1";
entity.udtValue = new WithNullableEmbeddedType();
entity.udtValue.value = "value-string";
entity.udtValue.nested = new EmbeddedWithSimpleTypes();
entity.udtValue.nested.firstname = "fn";
entity.udtValue.nested.age = 30;
Map<CqlIdentifier, Object> sink = new LinkedHashMap<>();
mappingCassandraConverter.write(entity, sink);
assertThat(sink).containsEntry(CqlIdentifier.fromInternal("id"), "id-1");
assertThat((UdtValue) sink.get(CqlIdentifier.fromInternal("udtvalue"))).extracting(UdtValue::getFormattedContents)
.isEqualTo("{value:'value-string',firstname:'fn',age:30}");
}
@Test // DATACASS-167
public void writeNullEmbeddedType() {
OuterWithNullableEmbeddedType entity = new OuterWithNullableEmbeddedType();
entity.id = "id-1";
entity.udtValue = new WithNullableEmbeddedType();
entity.udtValue.value = "value-string";
entity.udtValue.nested = null;
Map<CqlIdentifier, Object> sink = new LinkedHashMap<>();
mappingCassandraConverter.write(entity, sink);
assertThat(sink).containsEntry(CqlIdentifier.fromInternal("id"), "id-1");
assertThat((UdtValue) sink.get(CqlIdentifier.fromInternal("udtvalue"))).extracting(UdtValue::getFormattedContents)
.isEqualTo("{value:'value-string',firstname:NULL,age:NULL}");
}
@Test // DATACASS-167
public void writePrefixesEmbeddedType() {
OuterWithPrefixedNullableEmbeddedType entity = new OuterWithPrefixedNullableEmbeddedType();
entity.id = "id-1";
entity.udtValue = new WithPrefixedNullableEmbeddedType();
entity.udtValue.value = "value-string";
entity.udtValue.nested = new EmbeddedWithSimpleTypes();
entity.udtValue.nested.firstname = "fn";
entity.udtValue.nested.age = 30;
Map<CqlIdentifier, Object> sink = new LinkedHashMap<>();
mappingCassandraConverter.write(entity, sink);
assertThat(sink).containsEntry(CqlIdentifier.fromInternal("id"), "id-1");
assertThat((UdtValue) sink.get(CqlIdentifier.fromInternal("udtvalue"))).extracting(UdtValue::getFormattedContents)
.isEqualTo("{value:'value-string',prefixfirstname:'fn',prefixage:30}");
}
@Test // DATACASS-167
public void readEmbeddedType() {
UdtValue udtValue = withnullableembeddedtype.newValue().setString("value", "value-string")
.setString("firstname", "fn").setInt("age", 30);
rowMock = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT),
column("udtvalue", udtValue, withnullableembeddedtype));
OuterWithNullableEmbeddedType target = mappingCassandraConverter.read(OuterWithNullableEmbeddedType.class, rowMock);
assertThat(target.getId()).isEqualTo("id-1");
assertThat(target.udtValue).isNotNull();
assertThat(target.udtValue.value).isEqualTo("value-string");
assertThat(target.udtValue.nested.firstname).isEqualTo("fn");
assertThat(target.udtValue.nested.age).isEqualTo(30);
}
@Test // DATACASS-167
public void readPrefixedEmbeddedType() {
UdtValue udtValue = withprefixednullableembeddedtype.newValue().setString("value", "value-string")
.setString("prefixfirstname", "fn").setInt("prefixage", 30);
rowMock = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT),
column("udtvalue", udtValue, withprefixednullableembeddedtype));
OuterWithPrefixedNullableEmbeddedType target = mappingCassandraConverter
.read(OuterWithPrefixedNullableEmbeddedType.class, rowMock);
assertThat(target.getId()).isEqualTo("id-1");
assertThat(target.udtValue).isNotNull();
assertThat(target.udtValue.value).isEqualTo("value-string");
assertThat(target.udtValue.nested.firstname).isEqualTo("fn");
assertThat(target.udtValue.nested.age).isEqualTo(30);
}
@UserDefinedType
@Data
@AllArgsConstructor
@@ -148,4 +253,63 @@ public class MappingCassandraConverterUDTUnitTests {
private static class Supplier {
Map<Manufacturer, List<Currency>> acceptedCurrencies;
}
@Data
static class OuterWithNullableEmbeddedType {
@Id String id;
WithNullableEmbeddedType udtValue;
}
@Data
static class OuterWithPrefixedNullableEmbeddedType {
@Id String id;
WithPrefixedNullableEmbeddedType udtValue;
}
@UserDefinedType
@Data
static class WithNullableEmbeddedType {
String value;
@Embedded.Nullable EmbeddedWithSimpleTypes nested;
}
@UserDefinedType
@Data
static class WithPrefixedNullableEmbeddedType {
String value;
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
}
@UserDefinedType
@Data
static class WithEmptyEmbeddedType {
String value;
@Embedded.Empty EmbeddedWithSimpleTypes nested;
}
@Data
static class EmbeddedWithSimpleTypes {
String firstname;
Integer age;
public String getFirstname() {
return firstname;
}
public Integer getAge() {
return age;
}
}
}

View File

@@ -20,7 +20,10 @@ import static org.springframework.data.cassandra.core.mapping.BasicMapId.*;
import static org.springframework.data.cassandra.test.util.RowMockUtil.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -37,7 +40,6 @@ import java.util.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
@@ -46,6 +48,7 @@ import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.BasicMapId;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.MapId;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass;
@@ -68,6 +71,7 @@ import com.datastax.oss.driver.api.core.type.DataTypes;
* Unit tests for {@link MappingCassandraConverter}.
*
* @author Mark Paluch
* @author Christoph Strobl
* @soundtrack Outlandich - Dont Leave Me Feat Cyt (Sun Kidz Electrocore Mix)
*/
public class MappingCassandraConverterUnitTests {
@@ -292,8 +296,7 @@ public class MappingCassandraConverterUnitTests {
@Test // DATACASS-280
public void shouldReadInetAddressCorrectly() throws UnknownHostException {
InetAddress localHost = InetAddress.getLocalHost();
InetAddress localHost = InetAddress.getLoopbackAddress();
rowMock = RowMockUtil.newRowMock(column("foo", localHost, DataTypes.UUID));
InetAddress result = mappingCassandraConverter.readRow(InetAddress.class, rowMock);
@@ -1255,4 +1258,128 @@ public class MappingCassandraConverterUnitTests {
this.firstname = firstname;
}
}
@ToString
static class WithNullableEmbeddedType {
String id;
@Embedded.Nullable EmbeddedWithSimpleTypes nested;
}
@ToString
static class WithPrefixedNullableEmbeddedType {
String id;
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
}
@ToString
static class WithEmptyEmbeddedType {
String id;
@Embedded.Empty EmbeddedWithSimpleTypes nested;
}
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
static class EmbeddedWithSimpleTypes {
String firstname;
Integer age;
@Transient String displayName;
}
@Test // DATACASS-167
public void writeFlattensEmbeddedType() {
WithNullableEmbeddedType entity = new WithNullableEmbeddedType();
entity.id = "id-1";
entity.nested = new EmbeddedWithSimpleTypes();
entity.nested.firstname = "fn";
entity.nested.age = 30;
entity.nested.displayName = "dp-name";
Map<CqlIdentifier, Object> sink = new LinkedHashMap<>();
mappingCassandraConverter.write(entity, sink);
assertThat(sink) //
.containsEntry(CqlIdentifier.fromCql("id"), "id-1") //
.containsEntry(CqlIdentifier.fromCql("age"), 30) //
.containsEntry(CqlIdentifier.fromCql("firstname"), "fn") //
.doesNotContainKey(CqlIdentifier.fromCql("displayName"));
}
@Test // DATACASS-167
public void writePrefixesEmbeddedType() {
WithPrefixedNullableEmbeddedType entity = new WithPrefixedNullableEmbeddedType();
entity.id = "id-1";
entity.nested = new EmbeddedWithSimpleTypes();
entity.nested.firstname = "fn";
entity.nested.age = 30;
entity.nested.displayName = "dp-name";
Map<CqlIdentifier, Object> sink = new LinkedHashMap<>();
mappingCassandraConverter.write(entity, sink);
assertThat(sink) //
.containsEntry(CqlIdentifier.fromCql("id"), "id-1") //
.containsEntry(CqlIdentifier.fromCql("prefixage"), 30) //
.containsEntry(CqlIdentifier.fromCql("prefixfirstname"), "fn") //
.doesNotContainKey(CqlIdentifier.fromCql("displayName"));
}
@Test // DATACASS-167
public void writeNullEmbeddedType() {
WithNullableEmbeddedType entity = new WithNullableEmbeddedType();
entity.id = "id-1";
entity.nested = null;
Map<CqlIdentifier, Object> sink = new LinkedHashMap<>();
mappingCassandraConverter.write(entity, sink);
assertThat(sink) //
.containsEntry(CqlIdentifier.fromCql("id"), "id-1") //
.doesNotContainKey(CqlIdentifier.fromCql("age")) //
.doesNotContainKey(CqlIdentifier.fromCql("firstname")) //
.doesNotContainKey(CqlIdentifier.fromCql("displayName"));
}
@Test // DATACASS-167
public void readEmbeddedType() {
Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT), column("age", 30, DataTypes.INT),
column("firstname", "fn", DataTypes.TEXT));
WithNullableEmbeddedType target = mappingCassandraConverter.read(WithNullableEmbeddedType.class, source);
assertThat(target.nested).isEqualTo(new EmbeddedWithSimpleTypes("fn", 30, null));
}
@Test // DATACASS-167
public void readPrefixedEmbeddedType() {
Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT), column("prefixage", 30, DataTypes.INT),
column("prefixfirstname", "fn", DataTypes.TEXT));
WithPrefixedNullableEmbeddedType target = mappingCassandraConverter.read(WithPrefixedNullableEmbeddedType.class, source);
assertThat(target.nested).isEqualTo(new EmbeddedWithSimpleTypes("fn", 30, null));
}
@Test // DATACASS-167
public void readEmbeddedTypeWhenSourceDoesNotContainValues() {
Row source = RowMockUtil.newRowMock(column("id", "id-1", DataTypes.TEXT));
WithNullableEmbeddedType target = mappingCassandraConverter.read(WithNullableEmbeddedType.class, source);
assertThat(target.nested).isNull();
}
}

View File

@@ -20,6 +20,7 @@ import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Collection;
import java.util.Collections;
@@ -36,12 +37,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.Element;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.Tuple;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
@@ -378,6 +379,28 @@ public class QueryMapperUnitTests {
this.mappingContext.getRequiredPersistentEntity(Person.class)));
}
@Test // DATACASS-167
public void shouldMapEmbeddedType() {
Filter filter = Filter.from(Criteria.where("nested.firstname").is("spring"));
Filter mappedObject = this.queryMapper.getMappedObject(filter,
this.mappingContext.getRequiredPersistentEntity(WithNullableEmbeddedType.class));
assertThat(mappedObject.iterator().next().getColumnName()).isEqualTo(ColumnName.from("firstname"));
}
@Test // DATACASS-167
public void shouldMapPrefixedEmbeddedType() {
Filter filter = Filter.from(Criteria.where("nested.firstname").is("spring"));
Filter mappedObject = this.queryMapper.getMappedObject(filter,
this.mappingContext.getRequiredPersistentEntity(WithPrefixedNullableEmbeddedType.class));
assertThat(mappedObject.iterator().next().getColumnName()).isEqualTo(ColumnName.from("prefixfirstname"));
}
static class Person {
@Id String id;
@@ -413,4 +436,27 @@ public class QueryMapperUnitTests {
enum State {
Active, Inactive;
}
@Data
static class WithNullableEmbeddedType {
@Id String id;
@Embedded.Nullable EmbeddedWithSimpleTypes nested;
}
@Data
static class WithPrefixedNullableEmbeddedType {
@Id String id;
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
}
@Data
static class EmbeddedWithSimpleTypes {
String firstname;
Integer age;
}
}

View File

@@ -35,7 +35,6 @@ import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.cql.Ordering;
@@ -44,16 +43,7 @@ import org.springframework.data.cassandra.core.cql.keyspace.ColumnSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification.ColumnFunction;
import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecification;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.Element;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.mapping.Table;
import org.springframework.data.cassandra.core.mapping.Tuple;
import org.springframework.data.cassandra.core.mapping.*;
import org.springframework.data.cassandra.domain.AllPossibleTypes;
import org.springframework.data.cassandra.support.UserDefinedTypeBuilder;
import org.springframework.data.mapping.MappingException;
@@ -76,6 +66,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*
* @author Matthew T. Adams
* @author Mark Paluch
* @author Christoph Strobl
*/
public class SchemaFactoryUnitTests {
@@ -827,4 +818,61 @@ public class SchemaFactoryUnitTests {
@Id String id;
Map<String, TupleValue> untyped;
}
@Table
@Data
static class TypeWithEmbedded {
@Id String id;
@Embedded.Nullable EmbeddedTpe name;
@Embedded.Nullable(prefix = "a") EmbeddedTpe alias;
@Indexed @Embedded.Nullable(prefix = "aego") EmbeddedTpe alterEgo;
}
@Data
static class EmbeddedTpe {
@Indexed String firstname;
String lastname;
}
@Test // DATACASS-167
public void createTableSpecificationShouldConsiderEmbeddedType() {
CreateTableSpecification specification = getCreateTableSpecificationFor(TypeWithEmbedded.class);
assertThat(specification).isNotNull();
assertThat(getColumn("firstname", specification)).isNotNull();
assertThat(getColumn("lastname", specification)).isNotNull();
assertThat(getColumn("afirstname", specification)).isNotNull();
assertThat(getColumn("alastname", specification)).isNotNull();
assertThat(getColumn("aegofirstname", specification)).isNotNull();
assertThat(getColumn("aegolastname", specification)).isNotNull();
}
@Test // DATACASS-167
public void createIndexSpecificationShouldConsiderEmbeddedType() {
List<CreateIndexSpecification> specifications = schemaFactory
.getCreateIndexSpecificationsFor(ctx.getRequiredPersistentEntity(TypeWithEmbedded.class));
CreateIndexSpecification firstname = getSpecificationFor("firstname", specifications);
assertThat(firstname.getColumnName()).isEqualTo(CqlIdentifier.fromCql("firstname"));
assertThat(firstname.getName()).isNull();
assertThat(firstname.getColumnFunction()).isEqualTo(ColumnFunction.NONE);
CreateIndexSpecification afirstname = getSpecificationFor("afirstname", specifications);
assertThat(afirstname.getColumnName()).isEqualTo(CqlIdentifier.fromCql("afirstname"));
assertThat(afirstname.getName()).isNull();
assertThat(afirstname.getColumnFunction()).isEqualTo(ColumnFunction.NONE);
CreateIndexSpecification aegofirstname = getSpecificationFor("aegofirstname", specifications);
assertThat(aegofirstname.getColumnName()).isEqualTo(CqlIdentifier.fromCql("aegofirstname"));
CreateIndexSpecification aegolastname = getSpecificationFor("aegolastname", specifications);
assertThat(aegolastname.getColumnName()).isEqualTo(CqlIdentifier.fromCql("aegolastname"));
}
}

View File

@@ -15,9 +15,11 @@
*/
package org.springframework.data.cassandra.core.convert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalTime;
import java.util.Collections;
@@ -31,12 +33,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.Element;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.Tuple;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
@@ -46,13 +49,11 @@ import org.springframework.data.cassandra.support.UserDefinedTypeBuilder;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.type.DataTypes;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Unit tests for {@link UpdateMapper}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class UpdateMapperUnitTests {
@@ -259,6 +260,26 @@ public class UpdateMapperUnitTests {
() -> this.updateMapper.getMappedObject(Update.empty().set("tuple.zip", "bar"), this.persistentEntity));
}
@Test // DATACASS-167
public void shouldMapEmbeddedEntity() {
Update update = this.updateMapper.getMappedObject(Update.empty().set("nested.firstname", "spring"),
mappingContext.getRequiredPersistentEntity(WithNullableEmbeddedType.class));
assertThat(update.getUpdateOperations()).hasSize(1);
assertThat(update.toString()).startsWith("firstname = 'spring'");
}
@Test // DATACASS-167
public void shouldMapPrefixedEmbeddedEntity() {
Update update = this.updateMapper.getMappedObject(Update.empty().set("nested.firstname", "spring"),
mappingContext.getRequiredPersistentEntity(WithPrefixedNullableEmbeddedType.class));
assertThat(update.getUpdateOperations()).hasSize(1);
assertThat(update.toString()).startsWith("prefixfirstname = 'spring'");
}
@SuppressWarnings("unused")
static class Person {
@@ -295,4 +316,30 @@ public class UpdateMapperUnitTests {
static class Manufacturer {
String name;
}
@Data
static class WithNullableEmbeddedType {
@Id String id;
@Embedded.Nullable EmbeddedWithSimpleTypes nested;
}
@Data
static class WithPrefixedNullableEmbeddedType {
@Id String id;
// @Indexed -> index for all properties of nested
@Embedded.Nullable(prefix = "prefix") EmbeddedWithSimpleTypes nested;
}
@Data
static class EmbeddedWithSimpleTypes {
@Indexed // single property index (IndexSpecificationFactory) | sassi index etc. :'(
String firstname;
Integer age;
}
}

View File

@@ -18,11 +18,13 @@ package org.springframework.data.cassandra.repository;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import lombok.Data;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -30,16 +32,19 @@ import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.config.SchemaAction;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.cql.generator.CreateIndexCqlGenerator;
import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification;
import org.springframework.data.cassandra.core.mapping.Embedded;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.Table;
import org.springframework.data.cassandra.core.query.CassandraPageRequest;
import org.springframework.data.cassandra.domain.AddressType;
import org.springframework.data.cassandra.domain.Person;
@@ -64,6 +69,7 @@ import com.datastax.oss.driver.api.core.CqlSession;
* Integration tests for query derivation through {@link PersonRepository}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@@ -77,18 +83,19 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC
@Override
protected Set<Class<?>> getInitialEntitySet() {
return Collections.singleton(Person.class);
return new HashSet<>(Arrays.asList(Person.class, PersonWithEmbedded.class));
}
@Override
public SchemaAction getSchemaAction() {
return SchemaAction.RECREATE_DROP_UNUSED;
return SchemaAction.CREATE;
}
}
@Autowired CassandraOperations template;
@Autowired CqlSession session;
@Autowired PersonRepository personRepository;
@Autowired EmbeddedPersonRepository personWithEmbeddedRepository;
private Person walter;
private Person skyler;
@@ -368,6 +375,20 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC
assertThat(personRepository.existsByLastname("Schrader")).isFalse();
}
@Test // DATACASS-167
public void derivedQueryOnPropertyOfEmbeddedEntity() {
PersonWithEmbedded source = new PersonWithEmbedded();
source.id = "id-1";
source.name = new Name();
source.name.firstname = "spring";
source.name.lastname = "data";
personWithEmbeddedRepository.save(source);
assertThat(personWithEmbeddedRepository.findByName_Firstname("spring")).isEqualTo(source);
}
/**
* @author Mark Paluch
*/
@@ -439,4 +460,28 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC
}
}
}
/**
* @author Christoph Strobl
*/
static interface EmbeddedPersonRepository extends CassandraRepository<PersonWithEmbedded, String> {
PersonWithEmbedded findByName_Firstname(String firstname);
}
@Table
@Data
static class PersonWithEmbedded {
@Id String id;
@Embedded.Nullable Name name;
}
@Data
static class Name {
@Indexed String firstname;
String lastname;
}
}

View File

@@ -21,9 +21,13 @@ import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.convert.SchemaFactory;
import org.springframework.data.cassandra.core.cql.SessionCallback;
import org.springframework.data.cassandra.core.cql.generator.CreateTableCqlGenerator;
import org.springframework.data.cassandra.core.cql.generator.CreateUserTypeCqlGenerator;
import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecification;
import org.springframework.data.cassandra.core.cql.keyspace.CreateUserTypeSpecification;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.mapping.EmbeddedEntityOperations;
import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
@@ -32,6 +36,7 @@ import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
* scenarios.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
public class SchemaTestUtils {
@@ -43,10 +48,36 @@ public class SchemaTestUtils {
*/
public static void potentiallyCreateTableFor(Class<?> entityClass, CassandraOperations operations) {
CassandraMappingContext mappingContext = operations.getConverter().getMappingContext();
CassandraPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entityClass);
potentiallyCreateTableFor(persistentEntity, operations, new SchemaFactory(operations.getConverter()));
}
/**
* Create a table and UDTs for {@code entityClass} if it not exists.
*
* @param entityClass must not be {@literal null}.
* @param operations must not be {@literal null}.
*/
public static void createTableAndTypes(Class<?> entityClass, CassandraOperations operations) {
CassandraMappingContext mappingContext = operations.getConverter().getMappingContext();
CassandraPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entityClass);
SchemaFactory schemaFactory = new SchemaFactory(operations.getConverter());
potentiallyCreateUdtFor(persistentEntity, operations, schemaFactory);
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) {
operations.getCqlOperations().execute((SessionCallback<Object>) session -> {
Optional<TableMetadata> table = session.getKeyspace().flatMap(it -> session.getMetadata().getKeyspace(it))
@@ -60,6 +91,32 @@ public class SchemaTestUtils {
});
}
private static void potentiallyCreateUdtFor(CassandraPersistentEntity<?> persistentEntity,
CassandraOperations operations, SchemaFactory schemaFactory) {
if (persistentEntity.isUserDefinedType()) {
CreateUserTypeSpecification udtspec = schemaFactory.getCreateUserTypeSpecificationFor(persistentEntity)
.ifNotExists();
operations.getCqlOperations().execute(CreateUserTypeCqlGenerator.toCql(udtspec));
} 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);
}
}
}
}
}
/**
* Truncate table for {@code entityClass}.
*

View File

@@ -344,6 +344,63 @@ 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.
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`).
[WARNING]
====
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`. +
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 User {
@PrimaryKey("user_id")
private String userId;
@Embedded(onEmpty = USE_NULL) <1>
UserName name;
}
public class UserName {
private String firstname;
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.
====
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.
[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.
[source, java]
----
public class MyEntity {
@Id
Integer id;
@Embedded.Nullable <1>
EmbeddedEntity embeddedEntity;
}
----
<1> Shortcut for `@Embedded(onEmpty = USE_NULL)`.
====
[[mapping.usage-annotations]]
=== Mapping Annotation Overview
@@ -443,6 +500,11 @@ include::../{example-root}/mapping/PersonWithIndexes.java[tags=class]
----
====
[NOTE]
====
The `@Indexed` annotation can be applied to single properties of embedded entities or along side with the `@Embedded` annotation, in which case all properties of the embedded are indexed.
====
CAUTION: Index creation on session initialization may have a severe performance impact on application startup.
include::./converters.adoc[]