diff --git a/build.gradle b/build.gradle index f47f1d4fe..a98d09edf 100644 --- a/build.gradle +++ b/build.gradle @@ -74,13 +74,14 @@ javadoc { ext.tmpDir = file("${buildDir}/api-work") configure(options) { - stylesheetFile = file("${srcDir}/spring-javadoc.css") - overview = "${srcDir}/overview.html" + //stylesheetFile = file("${srcDir}/spring-javadoc.css") + //overview = "${srcDir}/overview.html" docFilesSubDirs = true outputLevel = org.gradle.external.javadoc.JavadocOutputLevel.QUIET breakIterator = true showFromProtected() groups = [ + 'Spring Cassandra' : ['org.springframework.cassandra*'], 'Spring Data Cassandra' : ['org.springframework.data.cassandra*'], ] 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/CassandraOperations.java b/src/main/java/org/springframework/cassandra/core/CassandraOperations.java index fbd3653d6..991c9ec95 100644 --- a/src/main/java/org/springframework/cassandra/core/CassandraOperations.java +++ b/src/main/java/org/springframework/cassandra/core/CassandraOperations.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.dao.DataAccessException; import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Session; /** * Operations for interacting with Cassandra at the lowest level. This interface provides Exception Translation. @@ -36,7 +37,7 @@ public interface CassandraOperations { * SessionCallback can decide whether or not to execute() or executeAsync() the operation. * * @param sessionCallback - * @return + * @return Type defined in the SessionCallback */ T execute(SessionCallback sessionCallback) throws DataAccessException; @@ -48,19 +49,19 @@ public interface CassandraOperations { void execute(final String cql) throws DataAccessException; /** - * Executes the supplied CQL Query Asynchrously and returns nothing. + * Executes the supplied CQL Query Asynchronously and returns nothing. * - * @param cql + * @param cql The CQL Statement to execute */ void executeAsynchronously(final String cql) throws DataAccessException; /** - * Executes the provided CQL Query, and extracts the results with the ResultSetExtractor + * Executes the provided CQL Query, and extracts the results with the ResultSetExtractor. * * @param cql The Query - * @param rse The implementation for extracting the results + * @param rse The implementation for extracting the ResultSet * - * @return + * @return Type specified in the ResultSetExtractor * @throws DataAccessException */ T query(final String cql, ResultSetExtractor rse) throws DataAccessException; @@ -69,72 +70,332 @@ public interface CassandraOperations { * Executes the provided CQL Query asynchronously, and extracts the results with the ResultSetFutureExtractor * * @param cql The Query - * @param rse The implementation for extracting the results + * @param rse The implementation for extracting the future results * @return * @throws DataAccessException */ T queryAsynchronously(final String cql, ResultSetFutureExtractor rse) throws DataAccessException; + /** + * Executes the provided CQL Query, and then processes the results with the RowCallbackHandler. + * + * @param cql The Query + * @param rch The implementation for processing the rows returned. + * @throws DataAccessException + */ void query(final String cql, RowCallbackHandler rch) throws DataAccessException; + /** + * Processes the ResultSet through the RowCallbackHandler and return nothing. This is used internal to the Template + * for core operations, but is made available through Operations in the event you have a ResultSet to process. The + * ResultsSet could come from a ResultSetFuture after an asynchronous query. + * + * @param resultSet Results to process + * @param rch RowCallbackHandler with the processing implementation + * @throws DataAccessException + */ void process(ResultSet resultSet, RowCallbackHandler rch) throws DataAccessException; + /** + * Executes the provided CQL Query, and maps all Rows returned with the supplied RowMapper. + * + * @param cql The Query + * @param rowMapper The implementation for mapping all rows + * @return List of processed by the RowMapper + * @throws DataAccessException + */ List query(final String cql, RowMapper rowMapper) throws DataAccessException; + /** + * Processes the ResultSet through the RowMapper and returns the List of mapped Rows. This is used internal to the + * Template for core operations, but is made available through Operations in the event you have a ResultSet to + * process. The ResultsSet could come from a ResultSetFuture after an asynchronous query. + * + * @param resultSet Results to process + * @param rowMapper RowMapper with the processing implementation + * @return List of generated by the RowMapper + * @throws DataAccessException + */ List process(ResultSet resultSet, RowMapper rowMapper) throws DataAccessException; + /** + * Executes the provided CQL Query, and maps ONE Row returned with the supplied RowMapper. + * + *

+ * This expects only ONE row to be returned. More than one Row will cause an Exception to be thrown. + *

+ * + * @param cql The Query + * @param rowMapper The implementation for convert the Row to + * @return Object + * @throws DataAccessException + */ T queryForObject(final String cql, RowMapper rowMapper) throws DataAccessException; + /** + * Process a ResultSet through a RowMapper. This is used internal to the Template for core operations, but is made + * available through Operations in the event you have a ResultSet to process. The ResultsSet could come from a + * ResultSetFuture after an asynchronous query. + * + * @param resultSet + * @param rowMapper + * @return + * @throws DataAccessException + */ T processOne(ResultSet resultSet, RowMapper rowMapper) throws DataAccessException; + /** + * Executes the provided query and tries to return the first column of the first Row as a Class. + * + * @param cql The Query + * @param requiredType Valid Class that Cassandra Data Types can be converted to. + * @return The Object - item [0,0] in the result table of the query. + * @throws DataAccessException + */ T queryForObject(final String cql, Class requiredType) throws DataAccessException; + /** + * Process a ResultSet, trying to convert the first columns of the first Row to Class. This is used internal to the + * Template for core operations, but is made available through Operations in the event you have a ResultSet to + * process. The ResultsSet could come from a ResultSetFuture after an asynchronous query. + * + * @param resultSet + * @param requiredType + * @return + * @throws DataAccessException + */ T processOne(ResultSet resultSet, Class requiredType) throws DataAccessException; + /** + * Executes the provided CQL Query and maps ONE Row to a basic Map of Strings and Objects. If more than one Row + * is returned from the Query, an exception will be thrown. + * + * @param cql The Query + * @return Map representing the results of the Query + * @throws DataAccessException + */ Map queryForMap(final String cql) throws DataAccessException; + /** + * Process a ResultSet with ONE Row and convert to a Map. This is used internal to the Template for core + * operations, but is made available through Operations in the event you have a ResultSet to process. The ResultsSet + * could come from a ResultSetFuture after an asynchronous query. + * + * @param resultSet + * @return + * @throws DataAccessException + */ Map processMap(ResultSet resultSet) throws DataAccessException; + /** + * Executes the provided CQL and returns all values in the first column of the Results as a List of the Type in the + * second argument. + * + * @param cql The Query + * @param elementType Type to cast the data values to + * @return List of elementType + * @throws DataAccessException + */ List queryForList(final String cql, Class elementType) throws DataAccessException; + /** + * Process a ResultSet and convert the first column of the results to a List. This is used internal to the Template + * for core operations, but is made available through Operations in the event you have a ResultSet to process. The + * ResultsSet could come from a ResultSetFuture after an asynchronous query. + * + * @param resultSet + * @param elementType + * @return + * @throws DataAccessException + */ List processList(ResultSet resultSet, Class elementType) throws DataAccessException; + /** + * Executes the provided CQL and converts the results to a basic List of Maps. Each element in the List represents a + * Row returned from the Query. Each Row's columns are put into the map as column/value. + * + * @param cql The Query + * @return List of Maps with the query results + * @throws DataAccessException + */ List> queryForListOfMap(final String cql) throws DataAccessException; + /** + * Process a ResultSet and convert it to a List of Maps with column/value. This is used internal to the Template for + * core operations, but is made available through Operations in the event you have a ResultSet to process. The + * ResultsSet could come from a ResultSetFuture after an asynchronous query. + * + * @param resultSet + * @return + * @throws DataAccessException + */ List> processListOfMap(ResultSet resultSet) throws DataAccessException; + /** + * Converts the CQL provided into a {@link SimplePreparedStatementCreator}. This can only be used for CQL + * Statements that do not have data binding. The results of the PreparedStatement are processed with + * PreparedStatementCallback implementation provided by the Application Code. + * + * @param cql The CQL Statement to Execute + * @param action What to do with the results of the PreparedStatement + * @return Type as determined by the supplied Callback. + * @throws DataAccessException + */ T execute(String cql, PreparedStatementCallback action) throws DataAccessException; + /** + * Uses the provided PreparedStatementCreator to prepare a new Session call, then executes the statement and processes + * the statement using the provided Callback. This can only be used for CQL Statements that do not have data + * binding. The results of the PreparedStatement are processed with PreparedStatementCallback implementation + * provided by the Application Code. + * + * @param psc The implementation to create the PreparedStatement + * @param action What to do with the results of the PreparedStatement + * @return Type as determined by the supplied Callback. + * @throws DataAccessException + */ T execute(PreparedStatementCreator psc, PreparedStatementCallback action) throws DataAccessException; - T query(final String cql, PreparedStatementBinder pss, ResultSetExtractor rse) throws DataAccessException; + /** + * Converts the CQL provided into a {@link SimplePreparedStatementCreator}. Then, the PreparedStatementBinder will + * bind its values to the bind variables in the provided CQL String. The results of the PreparedStatement are + * processed with the ResultSetExtractor implementation provided by the Application Code. The can return any object, + * including a List of Objects to support the ResultSet processing. + * + * @param cql The Query to Prepare + * @param psb The Binding implementation + * @param rse The implementation for extracting the results of the query. + * @return Type generated by the ResultSetExtractor + * @throws DataAccessException + */ + T query(final String cql, PreparedStatementBinder psb, ResultSetExtractor rse) throws DataAccessException; - void query(final String cql, PreparedStatementBinder pss, RowCallbackHandler rch) throws DataAccessException; + /** + * Converts the CQL provided into a {@link SimplePreparedStatementCreator}. Then, the PreparedStatementBinder will + * bind its values to the bind variables in the provided CQL String. The results of the PreparedStatement are + * processed with the RowCallbackHandler implementation provided and nothing is returned. + * + * @param cql The Query to Prepare + * @param psb The Binding implementation + * @param rch The RowCallbackHandler for processing the ResultSet + * @throws DataAccessException + */ + void query(final String cql, PreparedStatementBinder psb, RowCallbackHandler rch) throws DataAccessException; - List query(final String cql, PreparedStatementBinder pss, RowMapper rowMapper) throws DataAccessException; + /** + * Converts the CQL provided into a {@link SimplePreparedStatementCreator}. Then, the PreparedStatementBinder will + * bind its values to the bind variables in the provided CQL String. The results of the PreparedStatement are + * processed with the RowMapper implementation provided and a List is returned with elements of Type for each Row + * returned. + * + * @param cql The Query to Prepare + * @param psb The Binding implementation + * @param rowMapper The implementation for Mapping a Row to Type + * @return List of for each Row returned from the Query. + * @throws DataAccessException + */ + List query(final String cql, PreparedStatementBinder psb, RowMapper rowMapper) throws DataAccessException; + /** + * Uses the provided PreparedStatementCreator to prepare a new Session call. This can only be used for CQL + * Statements that do not have data binding. The results of the PreparedStatement are processed with + * ResultSetExtractor implementation provided by the Application Code. + * + * @param psc The implementation to create the PreparedStatement + * @param rse Implementation for extracting from the ResultSet + * @return Type which is the output of the ResultSetExtractor + * @throws DataAccessException + */ T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException; + /** + * Uses the provided PreparedStatementCreator to prepare a new Session call. This can only be used for CQL + * Statements that do not have data binding. The results of the PreparedStatement are processed with + * RowCallbackHandler and nothing is returned. + * + * @param psc The implementation to create the PreparedStatement + * @param rch The implementation to process Results + * @throws DataAccessException + */ void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException; + /** + * Uses the provided PreparedStatementCreator to prepare a new Session call. This can only be used for CQL + * Statements that do not have data binding. The results of the PreparedStatement are processed with RowMapper + * implementation provided and a List is returned with elements of Type for each Row returned. + * + * @param psc The implementation to create the PreparedStatement + * @param rowMapper The implementation for mapping each Row returned. + * @return List of Type mapped from each Row in the Results + * @throws DataAccessException + */ List query(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException; - T query(PreparedStatementCreator psc, final PreparedStatementBinder pss, final ResultSetExtractor rse) - throws DataAccessException; - - void query(PreparedStatementCreator psc, final PreparedStatementBinder pss, final RowCallbackHandler rch) - throws DataAccessException; - - List query(PreparedStatementCreator psc, final PreparedStatementBinder pss, final RowMapper rowMapper) + /** + * Uses the provided PreparedStatementCreator to prepare a new Session call. Binds the values from the + * PreparedStatementBinder to the available bind variables. The results of the PreparedStatement are processed with + * ResultSetExtractor implementation provided by the Application Code. + * + * @param psc The implementation to create the PreparedStatement + * @param psb The implementation to bind variables to values + * @param rse Implementation for extracting from the ResultSet + * @return Type which is the output of the ResultSetExtractor + * @throws DataAccessException + */ + T query(PreparedStatementCreator psc, final PreparedStatementBinder psb, final ResultSetExtractor rse) throws DataAccessException; /** - * Describe the current Ring + * Uses the provided PreparedStatementCreator to prepare a new Session call. Binds the values from the + * PreparedStatementBinder to the available bind variables. The results of the PreparedStatement are processed with + * RowCallbackHandler and nothing is returned. + * + * @param psc The implementation to create the PreparedStatement + * @param psb The implementation to bind variables to values + * @param rch The implementation to process Results + * @return Type which is the output of the ResultSetExtractor + * @throws DataAccessException + */ + void query(PreparedStatementCreator psc, final PreparedStatementBinder psb, final RowCallbackHandler rch) + throws DataAccessException; + + /** + * Uses the provided PreparedStatementCreator to prepare a new Session call. Binds the values from the + * PreparedStatementBinder to the available bind variables. The results of the PreparedStatement are processed with + * RowMapper implementation provided and a List is returned with elements of Type for each Row returned. + * + * @param psc The implementation to create the PreparedStatement + * @param psb The implementation to bind variables to values + * @param rowMapper The implementation for mapping each Row returned. + * @return Type which is the output of the ResultSetExtractor + * @throws DataAccessException + */ + List query(PreparedStatementCreator psc, final PreparedStatementBinder psb, final RowMapper rowMapper) + throws DataAccessException; + + /** + * Describe the current Ring. This uses the provided {@link RingMemberHostMapper} to provide the basics of the + * Cassandra Ring topology. * * @return The list of ring tokens that are active in the cluster */ List describeRing() throws DataAccessException; + /** + * Describe the current Ring. Application code must provide its own {@link HostMapper} implementation to process the + * lists of hosts returned by the Cassandra Cluster Metadata. + * + * @param hostMapper The implementation to use for host mapping. + * @return Collection generated by the provided HostMapper. + * @throws DataAccessException + */ Collection describeRing(HostMapper hostMapper) throws DataAccessException; + /** + * Get the current Session used for operations in the implementing class. + * + * @return The DataStax Driver Session Object + */ + Session getSession(); + } diff --git a/src/main/java/org/springframework/cassandra/core/CassandraTemplate.java b/src/main/java/org/springframework/cassandra/core/CassandraTemplate.java index 5ed7e8aba..0e599926d 100644 --- a/src/main/java/org/springframework/cassandra/core/CassandraTemplate.java +++ b/src/main/java/org/springframework/cassandra/core/CassandraTemplate.java @@ -40,8 +40,15 @@ import com.datastax.driver.core.Session; import com.datastax.driver.core.exceptions.DriverException; /** - * The CassandraTemplate is a Spring convenience wrapper for low level and explicit operations on the Cassandra - * Database. For working with POJOs, use the {@link CassandraDataTemplate} + * This is the Central class in the Cassandra core package. It simplifies the use of Cassandra and helps to avoid + * common errors. It executes the core Cassandra workflow, leaving application code to provide CQL and result + * extraction. This class execute CQL Queries, provides different ways to extract/map results, and provides Exception + * translation to the generic, more informative exception hierarchy defined in the org.springframework.dao + * package. + * + *

+ * For working with POJOs, use the {@link CassandraDataTemplate}. + *

* * @author David Webb * @author Matthew Adams @@ -458,7 +465,7 @@ public class CassandraTemplate extends CassandraAccessor implements CassandraOpe /* (non-Javadoc) * @see org.springframework.cassandra.core.CassandraOperations#query(org.springframework.cassandra.core.PreparedStatementCreator, org.springframework.cassandra.core.PreparedStatementSetter, org.springframework.cassandra.core.ResultSetExtractor) */ - public T query(PreparedStatementCreator psc, final PreparedStatementBinder pss, final ResultSetExtractor rse) + public T query(PreparedStatementCreator psc, final PreparedStatementBinder psb, final ResultSetExtractor rse) throws DataAccessException { Assert.notNull(rse, "ResultSetExtractor must not be null"); @@ -468,8 +475,8 @@ public class CassandraTemplate extends CassandraAccessor implements CassandraOpe public T doInPreparedStatement(PreparedStatement ps) throws DriverException { ResultSet rs = null; BoundStatement bs = null; - if (pss != null) { - bs = pss.bindValues(ps); + if (psb != null) { + bs = psb.bindValues(ps); } else { bs = ps.bind(); } @@ -483,31 +490,31 @@ public class CassandraTemplate extends CassandraAccessor implements CassandraOpe * @see org.springframework.cassandra.core.CassandraOperations#query(java.lang.String, org.springframework.cassandra.core.PreparedStatementSetter, org.springframework.cassandra.core.ResultSetExtractor) */ @Override - public T query(String cql, PreparedStatementBinder pss, ResultSetExtractor rse) throws DataAccessException { - return query(new SimplePreparedStatementCreator(cql), pss, rse); + public T query(String cql, PreparedStatementBinder psb, ResultSetExtractor rse) throws DataAccessException { + return query(new SimplePreparedStatementCreator(cql), psb, rse); } /* (non-Javadoc) * @see org.springframework.cassandra.core.CassandraOperations#query(java.lang.String, org.springframework.cassandra.core.PreparedStatementSetter, org.springframework.cassandra.core.RowCallbackHandler) */ @Override - public void query(String cql, PreparedStatementBinder pss, RowCallbackHandler rch) throws DataAccessException { - query(new SimplePreparedStatementCreator(cql), pss, rch); + public void query(String cql, PreparedStatementBinder psb, RowCallbackHandler rch) throws DataAccessException { + query(new SimplePreparedStatementCreator(cql), psb, rch); } /* (non-Javadoc) * @see org.springframework.cassandra.core.CassandraOperations#query(java.lang.String, org.springframework.cassandra.core.PreparedStatementSetter, org.springframework.cassandra.core.RowMapper) */ @Override - public List query(String cql, PreparedStatementBinder pss, RowMapper rowMapper) throws DataAccessException { - return query(new SimplePreparedStatementCreator(cql), pss, rowMapper); + public List query(String cql, PreparedStatementBinder psb, RowMapper rowMapper) throws DataAccessException { + return query(new SimplePreparedStatementCreator(cql), psb, rowMapper); } /* (non-Javadoc) * @see org.springframework.cassandra.core.CassandraOperations#query(org.springframework.cassandra.core.PreparedStatementCreator, org.springframework.cassandra.core.PreparedStatementBinder, org.springframework.cassandra.core.RowCallbackHandler) */ @Override - public void query(PreparedStatementCreator psc, final PreparedStatementBinder pss, final RowCallbackHandler rch) + public void query(PreparedStatementCreator psc, final PreparedStatementBinder psb, final RowCallbackHandler rch) throws DataAccessException { Assert.notNull(rch, "RowCallbackHandler must not be null"); logger.debug("Executing prepared CQL query"); @@ -516,8 +523,8 @@ public class CassandraTemplate extends CassandraAccessor implements CassandraOpe public Object doInPreparedStatement(PreparedStatement ps) throws DriverException { ResultSet rs = null; BoundStatement bs = null; - if (pss != null) { - bs = pss.bindValues(ps); + if (psb != null) { + bs = psb.bindValues(ps); } else { bs = ps.bind(); } @@ -532,7 +539,7 @@ public class CassandraTemplate extends CassandraAccessor implements CassandraOpe * @see org.springframework.cassandra.core.CassandraOperations#query(org.springframework.cassandra.core.PreparedStatementCreator, org.springframework.cassandra.core.PreparedStatementBinder, org.springframework.cassandra.core.RowMapper) */ @Override - public List query(PreparedStatementCreator psc, final PreparedStatementBinder pss, final RowMapper rowMapper) + public List query(PreparedStatementCreator psc, final PreparedStatementBinder psb, final RowMapper rowMapper) throws DataAccessException { Assert.notNull(rowMapper, "RowMapper must not be null"); logger.debug("Executing prepared CQL query"); @@ -541,8 +548,8 @@ public class CassandraTemplate extends CassandraAccessor implements CassandraOpe public List doInPreparedStatement(PreparedStatement ps) throws DriverException { ResultSet rs = null; BoundStatement bs = null; - if (pss != null) { - bs = pss.bindValues(ps); + if (psb != null) { + bs = psb.bindValues(ps); } else { bs = ps.bind(); } 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/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java b/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java index c76b3cdcc..9f4195758 100644 --- a/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java +++ b/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java @@ -33,7 +33,7 @@ public abstract class AbstractCassandraConverter implements CassandraConverter, protected EntityInstantiators instantiators = new EntityInstantiators(); /** - * Creates a new {@link AbstractMongoConverter} using the given {@link GenericConversionService}. + * Creates a new {@link AbstractCassandraConverter} using the given {@link GenericConversionService}. * * @param conversionService */ 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