DATACASS-406 - Support nested @UserDefinedType for schema creation.

Works for simple user defined types and those used in Lists or Sets.

It does not work yet for maps and other collection-like types.

Related tickets: DATACASS-409.

Original pull request: #100.
This commit is contained in:
Jens Schauder
2017-02-28 14:27:17 +01:00
committed by Mark Paluch
parent 9d5b05b36c
commit 3388c673d6
3 changed files with 187 additions and 52 deletions

View File

@@ -41,6 +41,7 @@ import org.springframework.util.Assert;
* This class generates CQL to create user types (UDT) and tables.
*
* @author Mark Paluch
* @author Jens Schauder
* @since 1.5
* @see org.springframework.data.cassandra.mapping.Table
* @see org.springframework.data.cassandra.mapping.UserDefinedType
@@ -127,12 +128,7 @@ public class CassandraPersistentEntitySchemaCreator {
Collection<? extends CassandraPersistentEntity<?>> entities = new ArrayList<CassandraPersistentEntity<?>>(
mappingContext.getUserDefinedTypeEntities());
// TODO simplify by using Java 8 Streams API in 2.0.x
Map<CqlIdentifier, CassandraPersistentEntity<?>> byTableName = new HashMap<CqlIdentifier, CassandraPersistentEntity<?>>();
for (CassandraPersistentEntity<?> entity : entities) {
byTableName.put(entity.getTableName(), entity);
}
Map<CqlIdentifier, CassandraPersistentEntity<?>> byTableName = getEntitiesByTableName(entities);
List<CreateUserTypeSpecification> specifications = new ArrayList<CreateUserTypeSpecification>();
@@ -140,6 +136,7 @@ public class CassandraPersistentEntitySchemaCreator {
Set<CqlIdentifier> created = new HashSet<CqlIdentifier>();
for (CassandraPersistentEntity<?> entity : entities) {
Set<CqlIdentifier> seen = new LinkedHashSet<CqlIdentifier>();
seen.add(entity.getTableName());
@@ -158,6 +155,16 @@ public class CassandraPersistentEntitySchemaCreator {
return specifications;
}
private Map<CqlIdentifier, CassandraPersistentEntity<?>> getEntitiesByTableName(Collection<? extends CassandraPersistentEntity<?>> entities) {
// TODO simplify by using Java 8 Streams API in 2.0.x
Map<CqlIdentifier, CassandraPersistentEntity<?>> byTableName = new HashMap<CqlIdentifier, CassandraPersistentEntity<?>>();
for (CassandraPersistentEntity<?> entity : entities) {
byTableName.put(entity.getTableName(), entity);
}
return byTableName;
}
private void visitUserTypes(CassandraPersistentEntity<?> entity, final Set<CqlIdentifier> seen) {
entity.doWithProperties(new PropertyHandler<CassandraPersistentProperty>() {

View File

@@ -29,11 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.TableMetadata;
import com.datastax.driver.core.UserType;
import org.springframework.beans.BeansException;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.cassandra.core.keyspace.CreateTableSpecification;
@@ -52,6 +47,10 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.TableMetadata;
import com.datastax.driver.core.UserType;
/**
* Default implementation of a {@link MappingContext} for Cassandra using {@link CassandraPersistentEntity} and
* {@link CassandraPersistentProperty} as primary abstractions.
@@ -60,6 +59,7 @@ import org.springframework.util.StringUtils;
* @author Matthew T. Adams
* @author Mark Paluch
* @author John Blum
* @author Jens Schauder
*/
public class BasicCassandraMappingContext
extends AbstractMappingContext<CassandraPersistentEntity<?>, CassandraPersistentProperty>
@@ -68,7 +68,7 @@ public class BasicCassandraMappingContext
protected ApplicationContext context;
protected CassandraPersistentEntityMetadataVerifier verifier =
new CompositeCassandraPersistentEntityMetadataVerifier();
new CompositeCassandraPersistentEntityMetadataVerifier();
protected ClassLoader beanClassLoader;
@@ -118,7 +118,7 @@ public class BasicCassandraMappingContext
CassandraPersistentEntity<?> entity = getPersistentEntity(entityClass);
Assert.state(entity != null,
String.format("Unknown persistent entity class name [%s]", entityClassName));
String.format("Unknown persistent entity class name [%s]", entityClassName));
String entityTableName = entityMapping.getTableName();
@@ -127,10 +127,9 @@ public class BasicCassandraMappingContext
}
processMappingOverrides(entity, entityMapping);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(
String.format("Unknown persistent entity name [%s]", entityClassName), e);
String.format("Unknown persistent entity name [%s]", entityClassName), e);
}
}
}
@@ -151,7 +150,7 @@ public class BasicCassandraMappingContext
CassandraPersistentProperty property = entity.getPersistentProperty(mapping.getPropertyName());
Assert.notNull(property, String.format("Entity class [%s] has no persistent property named [%s]",
entity.getType().getName(), mapping.getPropertyName()));
entity.getType().getName(), mapping.getPropertyName()));
boolean forceQuote = Boolean.valueOf(mapping.getForceQuote());
@@ -262,7 +261,7 @@ public class BasicCassandraMappingContext
protected <T> CassandraPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
UserDefinedType userDefinedType = AnnotatedElementUtils.findMergedAnnotation(
typeInformation.getType(), UserDefinedType.class);
typeInformation.getType(), UserDefinedType.class);
CassandraPersistentEntity<T> entity;
@@ -305,13 +304,13 @@ public class BasicCassandraMappingContext
@Override
public CassandraPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor,
CassandraPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
CassandraPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
return createPersistentProperty(field, descriptor, owner, (CassandraSimpleTypeHolder) simpleTypeHolder);
}
public CassandraPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor,
CassandraPersistentEntity<?> owner, CassandraSimpleTypeHolder simpleTypeHolder) {
CassandraPersistentEntity<?> owner, CassandraSimpleTypeHolder simpleTypeHolder) {
return new BasicCassandraPersistentProperty(field, descriptor, owner, simpleTypeHolder, userTypeResolver);
}
@@ -394,24 +393,20 @@ public class BasicCassandraMappingContext
public void doWithPersistentProperty(CassandraPersistentProperty primaryKeyProperty) {
if (primaryKeyProperty.isPartitionKeyColumn()) {
specification.partitionKeyColumn(primaryKeyProperty.getColumnName(),
getDataType(primaryKeyProperty));
}
else { // it's a cluster column
getDataType(primaryKeyProperty));
} else { // it's a cluster column
specification.clusteredKeyColumn(primaryKeyProperty.getColumnName(),
getDataType(primaryKeyProperty), primaryKeyProperty.getPrimaryKeyOrdering());
getDataType(primaryKeyProperty), primaryKeyProperty.getPrimaryKeyOrdering());
}
}
});
} else {
if (property.isIdProperty() || property.isPartitionKeyColumn()) {
specification.partitionKeyColumn(property.getColumnName(), getDataType(property));
}
else if (property.isClusterKeyColumn()) {
} else if (property.isClusterKeyColumn()) {
specification.clusteredKeyColumn(property.getColumnName(), getDataType(property),
property.getPrimaryKeyOrdering());
}
else {
property.getPrimaryKeyOrdering());
} else {
specification.column(property.getColumnName(), getDataType(property));
}
}
@@ -439,8 +434,12 @@ public class BasicCassandraMappingContext
entity.doWithProperties(new PropertyHandler<CassandraPersistentProperty>() {
@Override
public void doWithPersistentProperty(CassandraPersistentProperty property) {
specification.field(property.getColumnName(), getDataType(property));
public void doWithPersistentProperty(final CassandraPersistentProperty property) {
specification.field(
property.getColumnName(),
getDataTypeWithUserTypeFactory(property, UserDataTypeProvider.Fake)
);
}
});
@@ -475,6 +474,10 @@ public class BasicCassandraMappingContext
@Override
public DataType getDataType(CassandraPersistentProperty property) {
return getDataTypeWithUserTypeFactory(property, UserDataTypeProvider.Simple);
}
private DataType getDataTypeWithUserTypeFactory(CassandraPersistentProperty property, UserDataTypeProvider userDataTypeProvider) {
if (property.isCompositePrimaryKey()) {
return property.getDataType();
}
@@ -483,10 +486,12 @@ public class BasicCassandraMappingContext
return property.getDataType();
}
CassandraPersistentEntity<?> persistentEntity = getPersistentEntity(property.getType());
CassandraPersistentEntity<?> persistentEntity = getPersistentEntity(property.getActualType());
if (persistentEntity != null && persistentEntity.isUserDefinedType()) {
return persistentEntity.getUserType();
DataType elementType = getUserDataType(property, userDataTypeProvider, persistentEntity);
if (elementType != null) return elementType;
}
if (customConversions.hasCustomWriteTarget(property.getType())) {
@@ -512,13 +517,33 @@ public class BasicCassandraMappingContext
return property.getDataType();
}
private DataType getUserDataType(CassandraPersistentProperty property, UserDataTypeProvider userDataTypeProvider, CassandraPersistentEntity<?> persistentEntity) {
DataType elementType = userDataTypeProvider.get(persistentEntity);
if (property.isCollectionLike()) {
if (Set.class.isAssignableFrom(property.getType())) {
return DataType.set(elementType);
}
if (List.class.isAssignableFrom(property.getType())) {
return DataType.list(elementType);
}
}
if (!property.isCollectionLike() && !property.isMapLike()) {
return elementType;
}
return null;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getDataType(java.lang.Class)
*/
@Override
public DataType getDataType(Class<?> type) {
return (customConversions.hasCustomWriteTarget(type)
? getDataTypeFor(customConversions.getCustomWriteTarget(type)) : getDataTypeFor(type));
? getDataTypeFor(customConversions.getCustomWriteTarget(type)) : getDataTypeFor(type));
}
@Override
@@ -535,4 +560,44 @@ public class BasicCassandraMappingContext
public boolean contains(Class<?> type) {
return entitiesByType.containsKey(type);
}
enum UserDataTypeProvider {
Simple {
@Override
public DataType get(CassandraPersistentEntity<?> entity) {
return entity.getUserType();
}
},
Fake {
@Override
public DataType get(CassandraPersistentEntity<?> entity) {
return new FakeUserType(entity.getTableName());
}
};
abstract DataType get(CassandraPersistentEntity<?> entity);
}
static class FakeUserType extends DataType {
private final CqlIdentifier type;
protected FakeUserType(CqlIdentifier type) {
super(Name.UDT);
this.type = type;
}
@Override
public boolean isFrozen() {
return false;
}
@Override
public String toString() {
return String.format("frozen<%s>", type.toCql());
}
}
}

