DATACASS-705 - Introduce FactoryBean for SessionFactory.

We now provide SessionFactoryFactoryBean as factory bean and initializer for a SessionFactory.

<cassandra:session-factory>
	<cassandra:script
			location="my/scripts/schema.cql"/>
</cassandra:session-factory>

Original pull request: #167.
This commit is contained in:
Mark Paluch
2019-11-27 13:55:14 +01:00
parent efb5b4b5d4
commit 2dd83238af
14 changed files with 373 additions and 23 deletions

View File

@@ -330,7 +330,9 @@ public abstract class AbstractClusterConfiguration {
* {@link com.datastax.driver.core.Cluster} initialization.
*
* @return the list of startup scripts, may be empty but never {@link null}
* @deprecated Use {@link org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer}.
*/
@Deprecated
protected List<String> getStartupScripts() {
return Collections.emptyList();
}
@@ -340,7 +342,9 @@ public abstract class AbstractClusterConfiguration {
* {@link com.datastax.driver.core.Cluster} shutdown.
*
* @return the list of shutdown scripts, may be empty but never {@link null}
* @deprecated Use {@link org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer}.
*/
@Deprecated
protected List<String> getShutdownScripts() {
return Collections.emptyList();
}

View File

@@ -17,7 +17,9 @@ package org.springframework.data.cassandra.config;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.cassandra.SessionFactory;
import org.springframework.data.cassandra.core.cql.CqlTemplate;
import org.springframework.data.cassandra.core.cql.session.DefaultSessionFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -33,7 +35,7 @@ public class CassandraCqlTemplateFactoryBean implements FactoryBean<CqlTemplate>
private @Nullable CqlTemplate template;
private @Nullable Session session;
private @Nullable SessionFactory sessionFactory;
/* (non-Javadoc)
* @see org.springframework.beans.factory.FactoryBean#getObject()
@@ -65,9 +67,9 @@ public class CassandraCqlTemplateFactoryBean implements FactoryBean<CqlTemplate>
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(session, "Session must not be null");
Assert.notNull(sessionFactory, "SessionFactory must not be null");
this.template = new CqlTemplate(session);
this.template = new CqlTemplate(sessionFactory);
}
/**
@@ -81,6 +83,22 @@ public class CassandraCqlTemplateFactoryBean implements FactoryBean<CqlTemplate>
Assert.notNull(session, "Session must not be null");
this.session = session;
setSessionFactory(new DefaultSessionFactory(session));
}
/**
* Sets the Cassandra {@link SessionFactory} to use. The {@link CqlTemplate} will use the logged keyspace of the
* underlying {@link SessionFactory}. Don't change the keyspace using CQL but use an appropriate
* {@link SessionFactory}.
*
* @param sessionFactory must not be {@literal null}.
* @see SessionFactory
* @see org.springframework.data.cassandra.core.cql.session.lookup.AbstractRoutingSessionFactory
*/
public void setSessionFactory(SessionFactory sessionFactory) {
Assert.notNull(sessionFactory, "SessionFactory must not be null");
this.sessionFactory = sessionFactory;
}
}

View File

@@ -57,6 +57,12 @@ class CassandraCqlTemplateParser extends AbstractSingleBeanDefinitionParser {
*/
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
addOptionalPropertyReference(builder, "session", element, "session-ref", DefaultCqlBeanNames.SESSION);
if (element.hasAttribute("session-factory-ref")) {
addOptionalPropertyReference(builder, "sessionFactory", element, "session-factory-ref",
DefaultCqlBeanNames.SESSION);
} else {
addOptionalPropertyReference(builder, "session", element, "session-ref", DefaultCqlBeanNames.SESSION);
}
}
}

View File

@@ -35,6 +35,7 @@ public class CassandraNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("session", new CqlSessionParser());
registerBeanDefinitionParser("session-factory", new SessionFactoryBeanDefinitionParser());
registerBeanDefinitionParser("template", new CassandraTemplateParser());
registerBeanDefinitionParser("cql-template", new CassandraCqlTemplateParser());
registerBeanDefinitionParser("auditing", new CassandraAuditingBeanDefinitionParser());

View File

