From ca60ca64333c57fd68bcebdb850e1065968eb79f Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 28 Feb 2017 14:27:17 +0100 Subject: [PATCH] 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. --- ...assandraPersistentEntitySchemaCreator.java | 11 ++ .../mapping/BasicCassandraMappingContext.java | 112 ++++++++++++++---- ...ersistentEntitySchemaCreatorUnitTests.java | 97 ++++++++++++--- 3 files changed, 181 insertions(+), 39 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java index d01c0c130..5b9c0f71c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreator.java @@ -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 @@ -143,6 +144,16 @@ public class CassandraPersistentEntitySchemaCreator { return specifications; } + private Map> getEntitiesByTableName(Collection> entities) { + // TODO simplify by using Java 8 Streams API in 2.0.x + Map> byTableName = new HashMap>(); + + for (CassandraPersistentEntity entity : entities) { + byTableName.put(entity.getTableName(), entity); + } + return byTableName; + } + private void visitUserTypes(CassandraPersistentEntity entity, final Set seen) { entity.doWithProperties(new PropertyHandler() { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java index df995d2ef..e4d3c071a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java @@ -60,6 +60,7 @@ import com.datastax.driver.core.UserType; * @author Matthew T. Adams * @author Mark Paluch * @author John Blum + * @author Jens Schauder */ public class BasicCassandraMappingContext extends AbstractMappingContext, CassandraPersistentProperty> @@ -68,7 +69,7 @@ public class BasicCassandraMappingContext protected ApplicationContext context; protected CassandraPersistentEntityMetadataVerifier verifier = - new CompositeCassandraPersistentEntityMetadataVerifier(); + new CompositeCassandraPersistentEntityMetadataVerifier(); protected ClassLoader beanClassLoader; @@ -118,7 +119,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 +128,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); } }); } @@ -148,7 +148,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()); @@ -259,7 +259,7 @@ public class BasicCassandraMappingContext protected CassandraPersistentEntity createPersistentEntity(TypeInformation typeInformation) { UserDefinedType userDefinedType = AnnotatedElementUtils.findMergedAnnotation( - typeInformation.getType(), UserDefinedType.class); + typeInformation.getType(), UserDefinedType.class); CassandraPersistentEntity entity; @@ -302,13 +302,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); } @@ -389,24 +389,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)); } } @@ -434,8 +430,12 @@ public class BasicCassandraMappingContext entity.doWithProperties(new PropertyHandler() { @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) + ); } }); @@ -470,6 +470,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(); } @@ -478,10 +482,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())) { @@ -507,13 +513,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 @@ -530,4 +556,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()); + } + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java index d372b8e47..3fc4eb589 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java @@ -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,8 +40,9 @@ 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 { @@ -66,34 +68,87 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests { when(adminOperations.getCqlOperations()).thenReturn(operations); } - @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, adminOperations); + + schemaCreator.createUserTypes(false); + + verifyTypesGetCreatedInOrderFor( + "universetype", + "moontype" + ); + } + + @Test + public void createsCorrectTypeForSets(){ + + context.getPersistentEntity(PlanetType.class); + + CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(context, adminOperations); 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")); + 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, adminOperations); + + 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, adminOperations); + + 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; } @@ -110,4 +165,14 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests { Set moons; UniverseType universeType; } + + @UserDefinedType + static class AstronautType { + String name; + } + + @UserDefinedType + static class SpaceAgencyType { + List astronauts; + } }