View File

@@ -19,6 +19,7 @@ import static org.mockito.Mockito.*;
import lombok.Data;
import java.util.List;
import java.util.Set;
import org.junit.Before;
@@ -39,7 +40,8 @@ import com.datastax.driver.core.UserType;
/**
* Unit tests for {@link CassandraPersistentEntitySchemaCreator}.
*
* @author Mark Paluch.
* @author Mark Paluch
* @author Jens Schauder
*/
@RunWith(MockitoJUnitRunner.class)
public class CassandraPersistentEntitySchemaCreatorUnitTests {
@@ -62,34 +64,85 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests {
});
}
@Test // DATACASS-172
public void shouldCreateTypesInOrder() {
@Test
public void createsCorrectTypeForSimpleTypes(){
context.getPersistentEntity(MoonType.class);
context.getPersistentEntity(PlanetType.class);
context.getPersistentEntity(UniverseType.class);
when(metadata.getUserType("universetype")).thenReturn(universetype);
when(metadata.getUserType("moontype")).thenReturn(moontype);
CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(context,
operations);
CassandraPersistentEntitySchemaCreator schemaCreator =
new CassandraPersistentEntitySchemaCreator(context, operations);
schemaCreator.createUserTypes(false);
verify(operations).execute(Mockito.contains("CREATE TYPE universetype"));
verify(operations).execute(Mockito.contains("CREATE TYPE moontype"));
verify(operations).execute(Mockito.contains("CREATE TYPE planettype"));
verifyTypesGetCreatedInOrderFor(
"universetype",
"moontype"
);
}
@Test
public void createsCorrectTypeForSets(){
context.getPersistentEntity(PlanetType.class);
CassandraPersistentEntitySchemaCreator schemaCreator =
new CassandraPersistentEntitySchemaCreator(context, operations);
schemaCreator.createUserTypes(false);
verify(operations).execute(matches("CREATE TYPE planettype .* set<.*moontype>.*"));
verifyTypesGetCreatedInOrderFor(
"universetype",
"moontype",
"planettype"
);
}
@Test
public void createsCorrectTypeForLists(){
context.getPersistentEntity(SpaceAgencyType.class);
CassandraPersistentEntitySchemaCreator schemaCreator =
new CassandraPersistentEntitySchemaCreator(context, operations);
schemaCreator.createUserTypes(false);
verify(operations).execute(matches("CREATE TYPE spaceagencytype .* list<.*astronauttype>.*"));
verifyTypesGetCreatedInOrderFor(
"astronauttype",
"spaceagencytype"
);
}
@Test
public void createsCorrectTypesForNestedTypes(){
context.getPersistentEntity(PlanetType.class);
CassandraPersistentEntitySchemaCreator schemaCreator =
new CassandraPersistentEntitySchemaCreator(context, operations);
schemaCreator.createUserTypes(false);
verifyTypesGetCreatedInOrderFor(
"universetype",
"moontype",
"planettype"
);
}
private void verifyTypesGetCreatedInOrderFor(String ... typenames) {
InOrder inOrder = Mockito.inOrder(operations);
inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE universetype"));
inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE moontype"));
inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE planettype"));
for (String typename : typenames) {
inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE " + typename));
}
}
@UserDefinedType
@Data
static class UniverseType {
String name;
}
@@ -106,4 +159,14 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests {
Set<MoonType> moons;
UniverseType universeType;
}
@UserDefinedType
static class AstronautType {
String name;
}
@UserDefinedType
static class SpaceAgencyType {
List<AstronautType> astronauts;
}
}