diff --git a/src/main/java/org/springframework/cassandra/core/CachedPreparedStatementCreator.java b/src/main/java/org/springframework/cassandra/core/CachedPreparedStatementCreator.java new file mode 100644 index 000000000..90fdba895 --- /dev/null +++ b/src/main/java/org/springframework/cassandra/core/CachedPreparedStatementCreator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2011-2013 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.cassandra.core; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; + +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.exceptions.DriverException; + +/** + * Created a PreparedStatement and retrieved the PreparedStatement from cache if the statement has been prepared + * previously. In general, this creator should be used over the {@link SimplePreparedStatementCreator} as it provides + * better performance. + * + *

+ * There is overhead in Cassandra when Preparing a Statement. This is negligible on a single data center configuration, + * but when your cluster spans multiple data centers, preparing the same statement over and over again is not necessary + * and causes performance issues in high throughput use cases. + *

+ * + * @author David Webb + * + */ +public class CachedPreparedStatementCreator implements PreparedStatementCreator, CqlProvider { + + private static Logger log = LoggerFactory.getLogger(CachedPreparedStatementCreator.class); + + private final String cql; + + private PreparedStatement cache; + + /** + * Create a CachedPreparedStatementCreator from the provided CQL. + * + * @param cql + */ + public CachedPreparedStatementCreator(String cql) { + Assert.notNull(cql, "CQL is required to create a PreparedStatement"); + this.cql = cql; + } + + /* (non-Javadoc) + * @see org.springframework.cassandra.core.PreparedStatementCreator#createPreparedStatement(com.datastax.driver.core.Session) + */ + @Override + public PreparedStatement createPreparedStatement(Session session) throws DriverException { + if (cache == null) { + log.debug("PreparedStatement cache is null, preparing new Statement"); + cache = session.prepare(getCql()); + } else { + log.debug("Using cached PreparedStatement"); + } + return cache; + } + + /* (non-Javadoc) + * @see org.springframework.cassandra.core.CqlProvider#getCql() + */ + @Override + public String getCql() { + return this.cql; + } + +} diff --git a/src/main/java/org/springframework/cassandra/core/CqlParameter.java b/src/main/java/org/springframework/cassandra/core/CqlParameter.java new file mode 100644 index 000000000..bfd5db193 --- /dev/null +++ b/src/main/java/org/springframework/cassandra/core/CqlParameter.java @@ -0,0 +1,139 @@ +/* + * Copyright 2011-2013 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.cassandra.core; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.util.Assert; + +import com.datastax.driver.core.DataType; + +/** + * @author David Webb + * + */ +public class CqlParameter { + + /** The name of the parameter, if any */ + private String name; + + /** SQL type constant from {@link DataType} */ + private final DataType type; + + /** The scale to apply in case of a NUMERIC or DECIMAL type, if any */ + private Integer scale; + + /** + * Create a new anonymous CqlParameter, supplying the SQL type. + * + * @param type Cassandra Data Type of the parameter according to {@link DataType} + */ + public CqlParameter(DataType type) { + this.type = type; + } + + /** + * Create a new anonymous CqlParameter, supplying the SQL type. + * + * @param type Cassandra Data Type of the parameter according to {@link DataType} + * @param scale the number of digits after the decimal point + */ + public CqlParameter(DataType type, int scale) { + this.type = type; + this.scale = scale; + } + + /** + * Create a new CqlParameter, supplying name and SQL type. + * + * @param name name of the parameter, as used in input and output maps + * @param type Cassandra Data Type of the parameter according to {@link DataType} + */ + public CqlParameter(String name, DataType type) { + this.name = name; + this.type = type; + } + + /** + * Create a new CqlParameter, supplying name and SQL type. + * + * @param name name of the parameter, as used in input and output maps + * @param type Cassandra Data Type of the parameter according to {@link DataType} + * @param scale the number of digits after the decimal point (for DECIMAL and NUMERIC types) + */ + public CqlParameter(String name, DataType type, int scale) { + this.name = name; + this.type = type; + this.scale = scale; + } + + /** + * Copy constructor. + * + * @param otherParam the CqlParameter object to copy from + */ + public CqlParameter(CqlParameter otherParam) { + Assert.notNull(otherParam, "CqlParameter object must not be null"); + this.name = otherParam.name; + this.type = otherParam.type; + this.scale = otherParam.scale; + } + + /** + * Return the name of the parameter. + */ + public String getName() { + return this.name; + } + + /** + * Return the SQL type of the parameter. + */ + public DataType getType() { + return this.type; + } + + /** + * Return the scale of the parameter, if any. + */ + public Integer getScale() { + return this.scale; + } + + /** + * Return whether this parameter holds input values that should be set before execution even if they are {@code null}. + *

+ * This implementation always returns {@code true}. + */ + public boolean isInputValueProvided() { + return true; + } + + /** + * Convert a list of JDBC types, as defined in {@code java.sql.Types}, to a List of CqlParameter objects as used in + * this package. + */ + public static List sqlTypesToAnonymousParameterList(DataType[] types) { + List result = new LinkedList(); + if (types != null) { + for (DataType type : types) { + result.add(new CqlParameter(type)); + } + } + return result; + } +} diff --git a/src/main/java/org/springframework/cassandra/core/CqlParameterValue.java b/src/main/java/org/springframework/cassandra/core/CqlParameterValue.java new file mode 100644 index 000000000..c2932815f --- /dev/null +++ b/src/main/java/org/springframework/cassandra/core/CqlParameterValue.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011-2013 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.cassandra.core; + +import com.datastax.driver.core.DataType; + +/** + * @author David Webb + * + */ +public class CqlParameterValue extends CqlParameter { + + private final Object value; + + /** + * Create a new CqlParameterValue, supplying the Cassandra DataType. + * + * @param type Cassandra Data Type of the parameter according to {@link DataType} + * @param value the value object + */ + public CqlParameterValue(DataType type, Object value) { + super(type); + this.value = value; + } + + /** + * Create a new CqlParameterValue, supplying the Cassandra DataType. + * + * @param type Cassandra Data Type of the parameter according to {@link DataType} + * @param scale the number of digits after the decimal point (for DECIMAL and NUMERIC types) + * @param value the value object + */ + public CqlParameterValue(DataType type, int scale, Object value) { + super(type, scale); + this.value = value; + } + + /** + * Create a new CqlParameterValue based on the given CqlParameter declaration. + * + * @param declaredParam the declared CqlParameter to define a value for + * @param value the value object + */ + public CqlParameterValue(CqlParameter declaredParam, Object value) { + super(declaredParam); + this.value = value; + } + + /** + * Return the value object that this parameter value holds. + */ + public Object getValue() { + return this.value; + } +} diff --git a/src/main/java/org/springframework/cassandra/core/PreparedStatementCreatorFactory.java b/src/main/java/org/springframework/cassandra/core/PreparedStatementCreatorFactory.java new file mode 100644 index 000000000..974b0436e --- /dev/null +++ b/src/main/java/org/springframework/cassandra/core/PreparedStatementCreatorFactory.java @@ -0,0 +1,202 @@ +/* + * Copyright 2011-2013 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.cassandra.core; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.util.Assert; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.exceptions.DriverException; + +/** + * @author David Webb + * + */ +public class PreparedStatementCreatorFactory { + + /** + * The CQL, which won't change when the parameters change + */ + private final String cql; + + /** List of CqlParameter objects. May not be {@code null}. */ + private final List declaredParameters; + + /** + * Create a new factory. + */ + public PreparedStatementCreatorFactory(String cql) { + this.cql = cql; + this.declaredParameters = new LinkedList(); + } + + /** + * Create a new factory with the given CQL and parameters. + * + * @param cql CQL + * @param declaredParameters list of {@link CqlParameter} objects + * @see CqlParameter + */ + public PreparedStatementCreatorFactory(String cql, List declaredParameters) { + this.cql = cql; + this.declaredParameters = declaredParameters; + } + + /** + * Return a new PreparedStatementBinder for the given parameters. + * + * @param params list of parameters (may be {@code null}) + */ + public PreparedStatementBinder newPreparedStatementBinder(List params) { + return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementBinder for the given parameters. + * + * @param params the parameter array (may be {@code null}) + */ + public PreparedStatementBinder newPreparedStatementBinder(Object[] params) { + return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * + * @param params list of parameters (may be {@code null}) + */ + public PreparedStatementCreator newPreparedStatementCreator(List params) { + return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * + * @param params the parameter array (may be {@code null}) + */ + public PreparedStatementCreator newPreparedStatementCreator(Object[] params) { + return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * + * @param sqlToUse the actual SQL statement to use (if different from the factory's, for example because of named + * parameter expanding) + * @param params the parameter array (may be {@code null}) + */ + public PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, Object[] params) { + return new PreparedStatementCreatorImpl(sqlToUse, params != null ? Arrays.asList(params) : Collections.emptyList()); + } + + /** + * PreparedStatementCreator implementation returned by this class. + */ + private class PreparedStatementCreatorImpl implements PreparedStatementCreator, PreparedStatementBinder, CqlProvider { + + private final String actualCql; + + private final List parameters; + + public PreparedStatementCreatorImpl(List parameters) { + this(cql, parameters); + } + + /** + * @param actualCql + * @param parameters + */ + public PreparedStatementCreatorImpl(String actualCql, List parameters) { + this.actualCql = actualCql; + Assert.notNull(parameters, "Parameters List must not be null"); + this.parameters = parameters; + if (this.parameters.size() != declaredParameters.size()) { + Set names = new HashSet(); + for (int i = 0; i < parameters.size(); i++) { + Object param = parameters.get(i); + if (param instanceof CqlParameterValue) { + names.add(((CqlParameterValue) param).getName()); + } else { + names.add("Parameter #" + i); + } + } + if (names.size() != declaredParameters.size()) { + throw new InvalidDataAccessApiUsageException("CQL [" + cql + "]: given " + names.size() + + " parameters but expected " + declaredParameters.size()); + } + } + } + + /* (non-Javadoc) + * @see org.springframework.cassandra.core.PreparedStatementCreator#createPreparedStatement(com.datastax.driver.core.Session) + */ + @Override + public PreparedStatement createPreparedStatement(Session session) throws DriverException { + return session.prepare(this.actualCql); + } + + /* (non-Javadoc) + * @see org.springframework.cassandra.core.PreparedStatementBinder#bindValues(com.datastax.driver.core.PreparedStatement) + */ + @Override + public BoundStatement bindValues(PreparedStatement ps) throws DriverException { + if (this.parameters == null || this.parameters.size() == 0) { + return ps.bind(); + } + + // Test the type of the first value + Object v = this.parameters.get(0); + Object[] values; + if (v instanceof CqlParameterValue) { + LinkedList valuesList = new LinkedList(); + for (Object value : this.parameters) { + valuesList.add(((CqlParameterValue) value).getValue()); + } + values = valuesList.toArray(); + } else { + values = this.parameters.toArray(); + } + + return ps.bind(values); + } + + /* (non-Javadoc) + * @see org.springframework.cassandra.core.CqlProvider#getCql() + */ + @Override + public String getCql() { + return cql; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PreparedStatementCreatorFactory.PreparedStatementCreatorImpl: cql=["); + sb.append(cql).append("]; parameters=").append(this.parameters); + return sb.toString(); + } + + } +} diff --git a/src/test/java/org/springframework/data/cassandra/test/integration/config/TestConfig.java b/src/test/java/org/springframework/data/cassandra/test/integration/config/TestConfig.java index c7302288a..e69e13cd2 100644 --- a/src/test/java/org/springframework/data/cassandra/test/integration/config/TestConfig.java +++ b/src/test/java/org/springframework/data/cassandra/test/integration/config/TestConfig.java @@ -77,5 +77,4 @@ public class TestConfig extends AbstractCassandraConfiguration { return template; } - } diff --git a/src/test/java/org/springframework/data/cassandra/test/integration/table/Book.java b/src/test/java/org/springframework/data/cassandra/test/integration/table/Book.java index a3bb9d68d..b5e07e29f 100644 --- a/src/test/java/org/springframework/data/cassandra/test/integration/table/Book.java +++ b/src/test/java/org/springframework/data/cassandra/test/integration/table/Book.java @@ -90,4 +90,15 @@ public class Book { this.pages = pages; } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("isbn -> " + isbn).append("\n"); + sb.append("tile -> " + title).append("\n"); + sb.append("author -> " + author).append("\n"); + sb.append("pages -> " + pages).append("\n"); + return sb.toString(); + } } diff --git a/src/test/java/org/springframework/data/cassandra/test/integration/template/CassandraOperationsTest.java b/src/test/java/org/springframework/data/cassandra/test/integration/template/CassandraOperationsTest.java index f3dd9b5cd..3945a1942 100644 --- a/src/test/java/org/springframework/data/cassandra/test/integration/template/CassandraOperationsTest.java +++ b/src/test/java/org/springframework/data/cassandra/test/integration/template/CassandraOperationsTest.java @@ -41,15 +41,31 @@ import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.interceptor.DefaultKeyGenerator; +import org.springframework.cassandra.core.CachedPreparedStatementCreator; import org.springframework.cassandra.core.CassandraOperations; +import org.springframework.cassandra.core.CqlParameter; +import org.springframework.cassandra.core.CqlParameterValue; import org.springframework.cassandra.core.HostMapper; +import org.springframework.cassandra.core.PreparedStatementBinder; +import org.springframework.cassandra.core.PreparedStatementCreatorFactory; +import org.springframework.cassandra.core.ResultSetExtractor; import org.springframework.cassandra.core.RingMember; +import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.test.integration.config.TestConfig; +import org.springframework.data.cassandra.test.integration.table.Book; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.Host; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; import com.datastax.driver.core.exceptions.DriverException; /** @@ -84,8 +100,9 @@ public class CassandraOperationsTest { private final static int CASSANDRA_THRIFT_PORT = 9160; @Rule - public CassandraCQLUnit cassandraCQLUnit = new CassandraCQLUnit(new ClassPathCQLDataSet("cql-dataload.cql", - KEYSPACE_NAME), CASSANDRA_CONFIG, CASSANDRA_HOST, CASSANDRA_NATIVE_PORT); + public CassandraCQLUnit cassandraCQLUnit = new CassandraCQLUnit(new ClassPathCQLDataSet( + "cassandraOperationsTest-cql-dataload.cql", KEYSPACE_NAME), CASSANDRA_CONFIG, CASSANDRA_HOST, + CASSANDRA_NATIVE_PORT); @BeforeClass public static void startCassandra() throws IOException, TTransportException, ConfigurationException, @@ -146,6 +163,129 @@ public class CassandraOperationsTest { } + @Test + public void preparedStatementFactoryTest() { + + String cql = "select * from book where isbn = ?"; + + List parameters = new LinkedList(); + parameters.add(new CqlParameter("isbn", DataType.text())); + + PreparedStatementCreatorFactory factory = new PreparedStatementCreatorFactory(cql, parameters); + + List values = new LinkedList(); + values.add(new CqlParameterValue(DataType.text(), "999999999")); + + Book b = cassandraTemplate.query(factory.newPreparedStatementCreator(values), + factory.newPreparedStatementBinder(values), new ResultSetExtractor() { + + @Override + public Book extractData(ResultSet rs) throws DriverException, DataAccessException { + Row r = rs.one(); + Book b = new Book(); + b.setIsbn(r.getString("isbn")); + b.setTitle(r.getString("title")); + b.setAuthor(r.getString("author")); + b.setPages(r.getInt("pages")); + return b; + } + }); + + log.info(b.toString()); + + } + + // @Test + public void cachedPreparedStatementTest() { + + log.info(echoString("Hello")); + log.info(echoString("Hello")); + + String cql = "select * from book where isbn = ?"; + + CachedPreparedStatementCreator cpsc = new CachedPreparedStatementCreator(cql); + + Book b = cassandraTemplate.query(cpsc, new PreparedStatementBinder() { + + @Override + public BoundStatement bindValues(PreparedStatement ps) throws DriverException { + return ps.bind("999999999"); + } + }, new ResultSetExtractor() { + + @Override + public Book extractData(ResultSet rs) throws DriverException, DataAccessException { + Row r = rs.one(); + Book b = new Book(); + b.setIsbn(r.getString("isbn")); + b.setTitle(r.getString("title")); + b.setAuthor(r.getString("author")); + b.setPages(r.getInt("pages")); + return b; + } + }); + + assertNotNull(b); + + log.info(b.toString()); + + try { + DefaultKeyGenerator generator = new DefaultKeyGenerator(); + + // TODO Why does method have to be public to work? Options? + Object cacheKey = generator.generate(CachedPreparedStatementCreator.class, + CachedPreparedStatementCreator.class.getMethod("getCachedPreparedStatement", Session.class, String.class), + cassandraTemplate.getSession(), cql); + + log.info("cacheKey -> " + cacheKey); + + // ConcurrentMapCache cache = (ConcurrentMapCache) cacheManager.getCache("sdc-pstmts"); + // ConcurrentMap cacheMap = cache.getNativeCache(); + // assertNotNull(cacheMap); + // log.info("CacheMap.size() -> " + cacheMap.size()); + // ValueWrapper vw = cache.get(cacheKey); + // PreparedStatement pstmt = (PreparedStatement) vw.get(); + // assertNotNull(pstmt); + // log.info(pstmt.getQueryString()); + // assertEquals(pstmt.getQueryString(), cql); + } catch (NoSuchMethodException e) { + log.error("Failed to find method", e); + } + + CachedPreparedStatementCreator cpsc2 = new CachedPreparedStatementCreator(cql); + + Book b2 = cassandraTemplate.query(cpsc2, new PreparedStatementBinder() { + + @Override + public BoundStatement bindValues(PreparedStatement ps) throws DriverException { + return ps.bind("999999999"); + } + }, new ResultSetExtractor() { + + @Override + public Book extractData(ResultSet rs) throws DriverException, DataAccessException { + Row r = rs.one(); + Book b = new Book(); + b.setIsbn(r.getString("isbn")); + b.setTitle(r.getString("title")); + b.setAuthor(r.getString("author")); + b.setPages(r.getInt("pages")); + return b; + } + }); + + assertNotNull(b2); + + log.info(b2.toString()); + + } + + @Cacheable("sdc-pstmts") + public String echoString(String s) { + log.info("In EchoString"); + return s; + } + @After public void clearCassandra() { EmbeddedCassandraServerHelper.cleanEmbeddedCassandra(); @@ -154,6 +294,6 @@ public class CassandraOperationsTest { @AfterClass public static void stopCassandra() { - EmbeddedCassandraServerHelper.stopEmbeddedCassandra(); + // EmbeddedCassandraServerHelper.stopEmbeddedCassandra(); } } diff --git a/src/test/resources/cassandraOperationsTest-cql-dataload.cql b/src/test/resources/cassandraOperationsTest-cql-dataload.cql new file mode 100644 index 000000000..239ae3e25 --- /dev/null +++ b/src/test/resources/cassandraOperationsTest-cql-dataload.cql @@ -0,0 +1,3 @@ +create table book (isbn text, title text, author text, pages int, PRIMARY KEY (isbn)); +create table book_alt (isbn text, title text, author text, pages int, PRIMARY KEY (isbn)); +insert into book (isbn, title, author, pages) values ('999999999', 'Book of Nines', 'Nine Nine', 999); \ No newline at end of file diff --git a/src/test/resources/cql-dataload.cql b/src/test/resources/cql-dataload.cql index 4c8a0e324..e38d18d36 100644 --- a/src/test/resources/cql-dataload.cql +++ b/src/test/resources/cql-dataload.cql @@ -1,2 +1,3 @@ create table book (isbn text, title text, author text, pages int, PRIMARY KEY (isbn)); -create table book_alt (isbn text, title text, author text, pages int, PRIMARY KEY (isbn)); \ No newline at end of file +create table book_alt (isbn text, title text, author text, pages int, PRIMARY KEY (isbn)); +/*insert into book (isbn, title, author, pages) values ('999999999', 'Book of Nines', 'Nine Nine', 999);*/ \ No newline at end of file