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 784cb9077..785cb4b66 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 @@ -157,7 +157,6 @@ public class CassandraPersistentEntitySchemaCreator { 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) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java index 2a81110e6..3b25e1c26 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java @@ -15,18 +15,26 @@ */ package org.springframework.data.cassandra.core; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.TupleType; import com.datastax.driver.core.UserType; +import com.datastax.driver.core.UserType.Field; /** * Schema drop support for Cassandra based on {@link CassandraMappingContext} and {@link CassandraPersistentEntity}. @@ -85,23 +93,216 @@ public class CassandraPersistentEntitySchemaDropper { */ public void dropUserTypes(boolean dropUnused) { - KeyspaceMetadata keyspaceMetadata = cassandraAdminOperations.getKeyspaceMetadata(); - - Collection> userDefinedTypeEntities = mappingContext.getUserDefinedTypeEntities(); Set canRecreate = new HashSet(); - for (CassandraPersistentEntity userDefinedTypeEntity : userDefinedTypeEntities) { - canRecreate.add(userDefinedTypeEntity.getTableName()); + for (CassandraPersistentEntity entity : this.mappingContext.getUserDefinedTypeEntities()) { + canRecreate.add(entity.getTableName()); } - for (UserType userType : keyspaceMetadata.getUserTypes()) { - CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName()); + Collection userTypes = this.cassandraAdminOperations.getKeyspaceMetadata().getUserTypes(); - if (canRecreate.contains(identifier)) { - cassandraAdminOperations.dropUserType(identifier); - } else if (dropUnused && !mappingContext.usesUserType(userType)) { - cassandraAdminOperations.dropUserType(identifier); + List identifiers = getUserTypesToDrop(userTypes); + + for (CqlIdentifier identifier : identifiers) { + + if (canRecreate.contains(identifier) || (dropUnused && !mappingContext.usesUserType(identifier))) { + this.cassandraAdminOperations.dropUserType(identifier); } } } + + /** + * Create {@link List} of {@link CqlIdentifier} with User-Defined type names to drop considering dependencies between + * UDTs. + * + * @return {@link List} of {@link CqlIdentifier}. + */ + private List getUserTypesToDrop(Collection knownUserTypes) { + + List toDrop = new ArrayList(); + + UserTypeDependencyGraphBuilder builder = new UserTypeDependencyGraphBuilder(); + + for (UserType userType : knownUserTypes) { + builder.addUserType(userType); + } + + UserTypeDependencyGraph dependencyGraph = builder.build(); + + final Set globalSeen = new LinkedHashSet(); + + for (UserType userType : knownUserTypes) { + + CqlIdentifier typeName = CqlIdentifier.cqlId(userType.getTypeName()); + toDrop.addAll(dependencyGraph.getDropOrder(typeName, new Predicate() { + @Override + public boolean test(CqlIdentifier type) { + return globalSeen.add(type); + } + })); + } + + return toDrop; + } + + /** + * Builder for {@link UserTypeDependencyGraph}. Introspects {@link UserType} for dependencies to other user types to + * build a dependency graph between user types. + * + * @author Mark Paluch + * @since 2.0.7 + */ + static class UserTypeDependencyGraphBuilder { + + // Maps user types to other types they are referenced in. + private final MultiValueMap dependencies = new LinkedMultiValueMap(); + + /** + * Add {@link UserType} to the builder and inspect its dependencies. + * + * @param userType must not be {@literal null.} + */ + void addUserType(UserType userType) { + + final Set seen = new LinkedHashSet(); + + visitTypes(userType, new Predicate() { + @Override + public boolean test(CqlIdentifier type) { + return seen.add(type); + } + }); + } + + /** + * Build the {@link UserTypeDependencyGraph}. + * + * @return the {@link UserTypeDependencyGraph}. + */ + UserTypeDependencyGraph build() { + return new UserTypeDependencyGraph(new LinkedMultiValueMap(dependencies)); + } + + /** + * Visit a {@link UserType} and its fields. + * + * @param userType + * @param typeFilter + */ + private void visitTypes(UserType userType, final Predicate typeFilter) { + + final CqlIdentifier typeName = CqlIdentifier.cqlId(userType.getTypeName()); + + if (!typeFilter.test(typeName)) { + return; + } + + for (Field field : userType) { + + if (field.getType() instanceof UserType) { + + addDependency((UserType) field.getType(), typeName, typeFilter); + + return; + } + + doWithTypeArguments(field.getType(), new Consumer() { + @Override + public void accept(DataType type) { + if (type instanceof UserType) { + addDependency((UserType) type, typeName, typeFilter); + } + } + }); + } + } + + private void addDependency(UserType userType, CqlIdentifier requiredBy, Predicate typeFilter) { + + dependencies.add(CqlIdentifier.cqlId(userType.getTypeName()), requiredBy); + + visitTypes(userType, typeFilter); + } + + private static void doWithTypeArguments(DataType type, Consumer callback) { + + for (DataType nested : type.getTypeArguments()) { + callback.accept(nested); + doWithTypeArguments(nested, callback); + } + + if (type instanceof TupleType) { + + TupleType tupleType = (TupleType) type; + + for (DataType nested : tupleType.getComponentTypes()) { + callback.accept(nested); + doWithTypeArguments(nested, callback); + } + } + } + } + + /** + * Dependency graph representing user type field dependencies to other user types. + * + * @author Mark Paluch + * @since 1.5.12 + */ + static class UserTypeDependencyGraph { + + private final MultiValueMap dependencies; + + UserTypeDependencyGraph(MultiValueMap dependencies) { + this.dependencies = dependencies; + } + + /** + * Returns the names of user types in the order they need to be dropped including type {@code typeName}. + * + * @param typeName + * @param typeFilter + * @return + */ + List getDropOrder(CqlIdentifier typeName, Predicate typeFilter) { + + List toDrop = new ArrayList(); + + if (typeFilter.test(typeName)) { + + List dependants = dependencies.get(typeName); + if (dependants != null) { + + for (CqlIdentifier dependant : dependants) { + toDrop.addAll(getDropOrder(dependant, typeFilter)); + } + } + + toDrop.add(typeName); + } + + return toDrop; + } + } + + /** + * Represents a predicate (boolean-valued function) of one argument. + * + * @param + */ + interface Predicate { + + boolean test(T type); + } + + /** + * Represents an operation that accepts a single input argument and returns no result. Unlike most other functional + * interfaces, {@code Consumer} is expected to operate via side-effects. + * + * @param + */ + interface Consumer { + + void accept(T type); + } } 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 6ed9d50ba..77723ee0f 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 @@ -342,10 +342,15 @@ public class BasicCassandraMappingContext */ @Override public boolean usesUserType(final UserType userType) { + return usesUserType(CqlIdentifier.cqlId(userType.getTypeName())); + } - CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName()); - - return (hasMappedUserType(identifier) || hasReferencedUserType(identifier)); + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#usesUserType(CqlIdentifier) + */ + @Override + public boolean usesUserType(CqlIdentifier userType) { + return (hasMappedUserType(userType) || hasReferencedUserType(userType)); } private boolean hasReferencedUserType(final CqlIdentifier identifier) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java index f67f08ebf..93b93e7f9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java @@ -21,6 +21,7 @@ import com.datastax.driver.core.DataType; import com.datastax.driver.core.TableMetadata; import com.datastax.driver.core.UserType; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; import org.springframework.cassandra.core.keyspace.CreateUserTypeSpecification; import org.springframework.data.cassandra.convert.CustomConversions; @@ -113,6 +114,15 @@ public interface CassandraMappingContext */ boolean usesUserType(UserType userType); + /** + * Returns whether this mapping context has any entities using the given user type. + * + * @param userType must not be {@literal null}. + * @return {@literal true} is this {@literal UserType} is used. + * @since 1.5.12 + */ + boolean usesUserType(CqlIdentifier userType); + /** * Returns the existing {@link CassandraPersistentEntity} for the given {@link Class}. If it is not yet known to this * {@link CassandraMappingContext}, an {@link IllegalArgumentException} is thrown. 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 27254c65a..cef39e698 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 @@ -17,9 +17,6 @@ package org.springframework.data.cassandra.core; import static org.mockito.Mockito.*; -import java.util.List; -import java.util.Set; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,7 +26,6 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; -import org.springframework.data.cassandra.mapping.UserDefinedType; import org.springframework.data.cassandra.mapping.UserTypeResolver; import com.datastax.driver.core.UserType; @@ -41,7 +37,7 @@ import com.datastax.driver.core.UserType; * @author Jens Schauder */ @RunWith(MockitoJUnitRunner.class) -public class CassandraPersistentEntitySchemaCreatorUnitTests { +public class CassandraPersistentEntitySchemaCreatorUnitTests extends CassandraPersistentEntitySchemaTestSupport { @Mock CassandraAdminOperations operations; @@ -124,31 +120,4 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests { inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE " + typename)); } } - - @UserDefinedType - static class UniverseType { - String name; - } - - @UserDefinedType - static class MoonType { - UniverseType universeType; - } - - @UserDefinedType - static class PlanetType { - - Set moons; - UniverseType universeType; - } - - @UserDefinedType - static class AstronautType { - String name; - } - - @UserDefinedType - static class SpaceAgencyType { - List astronauts; - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java index f5a077e4a..f04e79f17 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java @@ -17,8 +17,6 @@ package org.springframework.data.cassandra.core; import static org.mockito.Mockito.*; -import lombok.Data; - import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -26,18 +24,19 @@ import java.util.HashSet; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.cassandra.core.cql.CqlIdentifier; -import org.springframework.data.annotation.Id; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; -import org.springframework.data.cassandra.mapping.Table; -import org.springframework.data.cassandra.mapping.UserDefinedType; import org.springframework.data.cassandra.mapping.UserTypeResolver; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.TableMetadata; import com.datastax.driver.core.UserType; +import org.springframework.data.cassandra.repository.support.UserTypeBuilder; /** * Unit tests for {@link CassandraPersistentEntitySchemaDropper}. @@ -46,13 +45,14 @@ import com.datastax.driver.core.UserType; */ @SuppressWarnings("unchecked") @RunWith(MockitoJUnitRunner.class) -public class CassandraPersistentEntitySchemaDropperUnitTests { +public class CassandraPersistentEntitySchemaDropperUnitTests extends CassandraPersistentEntitySchemaTestSupport { @Mock CassandraAdminOperations operations; @Mock KeyspaceMetadata metadata; - @Mock UserType universetype; - @Mock UserType moontype; - @Mock UserType planettype; + UserType universetype = UserTypeBuilder.forName("universetype").withField("name", DataType.varchar()).build(); + UserType moontype = UserTypeBuilder.forName("moontype").withField("universeType", universetype).build(); + UserType planettype = UserTypeBuilder.forName("planettype").withField("moonType", DataType.set(moontype)) + .withField("universeType", universetype).build(); @Mock TableMetadata person; @Mock TableMetadata contact; @@ -70,18 +70,12 @@ public class CassandraPersistentEntitySchemaDropperUnitTests { }); when(operations.getKeyspaceMetadata()).thenReturn(metadata); - when(universetype.getTypeName()).thenReturn("universetype"); - when(moontype.getTypeName()).thenReturn("moontype"); - when(planettype.getTypeName()).thenReturn("planettype"); when(person.getName()).thenReturn("person"); when(contact.getName()).thenReturn("contact"); } - @Test // DATACASS-355 - public void shouldDropTypes() throws Exception { - - context.setInitialEntitySet(new HashSet>(Arrays.asList(MoonType.class, UniverseType.class))); - context.afterPropertiesSet(); + @Test // DATACASS-355, DATACASS-546 + public void shouldDropTypesInOrderOfDependencies() throws Exception { when(metadata.getUserTypes()).thenReturn(Arrays.asList(universetype, moontype, planettype)); @@ -90,11 +84,7 @@ public class CassandraPersistentEntitySchemaDropperUnitTests { schemaDropper.dropUserTypes(true); - verify(operations).dropUserType(CqlIdentifier.cqlId("universetype")); - verify(operations).dropUserType(CqlIdentifier.cqlId("moontype")); - verify(operations).dropUserType(CqlIdentifier.cqlId("planettype")); - verify(operations).getKeyspaceMetadata(); - verifyNoMoreInteractions(operations); + verifyTypesGetDroppedInOrderFor("planettype", "moontype", "universetype"); } @Test // DATACASS-355 @@ -153,18 +143,15 @@ public class CassandraPersistentEntitySchemaDropperUnitTests { verifyNoMoreInteractions(operations); } - @UserDefinedType - @Data - static class UniverseType {} + private void verifyTypesGetDroppedInOrderFor(String... typenames) { - @UserDefinedType - static class MoonType {} + InOrder inOrder = Mockito.inOrder(operations); - @UserDefinedType - static class PlanetType {} + for (String typename : typenames) { + inOrder.verify(operations).dropUserType(CqlIdentifier.cqlId(typename)); + } - @Table - static class Person { - @Id String id; + inOrder.verifyNoMoreInteractions(); } + } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaTestSupport.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaTestSupport.java new file mode 100644 index 000000000..18af41b10 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaTestSupport.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 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 + * + * http://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; + +import java.util.List; +import java.util.Set; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Indexed; +import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.cassandra.mapping.UserDefinedType; + +/** + * Support class for schema creation/drop tests. + * + * @author Mark Paluch + */ +public abstract class CassandraPersistentEntitySchemaTestSupport { + + @UserDefinedType + static class UniverseType { + String name; + } + + @UserDefinedType + static class MoonType { + UniverseType universeType; + } + + @UserDefinedType + static class PlanetType { + + Set moons; + UniverseType universeType; + } + + @UserDefinedType + static class AstronautType { + String name; + } + + @UserDefinedType + static class SpaceAgencyType { + List astronauts; + } + + @Table + static class IndexedEntity { + + @Id String id; + @Indexed String firstName; + } + + @Table + static class Person { + @Id String id; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/UserTypeBuilder.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/UserTypeBuilder.java new file mode 100644 index 000000000..76b72fd94 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/UserTypeBuilder.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 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 + * + * http://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.repository.support; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.cassandra.core.cql.CqlIdentifier; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.UserType; +import com.datastax.driver.core.UserType.Field; + +/** + * @author Mark Paluch + */ +public class UserTypeBuilder { + + private final CqlIdentifier typeName; + private List fields = new ArrayList(); + + private UserTypeBuilder(CqlIdentifier typeName) { + this.typeName = typeName; + } + + public static UserTypeBuilder forName(String typeName) { + return forName(CqlIdentifier.cqlId(typeName)); + } + + public static UserTypeBuilder forName(CqlIdentifier typeName) { + return new UserTypeBuilder(typeName); + } + + public UserTypeBuilder withField(String fieldName, DataType dataType) { + this.fields.add(createField(fieldName, dataType)); + return this; + } + + public UserType build() { + return createUserType(this.typeName.getUnquoted(), fields); + } + + private Field createField(String fieldName, DataType dataType) { + + try { + Constructor constructor = Field.class.getDeclaredConstructor(String.class, DataType.class); + constructor.setAccessible(true); + return constructor.newInstance(fieldName, dataType); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private UserType createUserType(String typeName, Collection fields) { + + try { + Constructor constructor = UserType.class.getDeclaredConstructor(String.class, String.class, + Collection.class, ProtocolVersion.class, CodecRegistry.class); + constructor.setAccessible(true); + return constructor.newInstance(typeName, typeName, fields, ProtocolVersion.NEWEST_SUPPORTED, + CodecRegistry.DEFAULT_INSTANCE); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +}