DATACASS-546 - Consider user-type dependencies during schema drop.

We now consider user-type dependencies while dropping user-defined types. User types can utilize other user-defined types and dropping a type that is in use causes the drop operation to fail. We're introspecting user types reported by Cassandra and construct a dependency graph that is used to determine the proper drop order.
This commit is contained in:
Mark Paluch
2018-04-16 16:39:40 +02:00
parent c4be2852d2
commit 2e365cd5f2
8 changed files with 404 additions and 79 deletions

View File

@@ -157,7 +157,6 @@ public class CassandraPersistentEntitySchemaCreator {
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) {

View File

@@ -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<CassandraPersistentEntity<?>> userDefinedTypeEntities = mappingContext.getUserDefinedTypeEntities();
Set<CqlIdentifier> canRecreate = new HashSet<CqlIdentifier>();
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<UserType> userTypes = this.cassandraAdminOperations.getKeyspaceMetadata().getUserTypes();
if (canRecreate.contains(identifier)) {
cassandraAdminOperations.dropUserType(identifier);
} else if (dropUnused && !mappingContext.usesUserType(userType)) {
cassandraAdminOperations.dropUserType(identifier);
List<CqlIdentifier> 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<CqlIdentifier> getUserTypesToDrop(Collection<UserType> knownUserTypes) {
List<CqlIdentifier> toDrop = new ArrayList<CqlIdentifier>();
UserTypeDependencyGraphBuilder builder = new UserTypeDependencyGraphBuilder();
for (UserType userType : knownUserTypes) {
builder.addUserType(userType);
}
UserTypeDependencyGraph dependencyGraph = builder.build();
final Set<CqlIdentifier> globalSeen = new LinkedHashSet<CqlIdentifier>();
for (UserType userType : knownUserTypes) {
CqlIdentifier typeName = CqlIdentifier.cqlId(userType.getTypeName());
toDrop.addAll(dependencyGraph.getDropOrder(typeName, new Predicate<CqlIdentifier>() {
@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<CqlIdentifier, CqlIdentifier> dependencies = new LinkedMultiValueMap<CqlIdentifier, CqlIdentifier>();
/**
* Add {@link UserType} to the builder and inspect its dependencies.
*
* @param userType must not be {@literal null.}
*/
void addUserType(UserType userType) {
final Set<CqlIdentifier> seen = new LinkedHashSet<CqlIdentifier>();
visitTypes(userType, new Predicate<CqlIdentifier>() {
@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<CqlIdentifier, CqlIdentifier>(dependencies));
}
/**
* Visit a {@link UserType} and its fields.
*
* @param userType
* @param typeFilter
*/
private void visitTypes(UserType userType, final Predicate<CqlIdentifier> 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<DataType>() {
@Override
public void accept(DataType type) {
if (type instanceof UserType) {
addDependency((UserType) type, typeName, typeFilter);
}
}
});
}
}
private void addDependency(UserType userType, CqlIdentifier requiredBy, Predicate<CqlIdentifier> typeFilter) {
dependencies.add(CqlIdentifier.cqlId(userType.getTypeName()), requiredBy);
visitTypes(userType, typeFilter);
}
private static void doWithTypeArguments(DataType type, Consumer<DataType> 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<CqlIdentifier, CqlIdentifier> dependencies;
UserTypeDependencyGraph(MultiValueMap<CqlIdentifier, CqlIdentifier> 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<CqlIdentifier> getDropOrder(CqlIdentifier typeName, Predicate<CqlIdentifier> typeFilter) {
List<CqlIdentifier> toDrop = new ArrayList<CqlIdentifier>();
if (typeFilter.test(typeName)) {
List<CqlIdentifier> 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 <T>
*/
interface Predicate<T> {
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 <T>
*/
interface Consumer<T> {
void accept(T type);
}
}

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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<MoonType> moons;
UniverseType universeType;
}
@UserDefinedType
static class AstronautType {
String name;
}
@UserDefinedType
static class SpaceAgencyType {
List<AstronautType> astronauts;
}
}

View File

@@ -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<Class<?>>(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();
}
}

View File

@@ -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<MoonType> moons;
UniverseType universeType;
}
@UserDefinedType
static class AstronautType {
String name;
}
@UserDefinedType
static class SpaceAgencyType {
List<AstronautType> astronauts;
}
@Table
static class IndexedEntity {
@Id String id;
@Indexed String firstName;
}
@Table
static class Person {
@Id String id;
}
}

View File

@@ -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<Field> fields = new ArrayList<Field>();
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<Field> 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<Field> fields) {
try {
Constructor<UserType> 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);
}
}
}