@@ -63,8 +63,10 @@ class CassandraTemplateParser extends AbstractSingleBeanDefinitionParser {
super.doParse(element, parserContext, builder);
if (StringUtils.hasText(element.getAttribute("cql-template-ref"))) {
if (element.hasAttribute("cql-template-ref")) {
addRequiredPropertyReference(builder, "cqlOperations", element, "cql-template-ref");
} else if (element.hasAttribute("session-factory-ref")) {
addRequiredPropertyReference(builder, "sessionFactory", element, "session-factory-ref");
} else {
addOptionalPropertyReference(builder, "session", element, "session-ref", DefaultBeanNames.SESSION);
}

View File

@@ -282,6 +282,10 @@ public class CqlSessionFactoryBean implements FactoryBean<Session>, Initializing
/**
* Sets CQL scripts to be executed immediately after the session is connected.
*
* @deprecated Use {@link org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer} or
* {@link SessionFactoryFactoryBean} with
* {@link org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator} instead.
*/
public void setStartupScripts(@Nullable List<String> scripts) {
this.startupScripts = (scripts != null ? new ArrayList<>(scripts) : Collections.emptyList());
@@ -289,21 +293,36 @@ public class CqlSessionFactoryBean implements FactoryBean<Session>, Initializing
/**
* Returns an unmodifiable list of startup scripts.
*
* @deprecated Use {@link org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer} or
* {@link SessionFactoryFactoryBean} with
* {@link org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator} instead.
*/
@Deprecated
public List<String> getStartupScripts() {
return Collections.unmodifiableList(this.startupScripts);
}
/**
* Sets CQL scripts to be executed immediately before the session is shutdown.
*
* @deprecated Use {@link org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer} or
* {@link SessionFactoryFactoryBean} with
* {@link org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator} instead.
*/
@Deprecated
public void setShutdownScripts(@Nullable List<String> scripts) {
this.shutdownScripts = scripts != null ? new ArrayList<>(scripts) : Collections.emptyList();
}
/**
* Returns an unmodifiable list of shutdown scripts.
*
* @deprecated Use {@link org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer} or
* {@link SessionFactoryFactoryBean} with
* {@link org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator} instead.
*/
@Deprecated
public List<String> getShutdownScripts() {
return Collections.unmodifiableList(this.shutdownScripts);
}
@@ -313,7 +332,10 @@ public class CqlSessionFactoryBean implements FactoryBean<Session>, Initializing
* {@link CassandraMappingContext} inside {@code converter}.
*
* @param converter must not be {@literal null}.
* @deprecated Use {@link CassandraSessionFactoryBean} with
* {@link CassandraSessionFactoryBean#setConverter(CassandraConverter)} instead.
*/
@Deprecated
public void setConverter(CassandraConverter converter) {
Assert.notNull(converter, "CassandraConverter must not be null");
@@ -345,7 +367,10 @@ public class CqlSessionFactoryBean implements FactoryBean<Session>, Initializing
* Set the {@link SchemaAction}.
*
* @param schemaAction must not be {@literal null}.
* @deprecated Use {@link CassandraSessionFactoryBean} with
* {@link CassandraSessionFactoryBean#setSchemaAction(SchemaAction)} instead.
*/
@Deprecated
public void setSchemaAction(SchemaAction schemaAction) {
Assert.notNull(schemaAction, "SchemaAction must not be null");

View File

@@ -19,10 +19,12 @@ package org.springframework.data.cassandra.config;
* @author Alex Shvid
* @author David Webb
* @author Matthew T. Adams
* @author Mark Paluch
*/
public interface DefaultCqlBeanNames {
String CLUSTER = "cassandraCluster";
String SESSION = "cassandraSession";
String SESSION_FACTORY = "cassandraSessionFactory";
String TEMPLATE = "cqlTemplate";
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2013-2019 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
*
* https://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.config;
import static org.springframework.data.cassandra.config.ParsingUtils.*;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
/**
* Parser for &lt;session-factory&gt; definitions.
*
* @author Mark Paluch
*/
class SessionFactoryBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
/* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element)
*/
@Override
protected Class<?> getBeanClass(Element element) {
return SessionFactoryFactoryBean.class;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext)
*/
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {
String id = super.resolveId(element, definition, parserContext);
return StringUtils.hasText(id) ? id : DefaultBeanNames.SESSION_FACTORY;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder)
*/
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
addOptionalPropertyReference(builder, "session", element, "session-ref", DefaultBeanNames.SESSION);
addOptionalPropertyReference(builder, "converter", element, "cassandra-converter-ref", DefaultBeanNames.CONVERTER);
addOptionalPropertyValue(builder, "schemaAction", element, "schema-action", SchemaAction.NONE.name());
InitializeKeyspaceBeanDefinitionParser.parseKeyspacePopulator(element, builder);
}
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright 2013-2019 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
*
* https://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.config;
import org.springframework.beans.factory.config.AbstractFactoryBean;
import org.springframework.data.cassandra.SessionFactory;
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.core.convert.CassandraConverter;
import org.springframework.data.cassandra.core.cql.session.DefaultSessionFactory;
import org.springframework.data.cassandra.core.cql.session.init.KeyspacePopulator;
import org.springframework.data.cassandra.core.cql.session.init.SessionFactoryInitializer;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.datastax.driver.core.Session;
/**
* Factory to create and configure a Cassandra {@link SessionFactory} with support for executing CQL and initializing
* the database schema (a.k.a. keyspace). This factory bean invokes a {@link SessionFactoryInitializer} to prepare a
* keyspace before applying {@link SchemaAction schema actions} such as creating user-defined types and tables.
*
* @author Mark Paluch
* @since 3.0
* @see SessionFactoryInitializer
*/
public class SessionFactoryFactoryBean extends AbstractFactoryBean<SessionFactory> {
protected static final boolean DEFAULT_CREATE_IF_NOT_EXISTS = false;
protected static final boolean DEFAULT_DROP_TABLES = false;
protected static final boolean DEFAULT_DROP_UNUSED_TABLES = false;
private Session session;
private @Nullable KeyspacePopulator keyspacePopulator;
private @Nullable KeyspacePopulator keyspaceCleaner;
private CassandraConverter converter;
private SchemaAction schemaAction = SchemaAction.NONE;
/**
* Set the {@link Session} to use.
*
* @param session must not be {@literal null}.
*/
public void setSession(Session session) {
Assert.notNull(session, "Session must not be null");
this.session = session;
}
/**
* Set the {@link KeyspacePopulator} to execute during the bean initialization phase.
*
* @param keyspacePopulator the {@link KeyspacePopulator} to use during initialization.
* @see #setKeyspaceCleaner
*/
public void setKeyspacePopulator(KeyspacePopulator keyspacePopulator) {
this.keyspacePopulator = keyspacePopulator;
}
/**
* Set the {@link KeyspacePopulator} to execute during the bean destruction phase, cleaning up the keyspace and
* leaving it in a known state for others.
*
* @param keyspaceCleaner the {@link KeyspacePopulator} to use during destruction.
* @see #setKeyspacePopulator
*/
public void setKeyspaceCleaner(KeyspacePopulator keyspaceCleaner) {
this.keyspaceCleaner = keyspaceCleaner;
}
/**
* Set the {@link CassandraConverter} to use. Schema actions will derive table and user type information from the
* {@link CassandraMappingContext} inside {@code converter}.
*
* @param converter must not be {@literal null}.
*/
public void setConverter(CassandraConverter converter) {
Assert.notNull(converter, "CassandraConverter must not be null");
this.converter = converter;
}
/**
* Set the {@link SchemaAction}.
*
* @param schemaAction must not be {@literal null}.
*/
public void setSchemaAction(SchemaAction schemaAction) {
Assert.notNull(schemaAction, "SchemaAction must not be null");
this.schemaAction = schemaAction;
}
/*
* (non-Javadoc)
* @see org.springframework.cassandra.config.CassandraCqlSessionFactoryBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(this.session != null, "Session was not properly initialized");
Assert.state(this.converter != null, "Converter was not properly initialized");
super.afterPropertiesSet();
if (this.keyspacePopulator != null) {
this.keyspacePopulator.populate(getObject().getSession());
}
performSchemaAction();
}
@Override
public void destroy() throws Exception {
if (this.keyspaceCleaner != null) {
this.keyspaceCleaner.populate(getObject().getSession());
}
}
@Nullable
@Override
public Class<?> getObjectType() {
return SessionFactory.class;
}
@Override
protected SessionFactory createInstance() {
return new DefaultSessionFactory(session);
}
/**
* Perform the configure {@link SchemaAction} using {@link CassandraMappingContext} metadata.
*/
protected void performSchemaAction() throws Exception {
boolean create = false;
boolean drop = DEFAULT_DROP_TABLES;
boolean dropUnused = DEFAULT_DROP_UNUSED_TABLES;
boolean ifNotExists = DEFAULT_CREATE_IF_NOT_EXISTS;
switch (this.schemaAction) {
case RECREATE_DROP_UNUSED:
dropUnused = true;
case RECREATE:
drop = true;
case CREATE_IF_NOT_EXISTS:
ifNotExists = SchemaAction.CREATE_IF_NOT_EXISTS.equals(this.schemaAction);
case CREATE:
create = true;
case NONE:
default:
// do nothing
}
if (create) {
createTables(drop, dropUnused, ifNotExists);
}
}
/**
* Perform schema actions.
*
* @param drop {@literal true} to drop types/tables.
* @param dropUnused {@literal true} to drop unused types/tables (i.e. types/tables not know to be used by
* {@link CassandraMappingContext}).
* @param ifNotExists {@literal true} to perform creations fail-safe by adding {@code IF NOT EXISTS} to each creation
* statement.
*/
protected void createTables(boolean drop, boolean dropUnused, boolean ifNotExists) throws Exception {
performSchemaActions(drop, dropUnused, ifNotExists);
}
private void performSchemaActions(boolean drop, boolean dropUnused, boolean ifNotExists) throws Exception {
CassandraAdminOperations adminOperations = new CassandraAdminTemplate(getObject(), this.converter);
CassandraPersistentEntitySchemaCreator schemaCreator = new CassandraPersistentEntitySchemaCreator(
this.converter.getMappingContext(), adminOperations);
if (drop) {
CassandraPersistentEntitySchemaDropper schemaDropper = new CassandraPersistentEntitySchemaDropper(
this.converter.getMappingContext(), adminOperations);
schemaDropper.dropTables(dropUnused);
schemaDropper.dropUserTypes(dropUnused);
}
schemaCreator.createUserTypes(ifNotExists);
schemaCreator.createTables(ifNotExists);
schemaCreator.createIndexes(ifNotExists);
}
}

View File

@@ -45,7 +45,7 @@ public class SimpleUserTypeResolver implements UserTypeResolver {
*/
public SimpleUserTypeResolver(Session session) {
Assert.notNull(session, "Serssion must not be null");
Assert.notNull(session, "Session must not be null");
this.keyspaceName = session.getLoggedKeyspace();
this.cluster = session.getCluster();

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.data.cassandra.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,13 +34,6 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.Configuration;
import com.datastax.driver.core.HostDistance;
import com.datastax.driver.core.PoolingOptions;
import com.datastax.driver.core.ProtocolOptions.Compression;
import com.datastax.driver.core.SocketOptions;
/**
* Integration tests for XML-based Cassandra configuration using the Cassandra namespace parsed with
* {@link CqlNamespaceHandler}.
@@ -49,16 +45,25 @@ import com.datastax.driver.core.SocketOptions;
@SuppressWarnings("unused")
public class CassandraNamespaceIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest {
@Autowired
private ApplicationContext applicationContext;
@Autowired private ApplicationContext applicationContext;
@Test // DATACASS-705
public void keyspaceShouldBeInitialized() {
CqlTemplate cqlTemplate = this.applicationContext.getBean(CqlTemplate.class);
List<Map<String, Object>> result = cqlTemplate.queryForList("SELECT * FROM mytable1");
assertThat(result).isEmpty();
}
@Test // DATACASS-172
public void mappingContextShouldHaveUserTypeResolverConfigured() {
CassandraMappingContext mappingContext = this.applicationContext.getBean(CassandraMappingContext.class);
SimpleUserTypeResolver userTypeResolver =
(SimpleUserTypeResolver) ReflectionTestUtils.getField(mappingContext, "userTypeResolver");
SimpleUserTypeResolver userTypeResolver = (SimpleUserTypeResolver) ReflectionTestUtils.getField(mappingContext,
"userTypeResolver");
assertThat(userTypeResolver).isNotNull();
}

View File

@@ -17,12 +17,16 @@
<cassandra:session contact-points="${build.cassandra.host}"
port="${build.cassandra.native_transport_port}"
keyspace-name="${cassandra.keyspace}"
schema-action="NONE">
keyspace-name="${cassandra.keyspace}">
<cassandra:keyspace name="${cassandra.keyspace}" action="CREATE_DROP"
durable-writes="true"/>
</cassandra:session>
<cassandra:session-factory>
<cassandra:script
location="classpath:/org/springframework/data/cassandra/config/schema.cql"/>
</cassandra:session-factory>
<cassandra:mapping>
<cassandra:user-type-resolver session-ref="cassandraSession"/>
</cassandra:mapping>

View File

@@ -0,0 +1 @@
create table if not exists mytable1 (id uuid primary key, column1 text);

View File

@@ -610,8 +610,6 @@ NOTE: Keyspace creation allows rapid bootstrapping without the need of external
for certain scenarios but should be used with care. Dropping a keyspace on application shutdown removes the keyspace
and all data from the tables in the keyspace.
[[cassandra.schema-management.initializing]]
=== Initializing a `SessionFactory`