From 74943ca754dc1caeb7f4b0dc0d8d1a94f45f3fc9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 6 Jan 2017 11:03:18 +0100 Subject: [PATCH] DATACASS-355 - Drop tables before dropping user types. We now drop Cassandra tables before dropping user types as user types cannot be removed when they are used in table definitions. Schema creation and schema drops are now split into two classes. --- .../config/CassandraSessionFactoryBean.java | 17 +- ...assandraPersistentEntitySchemaCreator.java | 75 +------- ...assandraPersistentEntitySchemaDropper.java | 107 +++++++++++ ...ersistentEntitySchemaCreatorUnitTests.java | 11 +- ...ersistentEntitySchemaDropperUnitTests.java | 181 ++++++++++++++++++ .../cdi/CassandraOperationsProducer.java | 15 +- 6 files changed, 326 insertions(+), 80 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java index 3ae3c0029..c3f04dae4 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraSessionFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors + * Copyright 2013-2017 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.cassandra.config; import org.springframework.cassandra.config.CassandraCqlSessionFactoryBean; @@ -21,6 +20,7 @@ import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.core.CassandraPersistentEntitySchemaCreator; +import org.springframework.data.cassandra.core.CassandraPersistentEntitySchemaDropper; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.util.Assert; @@ -97,8 +97,17 @@ public class CassandraSessionFactoryBean extends CassandraCqlSessionFactoryBean CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator( getMappingContext(), getCassandraAdminOperations()); - schemaCreator.createUserTypes(drop, dropUnused, ifNotExists); - schemaCreator.createTables(drop, dropUnused, ifNotExists); + if (drop) { + + CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper( + getMappingContext(), getCassandraAdminOperations()); + + schemaDropper.dropTables(dropUnused); + schemaDropper.dropUserTypes(dropUnused); + } + + schemaCreator.createUserTypes(ifNotExists); + schemaCreator.createTables(ifNotExists); } /* (non-Javadoc) */ 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 87e090acf..9b1fa858a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -25,10 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import com.datastax.driver.core.KeyspaceMetadata; -import com.datastax.driver.core.TableMetadata; -import com.datastax.driver.core.UserType; - import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.cql.generator.CreateTableCqlGenerator; import org.springframework.cassandra.core.cql.generator.CreateUserTypeCqlGenerator; @@ -42,7 +38,7 @@ import org.springframework.util.Assert; /** * Schema creation support for Cassandra based on {@link CassandraMappingContext} and {@link CassandraPersistentEntity}. - * This class generates CQL to drop, recreate and create user types (UDT) and tables. + * This class generates CQL to create user types (UDT) and tables. * * @author Mark Paluch * @since 1.5 @@ -73,18 +69,11 @@ public class CassandraPersistentEntitySchemaCreator { } /** - * Create user types. Can drop types and drop unused types. + * Create tables from types known to {@link CassandraMappingContext}. * - * @param dropTables {@literal true} to drop tables before creation. - * @param dropUnused {@literal true} to drop unused tables before creation. Table usage is determined by existing - * table mappings. * @param ifNotExists {@literal true} to create tables using {@code IF NOT EXISTS}. */ - public void createTables(boolean dropTables, boolean dropUnused, boolean ifNotExists) { - - if (dropTables) { - dropTables(dropUnused); - } + public void createTables(boolean ifNotExists) { List specifications = createTableSpecifications(ifNotExists); @@ -97,7 +86,7 @@ public class CassandraPersistentEntitySchemaCreator { protected List createTableSpecifications(boolean ifNotExists) { Collection> entities = new ArrayList>( - mappingContext.getTableEntities()); + mappingContext.getTableEntities()); List specifications = new ArrayList(); @@ -108,32 +97,12 @@ public class CassandraPersistentEntitySchemaCreator { return specifications; } - /* (non-Javadoc) */ - private void dropTables(boolean dropUnused) { - - KeyspaceMetadata keyspaceMetadata = cassandraAdminOperations.getKeyspaceMetadata(); - - for (TableMetadata table : keyspaceMetadata.getTables()) { - if (dropUnused || mappingContext.usesTable(table)) { - cassandraAdminOperations.dropTable(CqlIdentifier.cqlId(table.getName())); - } - } - } - /** - * Create user types. Can drop types and drop unused types. + * Create user types from types known to {@link CassandraMappingContext}. * - * @param dropUserTypes {@literal true} to drop types before creation. - * @param dropUnused {@literal true} to drop unused types before creation. Type usage is determined from existing - * mapped {@link org.springframework.data.cassandra.mapping.UserDefinedType}s and UDT names on field - * specifications. * @param ifNotExists {@literal true} to create types using {@code IF NOT EXISTS}. */ - public void createUserTypes(boolean dropUserTypes, boolean dropUnused, boolean ifNotExists) { - - if (dropUserTypes) { - dropUserTypes(dropUnused); - } + public void createUserTypes(boolean ifNotExists) { List specifications = createUserTypeSpecifications(ifNotExists); @@ -149,8 +118,7 @@ public class CassandraPersistentEntitySchemaCreator { mappingContext.getUserDefinedTypeEntities()); // TODO simplify by using Java 8 Streams API in 2.0.x - Map> byTableName = - new HashMap>(); + Map> byTableName = new HashMap>(); for (CassandraPersistentEntity entity : entities) { byTableName.put(entity.getTableName(), entity); @@ -172,8 +140,8 @@ public class CassandraPersistentEntitySchemaCreator { for (CqlIdentifier identifier : ordered) { if (created.add(identifier)) { - specifications.add(mappingContext.getCreateUserTypeSpecificationFor(byTableName.get(identifier)) - .ifNotExists(ifNotExists)); + specifications.add( + mappingContext.getCreateUserTypeSpecificationFor(byTableName.get(identifier)).ifNotExists(ifNotExists)); } } } @@ -197,27 +165,4 @@ public class CassandraPersistentEntitySchemaCreator { } }); } - - /* (non-Javadoc) */ - private 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 (UserType userType : keyspaceMetadata.getUserTypes()) { - CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName()); - - if (canRecreate.contains(identifier)) { - cassandraAdminOperations.dropUserType(identifier); - } else if (dropUnused && !mappingContext.usesUserType(userType)) { - cassandraAdminOperations.dropUserType(identifier); - } - } - } } 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 new file mode 100644 index 000000000..8b05fda65 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 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.Collection; +import java.util.HashSet; +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 com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.UserType; + +/** + * Schema drop support for Cassandra based on {@link CassandraMappingContext} and {@link CassandraPersistentEntity}. + * This class generates CQL to drop user types (UDT) and tables. + * + * @author Mark Paluch + * @since 1.5 + * @see org.springframework.data.cassandra.mapping.Table + * @see org.springframework.data.cassandra.mapping.UserDefinedType + * @see org.springframework.data.cassandra.mapping.CassandraType + */ +public class CassandraPersistentEntitySchemaDropper { + + private final CassandraAdminOperations cassandraAdminOperations; + private final CassandraMappingContext mappingContext; + + /** + * Creates a new {@link CassandraPersistentEntitySchemaDropper} for the given {@link CassandraMappingContext} and + * {@link CassandraAdminOperations}. + * + * @param mappingContext must not be {@literal null}. + * @param cassandraAdminOperations must not be {@literal null}. + */ + public CassandraPersistentEntitySchemaDropper(CassandraMappingContext mappingContext, + CassandraAdminOperations cassandraAdminOperations) { + + Assert.notNull(cassandraAdminOperations, "CassandraAdminOperations must not be null"); + Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); + + this.cassandraAdminOperations = cassandraAdminOperations; + this.mappingContext = mappingContext; + } + + /** + * Drop tables that exist in the keyspace. + * + * @param dropUnused {@literal true} to drop unused tables. Table usage is determined by existing table mappings. + */ + public void dropTables(boolean dropUnused) { + + KeyspaceMetadata keyspaceMetadata = cassandraAdminOperations.getKeyspaceMetadata(); + + for (TableMetadata table : keyspaceMetadata.getTables()) { + if (dropUnused || mappingContext.usesTable(table)) { + cassandraAdminOperations.dropTable(CqlIdentifier.cqlId(table.getName())); + } + } + } + + /** + * Drop user types that exist in the keyspace. + * + * @param dropUnused {@literal true} to drop unused types before creation. Type usage is determined from existing + * mapped {@link org.springframework.data.cassandra.mapping.UserDefinedType}s and UDT names on field + * specifications. + */ + 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 (UserType userType : keyspaceMetadata.getUserTypes()) { + CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName()); + + if (canRecreate.contains(identifier)) { + cassandraAdminOperations.dropUserType(identifier); + } else if (dropUnused && !mappingContext.usesUserType(userType)) { + cassandraAdminOperations.dropUserType(identifier); + } + } + } +} 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 f2f93b045..4a552a1b5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -17,6 +17,8 @@ package org.springframework.data.cassandra.core; import static org.mockito.Mockito.*; +import lombok.Data; + import java.util.Set; import org.junit.Before; @@ -34,8 +36,6 @@ import org.springframework.data.cassandra.mapping.UserTypeResolver; import com.datastax.driver.core.KeyspaceMetadata; import com.datastax.driver.core.UserType; -import lombok.Data; - /** * Unit tests for {@link CassandraPersistentEntitySchemaCreator}. * @@ -48,9 +48,6 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests { @Mock KeyspaceMetadata metadata; @Mock UserType universetype; @Mock UserType moontype; - @Mock UserType manufacturertype; - @Mock UserType biketype; - @Mock UserType tiretype; BasicCassandraMappingContext context = new BasicCassandraMappingContext(); @@ -78,7 +75,7 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests { CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(context, operations); - schemaCreator.createUserTypes(false, false, false); + schemaCreator.createUserTypes(false); verify(operations).execute(Mockito.contains("CREATE TYPE universetype")); verify(operations).execute(Mockito.contains("CREATE TYPE moontype")); 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 new file mode 100644 index 000000000..7b812ba68 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java @@ -0,0 +1,181 @@ +/* + * Copyright 2017 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 static org.mockito.Mockito.*; + +import lombok.Data; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +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.KeyspaceMetadata; +import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.UserType; + +/** + * Unit tests for {@link CassandraPersistentEntitySchemaDropper}. + * + * @author Mark Paluch. + */ +@SuppressWarnings("unchecked") +@RunWith(MockitoJUnitRunner.class) +public class CassandraPersistentEntitySchemaDropperUnitTests { + + @Mock CassandraAdminOperations operations; + @Mock KeyspaceMetadata metadata; + @Mock UserType universetype; + @Mock UserType moontype; + @Mock UserType planettype; + @Mock TableMetadata person; + @Mock TableMetadata contact; + + BasicCassandraMappingContext context = new BasicCassandraMappingContext(); + + /** + * @see DATACASS-355 + */ + @Before + public void setUp() throws Exception { + + context.setUserTypeResolver(new UserTypeResolver() { + @Override + public UserType resolveType(CqlIdentifier typeName) { + return metadata.getUserType(typeName.toCql()); + } + }); + + 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"); + } + + /** + * @see DATACASS-355 + */ + @Test + public void shouldDropTypes() throws Exception { + + context.setInitialEntitySet(new HashSet>(Arrays.asList(MoonType.class, UniverseType.class))); + context.afterPropertiesSet(); + + when(metadata.getUserTypes()).thenReturn(Arrays.asList(universetype, moontype, planettype)); + + CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper(context, + operations); + + 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); + } + + /** + * @see DATACASS-355 + */ + @Test + public void dropUserTypesShouldRetainUnusedTypes() { + + context.setInitialEntitySet(new HashSet>(Arrays.asList(MoonType.class, UniverseType.class))); + context.afterPropertiesSet(); + + when(metadata.getUserTypes()).thenReturn(Arrays.asList(universetype, moontype, planettype)); + + CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper(context, + operations); + + schemaDropper.dropUserTypes(false); + + verify(operations).dropUserType(CqlIdentifier.cqlId("universetype")); + verify(operations).dropUserType(CqlIdentifier.cqlId("moontype")); + verify(operations).getKeyspaceMetadata(); + verifyNoMoreInteractions(operations); + } + + /** + * @see DATACASS-355 + */ + @Test + public void shouldDropTables() throws Exception { + + context.setInitialEntitySet(Collections.singleton(Person.class)); + context.afterPropertiesSet(); + + when(metadata.getTables()).thenReturn(Arrays.asList(person, contact)); + + CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper(context, + operations); + + schemaDropper.dropTables(true); + + verify(operations).dropTable(CqlIdentifier.cqlId("person")); + verify(operations).dropTable(CqlIdentifier.cqlId("contact")); + verify(operations).getKeyspaceMetadata(); + verifyNoMoreInteractions(operations); + } + + @Test + public void dropTablesShouldRetainUnusedTables() throws Exception { + + context.setInitialEntitySet(Collections.singleton(Person.class)); + context.afterPropertiesSet(); + + when(metadata.getTables()).thenReturn(Arrays.asList(person, contact)); + + CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper(context, + operations); + + schemaDropper.dropTables(false); + + verify(operations).dropTable(CqlIdentifier.cqlId("person")); + verify(operations).getKeyspaceMetadata(); + verifyNoMoreInteractions(operations); + } + + @UserDefinedType + @Data + static class UniverseType {} + + @UserDefinedType + static class MoonType {} + + @UserDefinedType + static class PlanetType {} + + @Table + static class Person { + @Id String id; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java index 74617de52..0ae29e77d 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/cdi/CassandraOperationsProducer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -31,6 +31,7 @@ import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.CassandraPersistentEntitySchemaCreator; +import org.springframework.data.cassandra.core.CassandraPersistentEntitySchemaDropper; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.SimpleUserTypeResolver; @@ -75,9 +76,15 @@ class CassandraOperationsProducer { cassandraTemplate.execute(createKeyspaceSpecification); cassandraTemplate.execute("USE " + KEYSPACE_NAME); - CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(mappingContext, cassandraTemplate); - schemaCreator.createUserTypes(false, false, true); - schemaCreator.createTables(false, false, true); + CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper(mappingContext, + cassandraTemplate); + schemaDropper.dropTables(false); + schemaDropper.dropUserTypes(false); + + CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(mappingContext, + cassandraTemplate); + schemaCreator.createUserTypes(false); + schemaCreator.createTables(false); for (CassandraPersistentEntity entity : cassandraTemplate.getConverter().getMappingContext() .getTableEntities()) {