DATACASS-335 - Add support for reactive data access.
We now support reactive data access with Spring Data Cassandra by adopting Datastax' asynchronous driver.
ReactiveCqlTemplate and ReactiveCassandraTemplate use Project Reactor wrapper types Mono and Flux to implement Template API and repository support. Reactive template supports common operations such as:
* Query/Execution methods for static CQL and prepared statements
* Insert/Save/Update/Delete methods
* Exists and Count projections
* Reactive Callback methods
Person person = new Person("Dave", 25);
template.insert(person) //
.flatMap(p -> template.update(new Person("Sven", 25))) //
.flatMap(p -> template.selectOneById(person.getId(), Person.class)) //
.subscribeWith(TestSubscriber.create()) //
.await() //
.assertValuesWith(result -> {
assertThat(result.getFirstName(), is(equalTo("Sven")));
});
Reactive Repository support is built on top of ReactiveCassandraTemplate using ReactiveCassandraRepository as the store-specific base repository. Reactive repositories are enabled by using @EnableReactiveCassandraRepositories on a @Configuration class to opt-in for reactive support. Reactive repositories can be composed of a reactive base interface such as
* ReactiveCrudRepository
* ReactiveSortingRepository
* RxJava1CrudRepository
* RxJava1SortingRepository
and are identified as reactive repository if one method uses a reactive wrapper type (such as Flux or Observable). If a reactive repository is discovered, it's not implemented by the blocking repository support but with the reactive repository factory. Blocking methods are not (yet) synchronized when using a reactive repository so each repository method must use a reactive wrapper result type. Reactive repository support with Spring Data allows using RxJava1 and Project Reactor types to declare repository methods. Reactive wrapper types are internally converted so the composition library choice on repository level is left up to the user.
There's feature parity between Reactive Cassandra repository support and blocking repository support.
Feature overview:
* Query Methods using String queries and Query Derivation
* Projections
@Configuration
@EnableReactiveCassandraRepositories
class ApplicationConfig extends AbstractReactiveCassandraConfiguration {
@Override
protected String getKeyspaceName() {
return "mykeyspace";
}
@Override
protected String getEntityBasePackages() {
return new String[] {"com.springdata.cassandra"};
}
}
public interface PersonRepository extends ReactiveSortingRepository<Person, String> {
Flux<Person> findByFirstname(String firstname);
Flux<Person> findByFirstname(Publisher<String> firstname);
Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);
}
public interface PersonRepository extends RxJava1SortingRepository<Person, String> {
Observable<Person> findByFirstname(String firstname);
Observable<Person> findByFirstname(Single<String> firstname);
Single<Person> findByFirstnameAndLastname(String firstname, String lastname);
}
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2016 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.BoundStatement;
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Simple adapter for {@link PreparedStatementBinder} that applies a given array of arguments.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ArgumentPreparedStatementBinder implements PreparedStatementBinder {
|
||||
|
||||
private final Object[] args;
|
||||
|
||||
/**
|
||||
* Create a new {@link ArgumentPreparedStatementBinder} for the given arguments.
|
||||
*
|
||||
* @param args the arguments to set. May be empty or {@link null} if no arguments are provided.
|
||||
*/
|
||||
public ArgumentPreparedStatementBinder(Object[] args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundStatement bindValues(PreparedStatement ps) throws DriverException {
|
||||
return args != null ? ps.bind(args) : ps.bind();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2016 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.Map;
|
||||
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
import com.datastax.driver.core.Row;
|
||||
|
||||
/**
|
||||
* {@link RowMapper} implementation that creates a {@code java.util.Map} for each row, representing all columns as
|
||||
* key-value pairs: one entry for each column, with the column name as key.
|
||||
* <p>
|
||||
* The Map implementation to use and the key to use for each column in the column Map can be customized through
|
||||
* overriding {@link #createColumnMap} and {@link #getColumnKey}, respectively.
|
||||
* <p>
|
||||
* <b>Note:</b> By default, ColumnMapRowMapper will try to build a linked Map with case-insensitive keys, to preserve
|
||||
* column order as well as allow any casing to be used for column names. This requires Commons Collections on the
|
||||
* classpath (which will be autodetected). Else, the fallback is a standard linked HashMap, which will still preserve
|
||||
* column order but requires the application to specify the column names in the same casing as exposed by the driver.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCqlTemplate#queryForFlux(String)
|
||||
* @see ReactiveCqlTemplate#queryForMap(String)
|
||||
*/
|
||||
public class ColumnMapRowMapper implements RowMapper<Map<String, Object>> {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> mapRow(Row rs, int rowNum) {
|
||||
|
||||
ColumnDefinitions columnDefinitions = rs.getColumnDefinitions();
|
||||
int columnCount = columnDefinitions.size();
|
||||
Map<String, Object> mapOfColValues = createColumnMap(columnCount);
|
||||
|
||||
for (int i = 0; i < columnCount; i++) {
|
||||
String key = getColumnKey(columnDefinitions.getName(i));
|
||||
Object obj = getColumnValue(rs, i);
|
||||
mapOfColValues.put(key, obj);
|
||||
}
|
||||
return mapOfColValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Map} instance to be used as column map.
|
||||
* <p>
|
||||
* By default, a linked case-insensitive Map will be created.
|
||||
*
|
||||
* @param columnCount the column count, to be used as initial capacity for the {@link Map}, must not be {@literal null}.
|
||||
* @return the new Map instance.
|
||||
* @see org.springframework.util.LinkedCaseInsensitiveMap
|
||||
*/
|
||||
protected Map<String, Object> createColumnMap(int columnCount) {
|
||||
return new LinkedCaseInsensitiveMap<>(columnCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the key to use for the given column in the column Map.
|
||||
*
|
||||
* @param columnName the column name as returned by the {@link Row}, must not be {@literal null}.
|
||||
* @return the column key to use.
|
||||
* @see ColumnDefinitions#getName(int)
|
||||
*/
|
||||
protected String getColumnKey(String columnName) {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a CQL object value for the specified column.
|
||||
* <p>
|
||||
* The default implementation uses the {@code getObject} method.
|
||||
*
|
||||
* @param row is the {@link Row} holding the data, must not be {@literal null}.
|
||||
* @param index is the column index.
|
||||
* @return the Object returned
|
||||
*/
|
||||
protected Object getColumnValue(Row row, int index) {
|
||||
return row.getObject(index);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2016 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;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by objects that can provide CQL strings.
|
||||
* <p>
|
||||
* Typically implemented by {@link PreparedStatementCreator}s and statement callbacks that want to expose the CQL they
|
||||
* use to create their statements, to allow for better contextual information in case of exceptions.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see PreparedStatementCreator
|
||||
* @see ReactivePreparedStatementCreator
|
||||
* @see ReactiveStatementCallback
|
||||
*/
|
||||
public interface CqlProvider {
|
||||
|
||||
/**
|
||||
* Return the CQL string for this object, i.e. typically the CQL used for creating statements.
|
||||
*
|
||||
* @return the CQL string, or {@literal null}.
|
||||
*/
|
||||
String getCql();
|
||||
}
|
||||
@@ -55,19 +55,7 @@ import org.springframework.dao.QueryTimeoutException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.BoundStatement;
|
||||
import com.datastax.driver.core.CodecRegistry;
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
import com.datastax.driver.core.Host;
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.ProtocolVersion;
|
||||
import com.datastax.driver.core.ResultSet;
|
||||
import com.datastax.driver.core.ResultSetFuture;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.TypeCodec;
|
||||
import com.datastax.driver.core.*;
|
||||
import com.datastax.driver.core.ColumnDefinitions.Definition;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
import com.datastax.driver.core.querybuilder.Batch;
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright 2016 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.*;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link ReactiveSession}. This implementation bridges asynchronous {@link Session} methods
|
||||
* to reactive execution patterns.
|
||||
* <p>
|
||||
* Calls are deferred until a subscriber subscribes to the resulting {@link org.reactivestreams.Publisher}. The calls
|
||||
* are executed by subscribing to {@link ListenableFuture} and returning the result as calls complete.
|
||||
* <p>
|
||||
* {@link ResultSet} implements transparent paging that invokes in the middle of result streaming blocking calls to
|
||||
* Cassandra. {@link DefaultBridgedReactiveSession} uses therefore {@link ReactiveResultSet} to avoid client thread
|
||||
* blocking. Elements are emitted on netty EventLoop threads and transported by the provided {@link Scheduler}. However,
|
||||
* this is an intermediate solution until Datastax can provide a fully reactive driver.
|
||||
* <p>
|
||||
* All CQL operations performed by this class are logged at debug level, using
|
||||
* "org.springframework.cassandra.core.DefaultBridgedReactiveSession" as log category.
|
||||
* <p>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see Mono
|
||||
* @see ReactiveResultSet
|
||||
* @see Scheduler
|
||||
* @see ReactiveSession
|
||||
*/
|
||||
public class DefaultBridgedReactiveSession implements ReactiveSession {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private final Session session;
|
||||
private final Scheduler scheduler;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DefaultBridgedReactiveSession} for a {@link Session} and {@link Scheduler}.
|
||||
*
|
||||
* @param session must not be {@literal null}.
|
||||
* @param scheduler must not be {@literal null}.
|
||||
*/
|
||||
public DefaultBridgedReactiveSession(Session session, Scheduler scheduler) {
|
||||
|
||||
Assert.notNull(session, "Session must not be null");
|
||||
Assert.notNull(scheduler, "Scheduler must not be null");
|
||||
|
||||
this.session = session;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#execute(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> execute(String query) {
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
|
||||
return execute(new SimpleStatement(query));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#execute(java.lang.String, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> execute(String query, Object... values) {
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
|
||||
return execute(new SimpleStatement(query, values));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#execute(java.lang.String, java.util.Map)
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> execute(String query, Map<String, Object> values) {
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
|
||||
return execute(new SimpleStatement(query, values));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#execute(com.datastax.driver.core.Statement)
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> execute(Statement statement) {
|
||||
|
||||
Assert.notNull(statement, "Statement must not be null");
|
||||
|
||||
return Mono.defer(() -> {
|
||||
|
||||
try {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing Statement [{}]", statement);
|
||||
}
|
||||
|
||||
CompletableFuture<ReactiveResultSet> future = new CompletableFuture<>();
|
||||
ResultSetFuture resultSetFuture = session.executeAsync(statement);
|
||||
|
||||
resultSetFuture.addListener(() -> {
|
||||
|
||||
if (resultSetFuture.isDone()) {
|
||||
|
||||
try {
|
||||
future.complete(new DefaultReactiveResultSet(resultSetFuture.getUninterruptibly(), scheduler));
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
}, Runnable::run);
|
||||
|
||||
return Mono.fromFuture(future);
|
||||
} catch (Exception e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
|
||||
}).subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#prepare(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Mono<PreparedStatement> prepare(String query) {
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
|
||||
return prepare(new SimpleStatement(query));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#prepare(com.datastax.driver.core.RegularStatement)
|
||||
*/
|
||||
@Override
|
||||
public Mono<PreparedStatement> prepare(RegularStatement statement) {
|
||||
|
||||
Assert.notNull(statement, "Statement must not be null");
|
||||
|
||||
return Mono.defer(() -> {
|
||||
|
||||
try {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Preparing Statement [{}]", statement);
|
||||
}
|
||||
|
||||
CompletableFuture<PreparedStatement> future = new CompletableFuture<>();
|
||||
ListenableFuture<PreparedStatement> resultSetFuture = session.prepareAsync(statement);
|
||||
|
||||
resultSetFuture.addListener(() -> {
|
||||
|
||||
if (resultSetFuture.isDone()) {
|
||||
try {
|
||||
future.complete(resultSetFuture.get());
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
}, Runnable::run);
|
||||
|
||||
return Mono.fromFuture(future);
|
||||
} catch (Exception e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
|
||||
}).subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#close()
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
session.close();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#isClosed()
|
||||
*/
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return session.isClosed();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveSession#getCluster()
|
||||
*/
|
||||
@Override
|
||||
public Cluster getCluster() {
|
||||
return session.getCluster();
|
||||
}
|
||||
|
||||
private static class DefaultReactiveResultSet implements ReactiveResultSet {
|
||||
|
||||
private final ResultSet resultSet;
|
||||
private final Scheduler scheduler;
|
||||
|
||||
DefaultReactiveResultSet(ResultSet resultSet, Scheduler scheduler) {
|
||||
this.resultSet = resultSet;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveResultSet#rows()
|
||||
*/
|
||||
@Override
|
||||
public Flux<Row> rows() {
|
||||
|
||||
int prefetch = Math.max(1, resultSet.getAvailableWithoutFetching());
|
||||
return Flux.fromIterable(resultSet) //
|
||||
.subscribeOn(scheduler) //
|
||||
.publishOn(Schedulers.immediate(), prefetch); // limit prefetching to available size
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveResultSet#getColumnDefinitions()
|
||||
*/
|
||||
@Override
|
||||
public ColumnDefinitions getColumnDefinitions() {
|
||||
return resultSet.getColumnDefinitions();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveResultSet#wasApplied()
|
||||
*/
|
||||
@Override
|
||||
public boolean wasApplied() {
|
||||
return resultSet.wasApplied();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveResultSet#getExecutionInfo()
|
||||
*/
|
||||
@Override
|
||||
public ExecutionInfo getExecutionInfo() {
|
||||
return resultSet.getExecutionInfo();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveResultSet#getAllExecutionInfo()
|
||||
*/
|
||||
@Override
|
||||
public List<ExecutionInfo> getAllExecutionInfo() {
|
||||
return resultSet.getAllExecutionInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2016 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;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ReactiveSessionFactory}.
|
||||
* <p>
|
||||
* This implementation returns always the same {@link ReactiveSession}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class DefaultReactiveSessionFactory implements ReactiveSessionFactory {
|
||||
|
||||
private final ReactiveSession session;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveRowMapperResultSetExtractor}.
|
||||
*
|
||||
* @param session the {@link ReactiveSession} provides connections to Cassandra, must not be {@literal null}.
|
||||
*/
|
||||
public DefaultReactiveSessionFactory(ReactiveSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveSession getSession() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,31 @@ import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* General callback interface used by the {@link CqlTemplate} and {@link ReactiveCqlTemplate} classes.
|
||||
* <p>
|
||||
* This interface binds values on a {@link PreparedStatement} provided by the {@link CqlTemplate} class, for each of a
|
||||
* number of updates in a batch using the same CQL. Implementations are responsible for setting any necessary
|
||||
* parameters. CQL with placeholders will already have been supplied.
|
||||
* <p>
|
||||
* It's easier to use this interface than {@link PreparedStatementCreator}: The {@link CqlTemplate} will create the
|
||||
* {@link PreparedStatement}, with the callback only being responsible for setting parameter values.
|
||||
* <p>
|
||||
* Implementations <i>do not</i> need to concern themselves with {@link DriverException}s that may be thrown from
|
||||
* operations they attempt. The {@link CqlTemplate} class will catch and handle {@link DriverException} appropriately.
|
||||
*
|
||||
* @author David Webb
|
||||
* @author Mark Paluch
|
||||
* @see CqlTemplate#query(String, PreparedStatementBinder, ResultSetExtractor)
|
||||
* @see ReactiveCqlTemplate#query(String, PreparedStatementBinder, ReactiveResultSetExtractor)
|
||||
*/
|
||||
public interface PreparedStatementBinder {
|
||||
|
||||
/**
|
||||
* Bind parameter values on the given {@link PreparedStatement}.
|
||||
*
|
||||
* @param ps the PreparedStatement to invoke setter methods on
|
||||
* @throws DriverException if a {@link DriverException} is encountered (i.e. there is no need to catch
|
||||
* {@link DriverException})
|
||||
*/
|
||||
BoundStatement bindValues(PreparedStatement ps) throws DriverException;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,709 @@
|
||||
/*
|
||||
* Copyright 2016 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.Map;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.Statement;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Interface specifying a basic set of CQL operations executed in a reactive fashion. Implemented by
|
||||
* {@link ReactiveCqlTemplate}. Not often used directly, but a useful option to enhance testability, as it can easily be
|
||||
* mocked or stubbed.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCqlTemplate
|
||||
* @see Mono
|
||||
* @see Flux
|
||||
*/
|
||||
public interface ReactiveCqlOperations {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with a plain ReactiveSession
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Execute a CQL data access operation, implemented as callback action working on a {@link ReactiveSession}. This
|
||||
* allows for implementing arbitrary data access operations, within Spring's managed CQL environment: that is,
|
||||
* converting CQL {@link com.datastax.driver.core.exceptions.DriverException}s into Spring's
|
||||
* {@link DataAccessException} hierarchy.
|
||||
* <p>
|
||||
* The callback action can return a result object, for example a domain object or a collection of domain objects.
|
||||
*
|
||||
* @param action the callback object that specifies the action.
|
||||
* @return a result object returned by the action, or {@literal null}.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Flux<T> execute(ReactiveSessionCallback<T> action) throws DataAccessException;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with static CQL
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Issue a single CQL execute, typically a DDL statement, insert, update or delete statement.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @return boolean value whether the statement was applied.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
Mono<Boolean> execute(String cql) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query given static CQL, reading the {@link ReactiveResultSet} with a {@link ReactiveResultSetExtractor}.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code query} method with {@literal null} as argument array.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param rse object that will extract all rows of results, must not be {@literal null}.
|
||||
* @return an arbitrary result object, as returned by the ReactiveResultSetExtractor.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #query(String, ReactiveResultSetExtractor, Object...)
|
||||
*/
|
||||
<T> Flux<T> query(String cql, ReactiveResultSetExtractor<T> rse) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query given static CQL, mapping each row to a Java object via a {@link RowMapper}.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code query} method with {@literal null} as argument array.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the result {@link Flux}, containing mapped objects.
|
||||
* @throws DataAccessException if there is any problem executing the query
|
||||
* @see #query(String, RowMapper, Object[])
|
||||
*/
|
||||
<T> Flux<T> query(String cql, RowMapper<T> rowMapper) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query given static CQL, mapping a single result row to a Java object via a {@link RowMapper}.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@link #queryForObject(String, RowMapper, Object...)} method with
|
||||
* {@literal null} as argument array.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the single mapped object.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForObject(String, RowMapper, Object[])
|
||||
*/
|
||||
<T> Mono<T> queryForObject(String cql, RowMapper<T> rowMapper) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result object, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@link #queryForObject(String, Class, Object...)} method with
|
||||
* {@literal null} as argument array.
|
||||
* <p>
|
||||
* This method is useful for running static CQL with a known outcome. The query is expected to be a single row/single
|
||||
* column query; the returned result will be directly mapped to the corresponding object type.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param requiredType the type that the result object is expected to match, must not be {@literal null}.
|
||||
* @return the result object of the required type, or {@link Mono#empty()} in case of CQL NULL.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row, or does not return
|
||||
* exactly one column in that row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForObject(String, Class, Object[])
|
||||
*/
|
||||
<T> Mono<T> queryForObject(String cql, Class<T> requiredType) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result Map, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@link #queryForMap(String, Object...)} method with {@literal null}
|
||||
* as argument array.
|
||||
* <p>
|
||||
* The query is expected to be a single row query; the result row will be mapped to a Map (one entry for each column,
|
||||
* using the column name as the key).
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @return the result Map (one entry for each column, using the column name as the key), must not be {@literal null}.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForMap(String, Object[])
|
||||
* @see ColumnMapRowMapper
|
||||
*/
|
||||
Mono<Map<String, Object>> queryForMap(String cql) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result {@link Flux}, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForFlux} method with {@literal null} as argument array.
|
||||
* <p>
|
||||
* The results will be mapped to a {@link Flux} (one item for each row) of result objects, each of them matching the
|
||||
* specified element type.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param elementType the required type of element in the result {@link Flux} (for example, {@code Integer.class}),
|
||||
* must not be {@literal null}.
|
||||
* @return a {@link Flux} of objects that match the specified element type.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForFlux(String, Class, Object[])
|
||||
* @see SingleColumnRowMapper
|
||||
*/
|
||||
<T> Flux<T> queryForFlux(String cql, Class<T> elementType) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result {@link Flux}, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForFlux} method with {@literal null} as argument array.
|
||||
* <p>
|
||||
* The results will be mapped to a {@link Flux} (one item for each row) of {@link Map}s (one entry for each column
|
||||
* using the column name as the key). Each item in the {@link Flux} will be of the form returned by this interface's
|
||||
* queryForMap() methods.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @return a {@link Flux} that contains a {@link Map} per row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForFlux(String, Object[])
|
||||
*/
|
||||
Flux<Map<String, Object>> queryForFlux(String cql) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a ResultSet, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForResultSet} method with {@literal null} as argument
|
||||
* array.
|
||||
* <p>
|
||||
* The results will be mapped to an {@link ReactiveResultSet}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @return a {@link ReactiveResultSet} representation.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForResultSet(String, Object[])
|
||||
*/
|
||||
Mono<ReactiveResultSet> queryForResultSet(String cql) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for Rows, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForResultSet} method with {@literal null} as argument
|
||||
* array.
|
||||
* <p>
|
||||
* The results will be mapped to {@link Row}s.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @return a Row representation.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForResultSet(String, Object[])
|
||||
*/
|
||||
Flux<Row> queryForRows(String cql) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Issue multiple CQL statements from a CQL statement {@link Publisher}.
|
||||
*
|
||||
* @param statementPublisher defining a {@link Publisher} of CQL statements that will be executed.
|
||||
* @return an array of the number of rows affected by each statement
|
||||
* @throws DataAccessException if there is any problem executing the batch
|
||||
*/
|
||||
Flux<Boolean> execute(Publisher<String> statementPublisher) throws DataAccessException;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with com.datastax.driver.core.Statement
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Issue a single CQL execute, typically a DDL statement, insert, update or delete statement.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @return boolean value whether the statement was applied.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
Mono<Boolean> execute(Statement statement) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query given static CQL, reading the {@link ReactiveResultSet} with a {@link ReactiveResultSetExtractor}.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code query} method with {@literal null} as argument array.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @param rse object that will extract all rows of results, must not be {@literal null}.
|
||||
* @return an arbitrary result object, as returned by the ReactiveResultSetExtractor.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #query(String, ReactiveResultSetExtractor, Object...)
|
||||
*/
|
||||
<T> Flux<T> query(Statement statement, ReactiveResultSetExtractor<T> rse) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query given static CQL, mapping each row to a Java object via a {@link RowMapper}.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code query} method with {@literal null} as argument array.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the result {@link Flux}, containing mapped objects.
|
||||
* @throws DataAccessException if there is any problem executing the query
|
||||
* @see #query(String, RowMapper, Object[])
|
||||
*/
|
||||
<T> Flux<T> query(Statement statement, RowMapper<T> rowMapper) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query given static CQL, mapping a single result row to a Java object via a {@link RowMapper}.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@link #queryForObject(String, RowMapper, Object...)} method with
|
||||
* {@literal null} as argument array.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the single mapped object.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForObject(String, RowMapper, Object[])
|
||||
*/
|
||||
<T> Mono<T> queryForObject(Statement statement, RowMapper<T> rowMapper) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result object, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@link #queryForObject(String, Class, Object...)} method with
|
||||
* {@literal null} as argument array.
|
||||
* <p>
|
||||
* This method is useful for running static CQL with a known outcome. The query is expected to be a single row/single
|
||||
* column query; the returned result will be directly mapped to the corresponding object type.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @param requiredType the type that the result object is expected to match, must not be {@literal null}.
|
||||
* @return the result object of the required type, or {@link Mono#empty()} in case of CQL NULL.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row, or does not return
|
||||
* exactly one column in that row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForObject(String, Class, Object[])
|
||||
*/
|
||||
<T> Mono<T> queryForObject(Statement statement, Class<T> requiredType) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result Map, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@link #queryForMap(String, Object...)} method with {@literal null}
|
||||
* as argument array.
|
||||
* <p>
|
||||
* The query is expected to be a single row query; the result row will be mapped to a Map (one entry for each column,
|
||||
* using the column name as the key).
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @return the result Map (one entry for each column, using the column name as the key), must not be {@literal null}.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForMap(String, Object[])
|
||||
* @see ColumnMapRowMapper
|
||||
*/
|
||||
Mono<Map<String, Object>> queryForMap(Statement statement) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result {@link Flux}, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForFlux} method with {@literal null} as argument array.
|
||||
* <p>
|
||||
* The results will be mapped to a {@link Flux} (one item for each row) of result objects, each of them matching the
|
||||
* specified element type.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @param elementType the required type of element in the result {@link Flux} (for example, {@code Integer.class}),
|
||||
* must not be {@literal null}.
|
||||
* @return a {@link Flux} of objects that match the specified element type.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForFlux(String, Class, Object[])
|
||||
* @see SingleColumnRowMapper
|
||||
*/
|
||||
<T> Flux<T> queryForFlux(Statement statement, Class<T> elementType) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a result {@link Flux}, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForFlux} method with {@literal null} as argument array.
|
||||
* <p>
|
||||
* The results will be mapped to a {@link Flux} (one item for each row) of {@link Map}s (one entry for each column
|
||||
* using the column name as the key). Each item in the {@link Flux} will be of the form returned by this interface's
|
||||
* queryForMap() methods.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @return a {@link Flux} that contains a {@link Map} per row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForFlux(String, Object[])
|
||||
*/
|
||||
Flux<Map<String, Object>> queryForFlux(Statement statement) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for a ResultSet, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForResultSet} method with {@literal null} as argument
|
||||
* array.
|
||||
* <p>
|
||||
* The results will be mapped to an {@link ReactiveResultSet}.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @return a {@link ReactiveResultSet} representation.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForResultSet(String, Object[])
|
||||
*/
|
||||
Mono<ReactiveResultSet> queryForResultSet(Statement statement) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a query for Rows, given static CQL.
|
||||
* <p>
|
||||
* Uses a CQL Statement, not a {@link PreparedStatement}. If you want to execute a static query with a
|
||||
* {@link PreparedStatement}, use the overloaded {@code queryForResultSet} method with {@literal null} as argument
|
||||
* array.
|
||||
* <p>
|
||||
* The results will be mapped to {@link Row}s.
|
||||
*
|
||||
* @param statement static CQL {@link Statement}, must not be {@literal null}.
|
||||
* @return a Row representation.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForResultSet(String, Object[])
|
||||
*/
|
||||
Flux<Row> queryForRows(Statement statement) throws DataAccessException;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with prepared statements
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Execute a CQL data access operation, implemented as callback action working on a CQL {@link PreparedStatement}.
|
||||
* This allows for implementing arbitrary data access operations on a single {@link PreparedStatement}, within
|
||||
* Spring's managed CQL environment: that is, participating in Spring-managed transactions and converting CQL
|
||||
* {@link com.datastax.driver.core.exceptions.DriverException}s into Spring's {@link DataAccessException} hierarchy.
|
||||
* <p>
|
||||
* The callback action can return a result object, for example a domain object or a collection of domain objects.
|
||||
*
|
||||
* @param psc object that can create a {@link PreparedStatement} given a {@link ReactiveSession}, must not be
|
||||
* {@literal null}.
|
||||
* @param action callback object that specifies the action, must not be {@literal null}.
|
||||
* @return a result object returned by the action, or {@literal null}.
|
||||
* @throws DataAccessException if there is any problem
|
||||
*/
|
||||
<T> Flux<T> execute(ReactivePreparedStatementCreator psc, ReactivePreparedStatementCallback<T> action)
|
||||
throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a CQL data access operation, implemented as callback action working on a CQL {@link PreparedStatement}.
|
||||
* This allows for implementing arbitrary data access operations on a single Statement, within Spring's managed CQL
|
||||
* environment: that is, participating in Spring-managed transactions and converting CQL
|
||||
* {@link com.datastax.driver.core.exceptions.DriverException}s into Spring's {@link DataAccessException} hierarchy.
|
||||
* <p>
|
||||
* The callback action can return a result object, for example a domain object or a collection of domain objects.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param action callback object that specifies the action, must not be {@literal null}.
|
||||
* @return a result object returned by the action, or {@literal null}
|
||||
* @throws DataAccessException if there is any problem
|
||||
*/
|
||||
<T> Flux<T> execute(String cql, ReactivePreparedStatementCallback<T> action) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query using a prepared statement, reading the {@link ReactiveResultSet} with a {@link ReactiveResultSetExtractor}.
|
||||
*
|
||||
* @param psc object that can create a {@link PreparedStatement} given a {@link ReactiveSession}, must not be
|
||||
* {@literal null}.
|
||||
* @param rse object that will extract results, must not be {@literal null}.
|
||||
* @return an arbitrary result object, as returned by the {@link ReactiveResultSetExtractor}
|
||||
* @throws DataAccessException if there is any problem
|
||||
*/
|
||||
<T> Flux<T> query(ReactivePreparedStatementCreator psc, ReactiveResultSetExtractor<T> rse) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query using a prepared statement, reading the {@link ReactiveResultSet} with a {@link ReactiveResultSetExtractor}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param psb object that knows how to set values on the prepared statement. If this is {@literal null}, the CQL will
|
||||
* be assumed to contain no bind parameters. Even if there are no bind parameters, this object may be used to
|
||||
* set fetch size and other performance options.
|
||||
* @param rse object that will extract results, must not be {@literal null}.
|
||||
* @return an arbitrary result object, as returned by the {@link ReactiveResultSetExtractor}.
|
||||
* @throws DataAccessException if there is any problem
|
||||
*/
|
||||
<T> Flux<T> query(String cql, PreparedStatementBinder psb, ReactiveResultSetExtractor<T> rse)
|
||||
throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query using a prepared statement and a {@link PreparedStatementBinder} implementation that knows how to bind values
|
||||
* to the query, reading the {@link ReactiveResultSet} with a {@link ResultSetExtractor}.
|
||||
*
|
||||
* @param psc object that can create a {@link PreparedStatement} given a {@link com.datastax.driver.core.Session},
|
||||
* must not be {@literal null}.
|
||||
* @param psb object that knows how to set values on the prepared statement. If this is {@literal null}, the CQL will
|
||||
* be assumed to contain no bind parameters. Even if there are no bind parameters, this object may be used to
|
||||
* set fetch size and other performance options.
|
||||
* @param rse object that will extract results, must not be {@literal null}.
|
||||
* @return an arbitrary result object, as returned by the {@link ResultSetExtractor}.
|
||||
* @throws DataAccessException if there is any problem
|
||||
*/
|
||||
<T> Flux<T> query(ReactivePreparedStatementCreator psc, PreparedStatementBinder psb,
|
||||
ReactiveResultSetExtractor<T> rse) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, reading the
|
||||
* {@link ReactiveResultSet} with a {@link ReactiveResultSetExtractor}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param rse object that will extract results, must not be {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return an arbitrary result object, as returned by the {@link ReactiveResultSetExtractor}
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Flux<T> query(String cql, ReactiveResultSetExtractor<T> rse, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query using a prepared statement, mapping each row to a Java object via a {@link RowMapper}.
|
||||
*
|
||||
* @param psc object that can create a {@link PreparedStatement} given a {@link ReactiveSession}, must not be
|
||||
* {@literal null}.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the result {@link Flux}, containing mapped objects.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Flux<T> query(ReactivePreparedStatementCreator psc, RowMapper<T> rowMapper) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a {@link PreparedStatement}Binder implementation that
|
||||
* knows how to bind values to the query, mapping each row to a Java object via a {@link RowMapper}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param psb object that knows how to set values on the prepared statement. If this is {@literal null}, the CQL will
|
||||
* be assumed to contain no bind parameters. Even if there are no bind parameters, this object may be used to
|
||||
* set fetch size and other performance options.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the result {@link Flux}, containing mapped objects.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Flux<T> query(String cql, PreparedStatementBinder psb, RowMapper<T> rowMapper) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query using a prepared statement and a {@link PreparedStatementBinder} implementation that knows how to bind values
|
||||
* to the query, mapping each row to a Java object via a {@link RowMapper}.
|
||||
*
|
||||
* @param psc object that can create a {@link PreparedStatement} given a {@link com.datastax.driver.core.Session},
|
||||
* must not be {@literal null}.
|
||||
* @param psb object that knows how to set values on the prepared statement. If this is {@literal null}, the CQL will
|
||||
* be assumed to contain no bind parameters. Even if there are no bind parameters, this object may be used to
|
||||
* set fetch size and other performance options.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @return the result {@link Flux}, containing mapped objects.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Flux<T> query(ReactivePreparedStatementCreator psc, PreparedStatementBinder psb, RowMapper<T> rowMapper)
|
||||
throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, mapping each
|
||||
* row to a Java object via a {@link RowMapper}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param rowMapper object that will map one object per row
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type)
|
||||
* @return the result {@link Flux}, containing mapped objects
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Flux<T> query(String cql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, mapping a
|
||||
* single result row to a Java object via a {@link RowMapper}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param rowMapper object that will map one object per row, must not be {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type)
|
||||
* @return the single mapped object
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
*/
|
||||
<T> Mono<T> queryForObject(String cql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, expecting a
|
||||
* result object.
|
||||
* <p>
|
||||
* The query is expected to be a single row/single column query; the returned result will be directly mapped to the
|
||||
* corresponding object type.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param requiredType the type that the result object is expected to match, must not be {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the PreparedStatement to guess the corresponding CQL
|
||||
* type)
|
||||
* @return the result object of the required type, or {@link Mono#empty()} in case of CQL NULL.
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row, or does not return
|
||||
* exactly one column in that row.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForObject(String, Class)
|
||||
*/
|
||||
<T> Mono<T> queryForObject(String cql, Class<T> requiredType, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, expecting a
|
||||
* result Map. The queryForMap() methods defined by this interface are appropriate when you don't have a domain model.
|
||||
* Otherwise, consider using one of the queryForObject() methods.
|
||||
* <p>
|
||||
* The query is expected to be a single row query; the result row will be mapped to a Map (one entry for each column,
|
||||
* using the column name as the key).
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return the result Map (one entry for each column, using the column name as the key).
|
||||
* @throws IncorrectResultSizeDataAccessException if the query does not return exactly one row
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForMap(String)
|
||||
* @see ColumnMapRowMapper
|
||||
*/
|
||||
Mono<Map<String, Object>> queryForMap(String cql, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, expecting a
|
||||
* result {@link Flux}.
|
||||
* <p>
|
||||
* The results will be mapped to a {@link Flux} (one item for each row) of result objects, each of them matching the
|
||||
* specified element type.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param elementType the required type of element in the result {@link Flux} (for example, {@code Integer.class}),
|
||||
* must not be {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return a {@link Flux} of objects that match the specified element type.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForFlux(String, Class)
|
||||
* @see SingleColumnRowMapper
|
||||
*/
|
||||
<T> Flux<T> queryForFlux(String cql, Class<T> elementType, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, expecting a
|
||||
* result {@link Flux}.
|
||||
* <p>
|
||||
* The results will be mapped to a {@link Flux} (one item for each row) of {@link Map}s (one entry for each column,
|
||||
* using the column name as the key). Each item in the {@link Flux} will be of the form returned by this interface's
|
||||
* queryForMap() methods.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return a {@link Flux} that contains a {@link Map} per row
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForFlux(String)
|
||||
*/
|
||||
Flux<Map<String, Object>> queryForFlux(String cql, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, expecting a
|
||||
* ResultSet.
|
||||
* <p>
|
||||
* The results will be mapped to an {@link ReactiveResultSet}.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return a {@link ReactiveResultSet} representation.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForResultSet(String)
|
||||
*/
|
||||
Mono<ReactiveResultSet> queryForResultSet(String cql, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Query given CQL to create a prepared statement from CQL and a list of arguments to bind to the query, expecting
|
||||
* Rows.
|
||||
* <p>
|
||||
* The results will be mapped to {@link Row}s.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return a {@link Row} representation.
|
||||
* @throws DataAccessException if there is any problem executing the query.
|
||||
* @see #queryForResultSet(String)
|
||||
*/
|
||||
Flux<Row> queryForRows(String cql, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Issue a single CQL execute operation (such as an insert, update or delete statement) using a
|
||||
* {@link ReactivePreparedStatementCreator} to provide CQL and any required parameters.
|
||||
*
|
||||
* @param psc object that provides CQL and any necessary parameters, must not be {@literal null}.
|
||||
* @return boolean value whether the statement was applied.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
// TODO: Interferes with execute(session callback lambda)
|
||||
Mono<Boolean> execute(ReactivePreparedStatementCreator psc) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Issue an statement using a {@link PreparedStatementBinder} to set bind parameters, with given CQL. Simpler than
|
||||
* using a {@link ReactivePreparedStatementCreator} as this method will create the {@link PreparedStatement}: The
|
||||
* {@link PreparedStatementBinder} just needs to set parameters.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param psb object that knows how to set values on the prepared statement. If this is {@literal null}, the CQL will
|
||||
* be assumed to contain no bind parameters. Even if there are no bind parameters, this object may be used to
|
||||
* set fetch size and other performance options.
|
||||
* @return boolean value whether the statement was applied.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Mono<Boolean> execute(String cql, PreparedStatementBinder psb) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Issue a single CQL operation (such as an insert, update or delete statement) via a prepared statement, binding the
|
||||
* given arguments.
|
||||
*
|
||||
* @param cql static CQL to execute, must not be empty or {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return boolean value whether the statement was applied.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Mono<Boolean> execute(String cql, Object... args) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Issue a single CQL operation (such as an insert, update or delete statement) via a prepared statement, binding the
|
||||
* given arguments.
|
||||
*
|
||||
* @param cql static CQL to execute containing bind parameters, must not be empty or {@literal null}.
|
||||
* @param args arguments to bind to the query (leaving it to the {@link PreparedStatement} to guess the corresponding
|
||||
* CQL type).
|
||||
* @return boolean value whether the statement was applied.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Flux<Boolean> execute(String cql, Publisher<Object[]> args) throws DataAccessException;
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
/*
|
||||
* Copyright 2016 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.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.cassandra.support.ReactiveCassandraAccessor;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.support.DataAccessUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.BoundStatement;
|
||||
import com.datastax.driver.core.ConsistencyLevel;
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
import com.datastax.driver.core.policies.RetryPolicy;
|
||||
import com.datastax.driver.core.querybuilder.QueryBuilder;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* <b>This is the central class in the CQL core package for reactive Cassandra data access.</b> It simplifies the use of
|
||||
* CQL and helps to avoid common errors. It executes core CQL workflow, leaving application code to provide CQL and
|
||||
* extract results. This class executes CQL queries or updates, initiating iteration over {@link ReactiveResultSet}s and
|
||||
* catching {@link DriverException} exceptions and translating them to the generic, more informative exception hierarchy
|
||||
* defined in the {@code org.springframework.dao} package.
|
||||
* <p>
|
||||
* Code using this class need only implement callback interfaces, giving them a clearly defined contract. The
|
||||
* {@link PreparedStatementCreator} callback interface creates a prepared statement given a Connection, providing CQL
|
||||
* and any necessary parameters. The {@link ResultSetExtractor} interface extracts values from a
|
||||
* {@link ReactiveResultSet}. See also {@link PreparedStatementBinder} and {@link RowMapper} for two popular alternative
|
||||
* callback interfaces.
|
||||
* <p>
|
||||
* Can be used within a service implementation via direct instantiation with a {@link ReactiveSessionFactory} reference,
|
||||
* or get prepared in an application context and given to services as bean reference. Note: The
|
||||
* {@link ReactiveSessionFactory} should always be configured as a bean in the application context, in the first case
|
||||
* given to the service directly, in the second case to the prepared template.
|
||||
* <p>
|
||||
* Because this class is parameterizable by the callback interfaces and the
|
||||
* {@link org.springframework.dao.support.PersistenceExceptionTranslator} interface, there should be no need to subclass
|
||||
* it.
|
||||
* <p>
|
||||
* All CQL operations performed by this class are logged at debug level, using
|
||||
* "org.springframework.cassandra.core.ReactiveCqlTemplate" as log category.
|
||||
* <p>
|
||||
* <b>NOTE: An instance of this class is thread-safe once configured.</b>
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see PreparedStatementCreator
|
||||
* @see PreparedStatementBinder
|
||||
* @see PreparedStatementCallback
|
||||
* @see ResultSetExtractor
|
||||
* @see RowCallbackHandler
|
||||
* @see RowMapper
|
||||
* @see org.springframework.dao.support.PersistenceExceptionTranslator
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class ReactiveCqlTemplate extends ReactiveCassandraAccessor implements ReactiveCqlOperations {
|
||||
|
||||
/**
|
||||
* Placeholder for default values.
|
||||
*/
|
||||
private final static Statement DEFAULTS = QueryBuilder.select().from("DEFAULT");
|
||||
|
||||
/**
|
||||
* If this variable is set to a non-negative value, it will be used for setting the {@code fetchSize} property on
|
||||
* statements used for query processing.
|
||||
*/
|
||||
private int fetchSize = -1;
|
||||
|
||||
/**
|
||||
* If this variable is set to a value, it will be used for setting the {@code retryPolicy} property on statements used
|
||||
* for query processing.
|
||||
*/
|
||||
private RetryPolicy retryPolicy;
|
||||
|
||||
/**
|
||||
* If this variable is set to a value, it will be used for setting the {@code consistencyLevel} property on statements
|
||||
* used for query processing.
|
||||
*/
|
||||
private com.datastax.driver.core.ConsistencyLevel consistencyLevel;
|
||||
|
||||
/**
|
||||
* Construct a new {@link ReactiveCqlTemplate Note: The {@link ReactiveSessionFactory} has to be set before using the
|
||||
* instance.
|
||||
*
|
||||
* @see #setSessionFactory
|
||||
*/
|
||||
public ReactiveCqlTemplate() {}
|
||||
|
||||
/**
|
||||
* Construct a new {@link ReactiveCqlTemplate}, given a {@link ReactiveSession}.
|
||||
*
|
||||
* @param reactiveSession the {@link ReactiveSession}, must not be {@literal null}.
|
||||
*/
|
||||
public ReactiveCqlTemplate(ReactiveSession reactiveSession) {
|
||||
|
||||
Assert.notNull(reactiveSession, "ReactiveSession must not be null");
|
||||
|
||||
setSessionFactory(new DefaultReactiveSessionFactory(reactiveSession));
|
||||
afterPropertiesSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@link ReactiveCqlTemplate}, given a {@link ReactiveSessionFactory} to obtain
|
||||
* {@link ReactiveSession}s from.
|
||||
*
|
||||
* @param reactiveSessionFactory the {@link ReactiveSessionFactory} to obtain {@link ReactiveSession}s from, must not
|
||||
* be {@literal null}.
|
||||
*/
|
||||
public ReactiveCqlTemplate(ReactiveSessionFactory reactiveSessionFactory) {
|
||||
setSessionFactory(reactiveSessionFactory);
|
||||
afterPropertiesSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fetch size for this {@link ReactiveCqlTemplate}. This is important for processing large result sets:
|
||||
* Setting this higher than the default value will increase processing speed at the cost of memory consumption;
|
||||
* setting this lower can avoid transferring row data that will never be read by the application. Default is -1,
|
||||
* indicating to use the CQL driver's default configuration (i.e. to not pass a specific fetch size setting on to the
|
||||
* driver).
|
||||
*
|
||||
* @see Statement#setFetchSize(int)
|
||||
*/
|
||||
public void setFetchSize(int fetchSize) {
|
||||
this.fetchSize = fetchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the fetch size specified for this {@link ReactiveCqlTemplate}.
|
||||
*/
|
||||
public int getFetchSize() {
|
||||
return this.fetchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the retry policy for this {@link ReactiveCqlTemplate}. This is important for defining behavior when a request
|
||||
* fails.
|
||||
*
|
||||
* @see Statement#setRetryPolicy(RetryPolicy)
|
||||
* @see RetryPolicy
|
||||
*/
|
||||
public void setRetryPolicy(RetryPolicy retryPolicy) {
|
||||
this.retryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link RetryPolicy} specified for this {@link ReactiveCqlTemplate}.
|
||||
*/
|
||||
public RetryPolicy getRetryPolicy() {
|
||||
return retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the consistency level for this {@link ReactiveCqlTemplate}. Consistency level defines the number of nodes
|
||||
* involved into query processing. Relaxed consistency level settings use fewer nodes but eventual consistency is more
|
||||
* likely to occur while a higher consistency level involves more nodes to obtain results with a higher consistency
|
||||
* guarantee.
|
||||
*
|
||||
* @see Statement#setConsistencyLevel(ConsistencyLevel)
|
||||
* @see RetryPolicy
|
||||
*/
|
||||
public void setConsistencyLevel(ConsistencyLevel consistencyLevel) {
|
||||
this.consistencyLevel = consistencyLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link ConsistencyLevel} specified for this {@link ReactiveCqlTemplate}.
|
||||
*/
|
||||
public ConsistencyLevel getConsistencyLevel() {
|
||||
return consistencyLevel;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with a plain org.springframework.cassandra.core.ReactiveSession
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(org.springframework.cassandra.core.ReactiveSessionCallback)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> execute(ReactiveSessionCallback<T> action) throws DataAccessException {
|
||||
|
||||
Assert.notNull(action, "Callback object must not be null");
|
||||
|
||||
return createFlux(action).onErrorResumeWith(translateException("ReactiveSessionCallback", getCql(action)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with static CQL
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> execute(String cql) throws DataAccessException {
|
||||
|
||||
Assert.hasText(cql, "CQL must not be empty");
|
||||
|
||||
return queryForResultSet(cql).map(ReactiveResultSet::wasApplied);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(java.lang.String, org.springframework.cassandra.core.ReactiveResultSetExtractor)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(String cql, ReactiveResultSetExtractor<T> rse) throws DataAccessException {
|
||||
|
||||
Assert.hasText(cql, "CQL must not be empty");
|
||||
Assert.notNull(rse, "ReactiveResultSetExtractor must not be null");
|
||||
|
||||
return createFlux(new SimpleStatement(cql), (session, stmt) -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing CQL Statement [{}]", cql);
|
||||
}
|
||||
|
||||
return session.execute(stmt).flatMap(rse::extractData);
|
||||
}).onErrorResumeWith(translateException("Query", cql));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(java.lang.String, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(String cql, RowMapper<T> rowMapper) throws DataAccessException {
|
||||
return query(cql, new ReactiveRowMapperResultSetExtractor<>(rowMapper));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForObject(java.lang.String, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> queryForObject(String cql, RowMapper<T> rowMapper) throws DataAccessException {
|
||||
return query(cql, rowMapper).buffer(2).flatMap(list -> Mono.just(DataAccessUtils.requiredSingleResult(list)))
|
||||
.next();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForObject(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> queryForObject(String cql, Class<T> requiredType) throws DataAccessException {
|
||||
return queryForObject(cql, getSingleColumnRowMapper(requiredType));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForMap(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Map<String, Object>> queryForMap(String cql) throws DataAccessException {
|
||||
return queryForObject(cql, getColumnMapRowMapper());
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForFlux(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> queryForFlux(String cql, Class<T> elementType) throws DataAccessException {
|
||||
return query(cql, getSingleColumnRowMapper(elementType));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForFlux(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Flux<Map<String, Object>> queryForFlux(String cql) throws DataAccessException {
|
||||
return query(cql, getColumnMapRowMapper());
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForResultSet(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> queryForResultSet(String cql) throws DataAccessException {
|
||||
|
||||
Assert.hasText(cql, "CQL must not be empty");
|
||||
|
||||
return createMono(new SimpleStatement(cql), (session, statement) -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing CQL [{}]", cql);
|
||||
|
||||
}
|
||||
return session.execute(statement);
|
||||
}).otherwise(translateException("QueryForResultSet", cql));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForRows(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Flux<Row> queryForRows(String cql) throws DataAccessException {
|
||||
return queryForResultSet(cql).flatMap(ReactiveResultSet::rows)
|
||||
.onErrorResumeWith(translateException("QueryForRows", cql));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(org.reactivestreams.Publisher)
|
||||
*/
|
||||
@Override
|
||||
public Flux<Boolean> execute(Publisher<String> statementPublisher) throws DataAccessException {
|
||||
|
||||
Assert.notNull(statementPublisher, "CQL Publisher must not be null");
|
||||
|
||||
return Flux.from(statementPublisher).flatMap(this::execute);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with com.datastax.driver.core.Statement
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(com.datastax.driver.core.Statement)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> execute(Statement statement) throws DataAccessException {
|
||||
|
||||
Assert.notNull(statement, "CQL Statement must not be null");
|
||||
|
||||
return queryForResultSet(statement).map(ReactiveResultSet::wasApplied);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(com.datastax.driver.core.Statement, org.springframework.cassandra.core.ReactiveResultSetExtractor)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(Statement statement, ReactiveResultSetExtractor<T> rse) throws DataAccessException {
|
||||
|
||||
Assert.notNull(statement, "CQL Statement must not be null");
|
||||
Assert.notNull(rse, "ReactiveResultSetExtractor must not be null");
|
||||
|
||||
return createFlux(statement, (session, stmt) -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing CQL Statement [{}]", statement);
|
||||
}
|
||||
|
||||
return session.execute(stmt).flatMap(rse::extractData);
|
||||
}).onErrorResumeWith(translateException("Query", statement.toString()));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(com.datastax.driver.core.Statement, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(Statement statement, RowMapper<T> rowMapper) throws DataAccessException {
|
||||
return query(statement, new ReactiveRowMapperResultSetExtractor<>(rowMapper));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForObject(com.datastax.driver.core.Statement, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> queryForObject(Statement statement, RowMapper<T> rowMapper) throws DataAccessException {
|
||||
return query(statement, rowMapper).buffer(2).flatMap(list -> Mono.just(DataAccessUtils.requiredSingleResult(list)))
|
||||
.next();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForObject(com.datastax.driver.core.Statement, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> queryForObject(Statement statement, Class<T> requiredType) throws DataAccessException {
|
||||
return queryForObject(statement, getSingleColumnRowMapper(requiredType));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForMap(com.datastax.driver.core.Statement)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Map<String, Object>> queryForMap(Statement statement) throws DataAccessException {
|
||||
return queryForObject(statement, getColumnMapRowMapper());
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForFlux(com.datastax.driver.core.Statement, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> queryForFlux(Statement statement, Class<T> elementType) throws DataAccessException {
|
||||
return query(statement, getSingleColumnRowMapper(elementType));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForFlux(com.datastax.driver.core.Statement)
|
||||
*/
|
||||
@Override
|
||||
public Flux<Map<String, Object>> queryForFlux(Statement statement) throws DataAccessException {
|
||||
return query(statement, getColumnMapRowMapper());
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForResultSet(com.datastax.driver.core.Statement)
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> queryForResultSet(Statement statement) throws DataAccessException {
|
||||
|
||||
Assert.notNull(statement, "CQL Statement must not be null");
|
||||
|
||||
return createMono(statement, (session, executedStatement) -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing CQL [{}]", executedStatement);
|
||||
|
||||
}
|
||||
return session.execute(executedStatement);
|
||||
}).otherwise(translateException("QueryForResultSet", statement.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Row> queryForRows(Statement statement) throws DataAccessException {
|
||||
return queryForResultSet(statement).flatMap(ReactiveResultSet::rows)
|
||||
.onErrorResumeWith(translateException("QueryForRows", statement.toString()));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with prepared statements
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(org.springframework.cassandra.core.ReactivePreparedStatementCreator, org.springframework.cassandra.core.ReactivePreparedStatementCallback)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> execute(ReactivePreparedStatementCreator psc, ReactivePreparedStatementCallback<T> action)
|
||||
throws DataAccessException {
|
||||
|
||||
Assert.notNull(psc, "ReactivePreparedStatementCreator must not be null");
|
||||
Assert.notNull(action, "ReactivePreparedStatementCallback object must not be null");
|
||||
|
||||
return createFlux(session -> {
|
||||
|
||||
logger.debug("Preparing statement [{}] using {}", getCql(psc), psc);
|
||||
|
||||
return psc.createPreparedStatement(session).doOnNext(this::applyStatementSettings)
|
||||
.flatMap(ps -> action.doInPreparedStatement(session, ps));
|
||||
}).onErrorResumeWith(translateException("ReactivePreparedStatementCallback", getCql(psc)));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(java.lang.String, org.springframework.cassandra.core.ReactivePreparedStatementCallback)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> execute(String cql, ReactivePreparedStatementCallback<T> action) throws DataAccessException {
|
||||
return execute(new SimpleReactivePreparedStatementCreator(cql), action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query using a prepared statement, reading the {@link ReactiveResultSet} with a {@link ReactiveResultSetExtractor}.
|
||||
*
|
||||
* @param psc object that can create a {@link PreparedStatement} given a {@link ReactiveSession}
|
||||
* @param psb object that knows how to set values on the prepared statement. If this is {@literal null}, the CQL will
|
||||
* be assumed to contain no bind parameters.
|
||||
* @param rse object that will extract results
|
||||
* @return an arbitrary result object, as returned by the {@link ReactiveResultSetExtractor}
|
||||
* @throws DataAccessException if there is any problem
|
||||
*/
|
||||
public <T> Flux<T> query(ReactivePreparedStatementCreator psc, PreparedStatementBinder psb,
|
||||
ReactiveResultSetExtractor<T> rse) throws DataAccessException {
|
||||
|
||||
Assert.notNull(psc, "ReactivePreparedStatementCreator must not be null");
|
||||
Assert.notNull(rse, "ReactiveResultSetExtractor object must not be null");
|
||||
|
||||
return execute(psc, (session, ps) -> Mono.just(ps).flatMap(pps -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing Prepared CQL Statement [{}]", ps.getQueryString());
|
||||
}
|
||||
|
||||
BoundStatement boundStatement = psb != null ? psb.bindValues(ps) : ps.bind();
|
||||
|
||||
applyStatementSettings(boundStatement);
|
||||
return session.execute(boundStatement);
|
||||
}).flatMap(rse::extractData)).onErrorResumeWith(translateException("Query", getCql(psc)));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(org.springframework.cassandra.core.ReactivePreparedStatementCreator, org.springframework.cassandra.core.ReactiveResultSetExtractor)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(ReactivePreparedStatementCreator psc, ReactiveResultSetExtractor<T> rse)
|
||||
throws DataAccessException {
|
||||
return query(psc, null, rse);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(java.lang.String, org.springframework.cassandra.core.PreparedStatementBinder, org.springframework.cassandra.core.ReactiveResultSetExtractor)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(String cql, PreparedStatementBinder psb, ReactiveResultSetExtractor<T> rse)
|
||||
throws DataAccessException {
|
||||
return query(new SimpleReactivePreparedStatementCreator(cql), psb, rse);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(java.lang.String, org.springframework.cassandra.core.ReactiveResultSetExtractor, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(String cql, ReactiveResultSetExtractor<T> rse, Object... args) throws DataAccessException {
|
||||
return query(new SimpleReactivePreparedStatementCreator(cql), newArgPreparedStatementBinder(args), rse);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(org.springframework.cassandra.core.ReactivePreparedStatementCreator, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(ReactivePreparedStatementCreator psc, RowMapper<T> rowMapper) throws DataAccessException {
|
||||
return query(psc, null, new ReactiveRowMapperResultSetExtractor<>(rowMapper));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(java.lang.String, org.springframework.cassandra.core.PreparedStatementBinder, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(String cql, PreparedStatementBinder psb, RowMapper<T> rowMapper) throws DataAccessException {
|
||||
return query(cql, psb, new ReactiveRowMapperResultSetExtractor<>(rowMapper));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(org.springframework.cassandra.core.ReactivePreparedStatementCreator, org.springframework.cassandra.core.PreparedStatementBinder, org.springframework.cassandra.core.RowMapper)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(ReactivePreparedStatementCreator psc, PreparedStatementBinder psb, RowMapper<T> rowMapper)
|
||||
throws DataAccessException {
|
||||
return query(psc, psb, new ReactiveRowMapperResultSetExtractor<>(rowMapper));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#query(java.lang.String, org.springframework.cassandra.core.RowMapper, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> query(String cql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
|
||||
return query(cql, newArgPreparedStatementBinder(args), rowMapper);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForObject(java.lang.String, org.springframework.cassandra.core.RowMapper, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> queryForObject(String cql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
|
||||
return query(cql, rowMapper, args).buffer(2).flatMap(list -> Mono.just(DataAccessUtils.requiredSingleResult(list)))
|
||||
.next();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForObject(java.lang.String, java.lang.Class, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> queryForObject(String cql, Class<T> requiredType, Object... args) throws DataAccessException {
|
||||
return queryForObject(cql, getSingleColumnRowMapper(requiredType), args);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForMap(java.lang.String, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Mono<Map<String, Object>> queryForMap(String cql, Object... args) throws DataAccessException {
|
||||
return queryForObject(cql, getColumnMapRowMapper(), args);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForFlux(java.lang.String, java.lang.Class, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> queryForFlux(String cql, Class<T> elementType, Object... args) throws DataAccessException {
|
||||
return query(cql, getSingleColumnRowMapper(elementType), args);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForFlux(java.lang.String, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Flux<Map<String, Object>> queryForFlux(String cql, Object... args) throws DataAccessException {
|
||||
return query(cql, getColumnMapRowMapper(), args);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForResultSet(java.lang.String, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Mono<ReactiveResultSet> queryForResultSet(String cql, Object... args) throws DataAccessException {
|
||||
|
||||
Assert.hasText(cql, "CQL must not be empty");
|
||||
|
||||
return query(new SimpleReactivePreparedStatementCreator(cql), newArgPreparedStatementBinder(args), Mono::just)
|
||||
.next();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#queryForRows(java.lang.String, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Flux<Row> queryForRows(String cql, Object... args) throws DataAccessException {
|
||||
return queryForResultSet(cql, args).flatMap(ReactiveResultSet::rows)
|
||||
.onErrorResumeWith(translateException("QueryForRows", cql));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(org.springframework.cassandra.core.ReactivePreparedStatementCreator)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> execute(ReactivePreparedStatementCreator psc) throws DataAccessException {
|
||||
return query(psc, resultSet -> Mono.just(resultSet.wasApplied())).last();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(java.lang.String, org.springframework.cassandra.core.PreparedStatementBinder)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> execute(String cql, PreparedStatementBinder psb) throws DataAccessException {
|
||||
return query(new SimpleReactivePreparedStatementCreator(cql), psb, resultSet -> Mono.just(resultSet.wasApplied()))
|
||||
.next();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(java.lang.String, java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> execute(String cql, Object... args) throws DataAccessException {
|
||||
return execute(cql, newArgPreparedStatementBinder(args));
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlOperations#execute(java.lang.String, org.reactivestreams.Publisher)
|
||||
*/
|
||||
@Override
|
||||
public Flux<Boolean> execute(String cql, Publisher<Object[]> args) throws DataAccessException {
|
||||
|
||||
Assert.notNull(args, "Args Publisher must not be null");
|
||||
|
||||
SimpleReactivePreparedStatementCreator psc = new SimpleReactivePreparedStatementCreator(cql);
|
||||
|
||||
return execute(psc, (session, ps) -> Flux.from(args).flatMap(objects -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing Prepared CQL Statement [{}]", cql);
|
||||
}
|
||||
|
||||
BoundStatement boundStatement = newArgPreparedStatementBinder(objects).bindValues(ps);
|
||||
applyStatementSettings(boundStatement);
|
||||
return session.execute(boundStatement);
|
||||
|
||||
}).map(ReactiveResultSet::wasApplied));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Implementation hooks and helper methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a reusable {@link Flux} given a {@link ReactiveStatementCallback} without exception translation.
|
||||
*
|
||||
* @param callback must not be {@literal null}.
|
||||
* @return a reusable {@link Flux} wrapping the {@link ReactiveStatementCallback}.
|
||||
*/
|
||||
protected <T> Flux<T> createFlux(Statement statement, ReactiveStatementCallback<T> callback) {
|
||||
|
||||
Assert.notNull(callback);
|
||||
|
||||
applyStatementSettings(statement);
|
||||
|
||||
ReactiveSession session = getSession();
|
||||
|
||||
return Flux.defer(() -> callback.doInStatement(session, statement));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reusable {@link Mono} given a {@link ReactiveStatementCallback} without exception translation.
|
||||
*
|
||||
* @param callback must not be {@literal null}.
|
||||
* @return a reusable {@link Mono} wrapping the {@link ReactiveStatementCallback }.
|
||||
*/
|
||||
protected <T> Mono<T> createMono(Statement statement, ReactiveStatementCallback<T> callback) {
|
||||
|
||||
Assert.notNull(callback);
|
||||
|
||||
applyStatementSettings(statement);
|
||||
|
||||
ReactiveSession session = getSession();
|
||||
|
||||
return Mono.defer(() -> Mono.from(callback.doInStatement(session, statement)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reusable {@link Flux} given a {@link ReactiveSessionCallback} without exception translation.
|
||||
*
|
||||
* @param callback must not be {@literal null}.
|
||||
* @return a reusable {@link Flux} wrapping the {@link ReactiveSessionCallback}.
|
||||
*/
|
||||
protected <T> Flux<T> createFlux(ReactiveSessionCallback<T> callback) {
|
||||
|
||||
Assert.notNull(callback);
|
||||
|
||||
ReactiveSession session = getSession();
|
||||
|
||||
return Flux.defer(() -> callback.doInSession(session));
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception translation {@link Function} intended for {@link Mono#otherwise(Function)} usage.
|
||||
*
|
||||
* @return the exception translation {@link Function}
|
||||
*/
|
||||
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
|
||||
protected <T> Function<Throwable, Mono<? extends T>> translateException() {
|
||||
|
||||
return throwable -> Mono.error(
|
||||
throwable instanceof DriverException ? translateExceptionIfPossible((DriverException) throwable) : throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception translation {@link Function} intended for {@link Mono#otherwise(Function)} usage.
|
||||
*
|
||||
* @param task readable text describing the task being attempted
|
||||
* @param cql CQL query or update that caused the problem (may be {@code null})
|
||||
* @return the exception translation {@link Function}
|
||||
* @see CqlProvider
|
||||
*/
|
||||
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
|
||||
protected <T> Function<Throwable, Mono<? extends T>> translateException(String task, String cql) {
|
||||
|
||||
return throwable -> Mono.error(throwable instanceof DriverException
|
||||
? ReactiveCqlTemplate.this.translate(task, cql, (DriverException) throwable) : throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new RowMapper for reading columns as key-value pairs.
|
||||
*
|
||||
* @return the RowMapper to use
|
||||
* @see ColumnMapRowMapper
|
||||
*/
|
||||
protected RowMapper<Map<String, Object>> getColumnMapRowMapper() {
|
||||
return new ColumnMapRowMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new RowMapper for reading result objects from a single column.
|
||||
*
|
||||
* @param requiredType the type that each result object is expected to match
|
||||
* @return the RowMapper to use
|
||||
* @see SingleColumnRowMapper
|
||||
*/
|
||||
protected <T> RowMapper<T> getSingleColumnRowMapper(Class<T> requiredType) {
|
||||
return SingleColumnRowMapper.newInstance(requiredType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given CQL Statement (or {@link com.datastax.driver.core.PreparedStatement}), applying statement
|
||||
* settings such as fetch size, retry policy, and consistency level.
|
||||
*
|
||||
* @param stmt the CQL Statement to prepare
|
||||
* @see #setFetchSize(int)
|
||||
* @see #setRetryPolicy(RetryPolicy)
|
||||
* @see #setConsistencyLevel(ConsistencyLevel)
|
||||
*/
|
||||
protected void applyStatementSettings(Statement stmt) {
|
||||
|
||||
int fetchSize = getFetchSize();
|
||||
if (fetchSize != -1 && stmt.getFetchSize() == DEFAULTS.getFetchSize()) {
|
||||
stmt.setFetchSize(fetchSize);
|
||||
}
|
||||
|
||||
RetryPolicy retryPolicy = getRetryPolicy();
|
||||
if (retryPolicy != null && stmt.getRetryPolicy() == DEFAULTS.getRetryPolicy()) {
|
||||
stmt.setRetryPolicy(retryPolicy);
|
||||
}
|
||||
|
||||
ConsistencyLevel consistencyLevel = getConsistencyLevel();
|
||||
if (consistencyLevel != null && stmt.getConsistencyLevel() == DEFAULTS.getConsistencyLevel()) {
|
||||
stmt.setConsistencyLevel(consistencyLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given CQL Statement (or {@link com.datastax.driver.core.PreparedStatement}), applying statement
|
||||
* settings such as retry policy and consistency level.
|
||||
*
|
||||
* @param stmt the CQL Statement to prepare
|
||||
* @see #setRetryPolicy(RetryPolicy)
|
||||
* @see #setConsistencyLevel(ConsistencyLevel)
|
||||
*/
|
||||
protected void applyStatementSettings(PreparedStatement stmt) {
|
||||
|
||||
RetryPolicy retryPolicy = getRetryPolicy();
|
||||
if (retryPolicy != null) {
|
||||
stmt.setRetryPolicy(retryPolicy);
|
||||
}
|
||||
|
||||
ConsistencyLevel consistencyLevel = getConsistencyLevel();
|
||||
if (consistencyLevel != null) {
|
||||
stmt.setConsistencyLevel(consistencyLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new arg-based PreparedStatementSetter using the args passed in.
|
||||
* <p>
|
||||
* By default, we'll create an {@link ArgumentPreparedStatementBinder}. This method allows for the creation to be
|
||||
* overridden by subclasses.
|
||||
*
|
||||
* @param args object array with arguments
|
||||
* @return the new {@link PreparedStatementBinder} to use
|
||||
*/
|
||||
protected PreparedStatementBinder newArgPreparedStatementBinder(Object[] args) {
|
||||
return new ArgumentPreparedStatementBinder(args);
|
||||
}
|
||||
|
||||
private ReactiveSession getSession() {
|
||||
return getSessionFactory().getSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine CQL from potential provider object.
|
||||
*
|
||||
* @param cqlProvider object that's potentially a {@link CqlProvider}
|
||||
* @return the CQL string, or {@code null}
|
||||
* @see CqlProvider
|
||||
*/
|
||||
private static String getCql(Object cqlProvider) {
|
||||
|
||||
if (cqlProvider instanceof CqlProvider) {
|
||||
return ((CqlProvider) cqlProvider).getCql();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class SimpleReactivePreparedStatementCreator implements ReactivePreparedStatementCreator, CqlProvider {
|
||||
|
||||
private final String cql;
|
||||
|
||||
SimpleReactivePreparedStatementCreator(String cql) {
|
||||
|
||||
Assert.notNull(cql, "CQL must not be null");
|
||||
|
||||
this.cql = cql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PreparedStatement> createPreparedStatement(ReactiveSession session) throws DriverException {
|
||||
return session.prepare(cql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCql() {
|
||||
return cql;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2016 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.reactivestreams.Publisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Generic callback interface for code that operates on a {@link PreparedStatement}. Allows to execute any number of
|
||||
* operations on a single {@link PreparedStatement}, for example a single {@link ReactiveSession#execute(Statement).
|
||||
* <p>
|
||||
* Used internally by {@link ReactiveCqlTemplate}, but also useful for application code. Note that the passed-in
|
||||
* {@link PreparedStatement} can have been created by the framework or by a custom
|
||||
* {@link ReactivePreparedStatementCreator}. However, the latter is hardly ever necessary, as most custom callback
|
||||
* actions will perform updates in which case a standard {@link PreparedStatement is fine. Custom actions will always
|
||||
* set parameter values themselves, so that {@link ReactivePreparedStatementCreator} capability is not needed either.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCqlTemplate#execute(ReactivePreparedStatementCreator, ReactivePreparedStatementCallback)
|
||||
* @see ReactiveCqlTemplate#execute(String, ReactivePreparedStatementCallback)
|
||||
*/
|
||||
public interface ReactivePreparedStatementCallback<T> {
|
||||
|
||||
/**
|
||||
* Gets called by {@link ReactiveCqlTemplate#execute(String, ReactivePreparedStatementCallback)} with an active CQL
|
||||
* session and {@link PreparedStatement}. Does not need to care about closing the session: this will all be handled by
|
||||
* Spring's {@link ReactiveCqlTemplate}.
|
||||
* <p>
|
||||
* Allows for returning a result object created within the callback, i.e. a domain object or a collection of domain
|
||||
* objects. Note that there's special support for single step actions: see
|
||||
* {@link ReactiveCqlTemplate#queryForObject(String, Class, Object...)} etc. A thrown RuntimeException is treated as
|
||||
* application exception, it gets propagated to the caller of the template.
|
||||
*
|
||||
* @param session active Cassandra session, must not be {@literal null}.
|
||||
* @param ps the {@link PreparedStatement}, must not be {@literal null}.
|
||||
* @return a result object publisher.
|
||||
* @throws DriverException if thrown by a session method, to be auto-converted to a DataAccessException.
|
||||
* @throws DataAccessException in case of custom exceptions.
|
||||
* @see ReactiveCqlTemplate#queryForObject(String, Class, Object...)
|
||||
* @see ReactiveCqlTemplate#queryForFlux(String, Object...)
|
||||
*/
|
||||
Publisher<T> doInPreparedStatement(ReactiveSession session, PreparedStatement ps)
|
||||
throws DriverException, DataAccessException;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2016 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.PreparedStatement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* One of the two central callback interfaces used by the {@link ReactiveCqlTemplate} class. This interface creates a
|
||||
* {@link PreparedStatement} given a {@link ReactiveSession}, provided by the {@link ReactiveCqlTemplate} class.
|
||||
* <p>
|
||||
* Implementations may either create new prepared statements or reuse cached instances. Implementations do not need to
|
||||
* concern themselves with {@link DriverException}s that may be thrown from operations they attempt. The
|
||||
* {@link ReactiveCqlTemplate} class will catch and handle {@link DriverException}s appropriately.
|
||||
* <p>
|
||||
* A {@link ReactivePreparedStatementCreator} should also implement the {@link CqlProvider} interface if it is able to
|
||||
* provide the CQL it uses for {@link PreparedStatement} creation. This allows for better contextual information in case
|
||||
* of exceptions.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface ReactivePreparedStatementCreator {
|
||||
|
||||
/**
|
||||
* Create a statement in this session. Allows implementations to use {@link PreparedStatement}s. The
|
||||
* {@link ReactiveCqlTemplate} will attempt to cache the {@link PreparedStatement}s for future use without the
|
||||
* overhead of re-preparing on the entire cluster.
|
||||
*
|
||||
* @param session Session to use to create statement, must not be {@literal null}.
|
||||
* @return a prepared statement
|
||||
* @throws DriverException there is no need to catch DriverException that may be thrown in the implementation of this
|
||||
* method. The {@link ReactiveCqlTemplate} class will handle them.
|
||||
*/
|
||||
Mono<PreparedStatement> createPreparedStatement(ReactiveSession session) throws DriverException;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2016 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.List;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
import com.datastax.driver.core.ExecutionInfo;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.Statement;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* The reactive result of a query.
|
||||
* <p>
|
||||
* The retrieval of the rows of a {@link ReactiveResultSet} is generally paged (a first page of result is fetched and
|
||||
* the next one is only fetched once all the results of the first one has been consumed). The size of the pages can be
|
||||
* configured either globally through {@link QueryOptions#setFetchSize} or per-statement with
|
||||
* {@link Statement#setFetchSize}.
|
||||
* <p>
|
||||
* Please note however that this {@link ReactiveResultSet} paging is not available with the version 1 of the native
|
||||
* protocol (i.e. with Cassandra 1.2 or if version 1 has been explicitly requested through
|
||||
* {@link com.datastax.driver.core.Cluster.Builder#withProtocolVersion}). If the protocol version 1 is in use, a
|
||||
* {@link ReactiveResultSet} is always fetched in it's entirely and it's up to the client to make sure that no query can
|
||||
* yield {@link ReactiveResultSet} that won't hold in memory.
|
||||
* <p>
|
||||
* Note that this class is not thread-safe.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see Flux
|
||||
* @see ReactiveSession
|
||||
* @see com.datastax.driver.core.ResultSet
|
||||
*/
|
||||
public interface ReactiveResultSet {
|
||||
|
||||
/**
|
||||
* Returns a {@link Flux} over the rows contained in this result set.
|
||||
* <p>
|
||||
* The {@link Flux} will stream over all records that in this {@link ReactiveResultSet} according to the reactive
|
||||
* demand.
|
||||
* <p>
|
||||
*
|
||||
* @return a {@link Flux} of rows that will stream over all {@link Row rows} in this {@link ReactiveResultSet}.
|
||||
*/
|
||||
Flux<Row> rows();
|
||||
|
||||
/**
|
||||
* Returns the columns returned in this ResultSet.
|
||||
*
|
||||
* @return the columns returned in this ResultSet.
|
||||
*/
|
||||
public ColumnDefinitions getColumnDefinitions();
|
||||
|
||||
/**
|
||||
* If the query that produced this ResultSet was a conditional update, return whether it was successfully applied.
|
||||
* <p>
|
||||
* For consistency, this method always returns {@code true} for non-conditional queries (although there is no reason
|
||||
* to call the method in that case). This is also the case for conditional DDL statements
|
||||
* ({@code CREATE KEYSPACE... IF NOT EXISTS}, {@code CREATE TABLE... IF NOT EXISTS}), for which Cassandra doesn't
|
||||
* return an {@code [applied]} column.
|
||||
* <p>
|
||||
* Note that, for versions of Cassandra strictly lower than 2.0.9 and 2.1.0-rc2, a server-side bug (CASSANDRA-7337)
|
||||
* causes this method to always return {@code true} for batches containing conditional queries.
|
||||
*
|
||||
* @return if the query was a conditional update, whether it was applied. {@code true} for other types of queries.
|
||||
* @see <a href="https://issues.apache.org/jira/browse/CASSANDRA-7337">CASSANDRA-7337</a>
|
||||
*/
|
||||
public boolean wasApplied();
|
||||
|
||||
/**
|
||||
* Returns information on the execution of the last query made for this result set.
|
||||
* <p>
|
||||
* Note that in most cases, a result set is fetched with only one query, but large result sets can be paged and thus
|
||||
* be retrieved by multiple queries. In that case this method return the {@link ExecutionInfo} for the last query
|
||||
* performed. To retrieve the information for all queries, use {@link #getAllExecutionInfo}.
|
||||
* <p>
|
||||
* The returned object includes basic information such as the queried hosts, but also the Cassandra query trace if
|
||||
* tracing was enabled for the query.
|
||||
*
|
||||
* @return the execution info for the last query made for this result set.
|
||||
*/
|
||||
ExecutionInfo getExecutionInfo();
|
||||
|
||||
/**
|
||||
* Return the execution information for all queries made to retrieve this result set.
|
||||
* <p>
|
||||
* Unless the result set is large enough to get paged underneath, the returned list will be singleton. If paging has
|
||||
* been used however, the returned list contains the {@link ExecutionInfo} objects for all the queries done to obtain
|
||||
* this result set (at the time of the call) in the order those queries were made.
|
||||
*
|
||||
* @return a list of the execution info for all the queries made for this result set.
|
||||
*/
|
||||
List<ExecutionInfo> getAllExecutionInfo();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2016 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.reactivestreams.Publisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Callback interface used by {@link ReactiveCqlTemplate}'s query methods. Implementations of this interface perform the
|
||||
* actual work of extracting results from a {@link ReactiveResultSet}, but don't need to worry about exception handling.
|
||||
* {@link DriverException}s will be caught and handled by the calling {@link ReactiveCqlTemplate}.
|
||||
* <p>
|
||||
* This interface is mainly used within the CQL framework itself. A {@link RowMapper} is usually a simpler choice for
|
||||
* {@link ReactiveResultSet} processing, mapping one result object per row instead of one result object for the entire
|
||||
* {@link ReactiveResultSet}.
|
||||
* <p>
|
||||
* Note: {@link ReactiveResultSetExtractor} object is typically stateless and thus reusable, as long as it doesn't
|
||||
* access stateful resources or keep result state within the object.
|
||||
*
|
||||
* @param <T>
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCqlTemplate
|
||||
* @see RowCallbackHandler
|
||||
* @see RowMapper
|
||||
*/
|
||||
public interface ReactiveResultSetExtractor<T> {
|
||||
|
||||
/**
|
||||
* Implementations must implement this method to process the entire {@link ReactiveResultSet}.
|
||||
*
|
||||
* @param resultSet {@link ReactiveResultSet} to extract data from, must not be {@literal null}.
|
||||
* @return an arbitrary result object {@link Publisher}.
|
||||
* @throws DriverException if a {@link DriverException} is encountered getting column values or navigating (that is,
|
||||
* there's no need to catch {@link DriverException}).
|
||||
* @throws DataAccessException in case of custom exceptions.
|
||||
*/
|
||||
Publisher<T> extractData(ReactiveResultSet resultSet) throws DriverException, DataAccessException;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2016 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.reactivestreams.Publisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Adapter implementation of the {@link ReactiveResultSetExtractor} interface that delegates to a {@link RowMapper}
|
||||
* which is supposed to create an object for each row. Each object is emitted through the {@link Publisher} of this
|
||||
* {@link ReactiveResultSetExtractor}.
|
||||
* <p>
|
||||
* Useful for the typical case of one object per row in the database table. The number of entries in the results will
|
||||
* match the number of rows.
|
||||
* <p>
|
||||
* Note that a {@link RowMapper} object is typically stateless and thus reusable.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see RowMapper
|
||||
* @see ReactiveCqlTemplate
|
||||
*/
|
||||
public class ReactiveRowMapperResultSetExtractor<T> implements ReactiveResultSetExtractor<T> {
|
||||
|
||||
private final RowMapper<T> rowMapper;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveRowMapperResultSetExtractor}.
|
||||
*
|
||||
* @param rowMapper the {@link RowMapper} which creates an object for each row, must not be {@literal null}.
|
||||
*/
|
||||
public ReactiveRowMapperResultSetExtractor(RowMapper<T> rowMapper) {
|
||||
|
||||
Assert.notNull(rowMapper, "RowMapper is must not be null");
|
||||
|
||||
this.rowMapper = rowMapper;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.cassandra.core.ReactiveResultSetExtractor#extractData(org.springframework.cassandra.core.ReactiveResultSet)
|
||||
*/
|
||||
@Override
|
||||
public Publisher<T> extractData(ReactiveResultSet resultSet) throws DriverException, DataAccessException {
|
||||
|
||||
return resultSet.rows().flatMap(row -> {
|
||||
|
||||
T value = this.rowMapper.mapRow(row, 0);
|
||||
|
||||
if (value == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2016 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.io.Closeable;
|
||||
import java.util.Map;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.RegularStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.exceptions.NoHostAvailableException;
|
||||
import com.datastax.driver.core.exceptions.QueryExecutionException;
|
||||
import com.datastax.driver.core.exceptions.QueryValidationException;
|
||||
import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* A session holds connections to a Cassandra cluster, allowing it to be queried. {@link ReactiveSession} executes
|
||||
* queries and prepares statements in a reactive style returning results wrapped in {@link Mono} and
|
||||
* {@link reactor.core.publisher.Flux}.
|
||||
* <p/>
|
||||
* Each session maintains multiple connections to the cluster nodes, provides policies to choose which node to use for
|
||||
* each query (round-robin on all nodes of the cluster by default), and handles retries for failed queries (when it
|
||||
* makes sense).
|
||||
* <p/>
|
||||
* Session instances are thread-safe and usually a single instance is enough per application. As a given session can
|
||||
* only be "logged" into one keyspace at a time (where the "logged" keyspace is the one used by queries that don't
|
||||
* explicitly use a fully qualified table name), it can make sense to create one session per keyspace used. This is
|
||||
* however not necessary when querying multiple keyspaces since it is always possible to use a single session with fully
|
||||
* qualified table names in queries.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see org.reactivestreams.Publisher
|
||||
* @see Mono
|
||||
* @see ReactiveResultSet
|
||||
*/
|
||||
public interface ReactiveSession extends Closeable {
|
||||
|
||||
/**
|
||||
* Executes the provided query.
|
||||
* <p/>
|
||||
* This is a convenience method for {@code execute(new SimpleStatement(query))}.
|
||||
*
|
||||
* @param query the CQL query to execute.
|
||||
* @return the result of the query. That result will never be null but can be empty (and will be for any non SELECT
|
||||
* query).
|
||||
* @throws NoHostAvailableException if no host in the cluster can be contacted successfully to execute this query.
|
||||
* @throws QueryExecutionException if the query triggered an execution exception, i.e. an exception thrown by
|
||||
* Cassandra when it cannot execute the query with the requested consistency level successfully.
|
||||
* @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any other validation
|
||||
* problem).
|
||||
*/
|
||||
Mono<ReactiveResultSet> execute(String query);
|
||||
|
||||
/**
|
||||
* Executes the provided query using the provided values.
|
||||
* <p/>
|
||||
* This is a convenience method for {@code execute(new SimpleStatement(query, values))}.
|
||||
*
|
||||
* @param query the CQL query to execute.
|
||||
* @param values values required for the execution of {@code query}. See
|
||||
* {@link SimpleStatement#SimpleStatement(String, Object...)} for more details.
|
||||
* @return the result of the query. That result will never be null but can be empty (and will be for any non SELECT
|
||||
* query).
|
||||
* @throws NoHostAvailableException if no host in the cluster can be contacted successfully to execute this query.
|
||||
* @throws QueryExecutionException if the query triggered an execution exception, i.e. an exception thrown by
|
||||
* Cassandra when it cannot execute the query with the requested consistency level successfully.
|
||||
* @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any other validation
|
||||
* problem).
|
||||
* @throws UnsupportedFeatureException if version 1 of the protocol is in use (i.e. if you've forced version 1 through
|
||||
* {@link Cluster.Builder#withProtocolVersion} or you use Cassandra 1.2).
|
||||
*/
|
||||
Mono<ReactiveResultSet> execute(String query, Object... values);
|
||||
|
||||
/**
|
||||
* Executes the provided query using the provided named values.
|
||||
* <p/>
|
||||
* This is a convenience method for {@code execute(new SimpleStatement(query, values))}.
|
||||
*
|
||||
* @param query the CQL query to execute.
|
||||
* @param values values required for the execution of {@code query}. See
|
||||
* {@link SimpleStatement#SimpleStatement(String, Map)} for more details.
|
||||
* @return the result of the query. That result will never be null but can be empty (and will be for any non SELECT
|
||||
* query).
|
||||
* @throws NoHostAvailableException if no host in the cluster can be contacted successfully to execute this query.
|
||||
* @throws QueryExecutionException if the query triggered an execution exception, i.e. an exception thrown by
|
||||
* Cassandra when it cannot execute the query with the requested consistency level successfully.
|
||||
* @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any other validation
|
||||
* problem).
|
||||
* @throws UnsupportedFeatureException if version 1 or 2 of the protocol is in use (i.e. if you've forced it through
|
||||
* {@link Cluster.Builder#withProtocolVersion} or you use Cassandra 1.2 or 2.0).
|
||||
*/
|
||||
Mono<ReactiveResultSet> execute(String query, Map<String, Object> values);
|
||||
|
||||
/**
|
||||
* Executes the provided query.
|
||||
* <p/>
|
||||
* This method blocks until at least some result has been received from the database. However, for SELECT queries, it
|
||||
* does not guarantee that the result has been received in full. But it does guarantee that some response has been
|
||||
* received from the database, and in particular guarantees that if the request is invalid, an exception will be
|
||||
* thrown by this method.
|
||||
*
|
||||
* @param statement the CQL query to execute (that can be any {@link Statement}).
|
||||
* @return the result of the query. That result will never be null but can be empty (and will be for any non SELECT
|
||||
* query).
|
||||
* @throws NoHostAvailableException if no host in the cluster can be contacted successfully to execute this query.
|
||||
* @throws QueryExecutionException if the query triggered an execution exception, i.e. an exception thrown by
|
||||
* Cassandra when it cannot execute the query with the requested consistency level successfully.
|
||||
* @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any other validation
|
||||
* problem).
|
||||
* @throws UnsupportedFeatureException if the protocol version 1 is in use and a feature not supported has been used.
|
||||
* Features that are not supported by the version protocol 1 include: BatchStatement, ReactiveResultSet
|
||||
* paging and binary values in RegularStatement.
|
||||
*/
|
||||
Mono<ReactiveResultSet> execute(Statement statement);
|
||||
|
||||
/**
|
||||
* Prepares the provided query string.
|
||||
*
|
||||
* @param query the CQL query string to prepare
|
||||
* @return the prepared statement corresponding to {@code query}.
|
||||
* @throws NoHostAvailableException if no host in the cluster can be contacted successfully to prepare this query.
|
||||
*/
|
||||
Mono<PreparedStatement> prepare(String query);
|
||||
|
||||
/**
|
||||
* Prepares the provided query.
|
||||
* <p/>
|
||||
* This method behaves like {@link #prepare(String)}, but note that the resulting {@code PreparedStatement} will
|
||||
* inherit the query properties set on {@code statement}. Concretely, this means that in the following code:
|
||||
*
|
||||
* <pre>
|
||||
* RegularStatement toPrepare = new SimpleStatement("SELECT * FROM test WHERE k=?")
|
||||
* .setConsistencyLevel(ConsistencyLevel.QUORUM);
|
||||
* PreparedStatement prepared = session.prepare(toPrepare);
|
||||
* session.execute(prepared.bind("someValue"));
|
||||
* </pre>
|
||||
*
|
||||
* the final execution will be performed with Quorum consistency.
|
||||
* <p/>
|
||||
* Please note that if the same CQL statement is prepared more than once, all calls to this method will return the
|
||||
* same {@code PreparedStatement} object but the method will still apply the properties of the prepared
|
||||
* {@code Statement} to this object.
|
||||
*
|
||||
* @param statement the statement to prepare
|
||||
* @return the prepared statement corresponding to {@code statement}.
|
||||
* @throws NoHostAvailableException if no host in the cluster can be contacted successfully to prepare this statement.
|
||||
* @throws IllegalArgumentException if {@code statement.getValues() != null} (values for executing a prepared
|
||||
* statement should be provided after preparation though the {@link PreparedStatement#bind} method or
|
||||
* through a corresponding {@link BoundStatement}).
|
||||
*/
|
||||
Mono<PreparedStatement> prepare(RegularStatement statement);
|
||||
|
||||
/**
|
||||
* Initiates a shutdown of this session instance and blocks until that shutdown completes.
|
||||
* <p/>
|
||||
* This method is a shortcut for {@code closeAsync().get()}.
|
||||
* <p/>
|
||||
* Note that this method does not close the corresponding {@code Cluster} instance (which holds additional resources,
|
||||
* in particular internal executors that must be shut down in order for the client program to terminate). If you want
|
||||
* to do so, use {@link Cluster#close}, but note that it will close all sessions created from that cluster.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Whether this Session instance has been closed.
|
||||
* <p/>
|
||||
* Note that this method returns true as soon as the closing of this Session has started but it does not guarantee
|
||||
* that the closing is done. If you want to guarantee that the closing is done, you can call {@code close()} and wait
|
||||
* until it returns (or call the get method on {@code closeAsync()} with a very short timeout and check this doesn't
|
||||
* timeout).
|
||||
*
|
||||
* @return {@code true} if this Session instance has been closed, {@code false} otherwise.
|
||||
*/
|
||||
boolean isClosed();
|
||||
|
||||
/**
|
||||
* Returns the {@code Cluster} object this session is part of.
|
||||
*
|
||||
* @return the {@code Cluster} object this session is part of.
|
||||
*/
|
||||
Cluster getCluster();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2016 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.reactivestreams.Publisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Generic callback interface for code that operates on a CQL {@link ReactiveSession}. Allows to execute any number of
|
||||
* operations on a single {@link ReactiveSession}, using any type and number of Statements.
|
||||
* <p>
|
||||
* This is particularly useful for delegating to existing data access code that expects a {@link ReactiveSession} to
|
||||
* work on and throws {@link DriverException}. For newly written code, it is strongly recommended to use
|
||||
* {@link CqlTemplate}'s more specific operations, for example a query or update variant.
|
||||
*
|
||||
* @param <T>
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCqlTemplate#execute(ReactiveSessionCallback)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveSessionCallback<T> {
|
||||
|
||||
/**
|
||||
* Gets called by {@link ReactiveCqlTemplate#execute(ReactiveSessionCallback)} with an active Cassandra session. Does not
|
||||
* need to care about activating or closing the {@link ReactiveSession}.
|
||||
* <p>
|
||||
* Allows for returning a result object created within the callback, i.e. a domain object or a collection of domain
|
||||
* objects. Note that there's special support for single step actions: see
|
||||
* {@link ReactiveCqlTemplate#queryForObject(Statement, Class)} etc. A thrown {@link RuntimeException} is treated as
|
||||
* application exception: it gets propagated to the caller of the template.
|
||||
*
|
||||
* @param session active Cassandra session.
|
||||
* @return a result object publisher
|
||||
* @throws DriverException if thrown by a session method, to be auto-converted to a DataAccessException
|
||||
* @throws DataAccessException in case of custom exceptions
|
||||
*/
|
||||
Publisher<T> doInSession(ReactiveSession session) throws DriverException, DataAccessException;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2016 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;
|
||||
|
||||
/**
|
||||
* Strategy interface to produce {@link ReactiveSession} instances.
|
||||
* <p>
|
||||
* Spring provides a {@link DefaultReactiveSessionFactory} implementation that just returns the same
|
||||
* {@link ReactiveSession} instance. Implementations are free to return the same session or route calls to different
|
||||
* sessions.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see 2.0
|
||||
* @see ReactiveSession
|
||||
* @see DefaultReactiveSessionFactory
|
||||
* @see ReactiveCqlTemplate
|
||||
*/
|
||||
public interface ReactiveSessionFactory {
|
||||
|
||||
/**
|
||||
* Return a {@link ReactiveSession} to be used directly or inside a callback inside {@link ReactiveCqlTemplate}.
|
||||
*
|
||||
* @return a {@link ReactiveSession}.
|
||||
*/
|
||||
ReactiveSession getSession();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2016 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.reactivestreams.Publisher;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Generic callback interface for code that operates on a CQL {@link Statement}. Allows to execute any number of
|
||||
* operations on a single {@link Statement}, for example a single {@link ReactiveSession#execute(Statement)}.
|
||||
* <p>
|
||||
* Used internally by {@link ReactiveCqlTemplate}, but also useful for application code.
|
||||
*
|
||||
* @param <T>
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveStatementCallback<T> {
|
||||
|
||||
/**
|
||||
* Gets called by {@link ReactiveCqlTemplate#execute(String)} with an active Cassandra session. Does not need to care about
|
||||
* closing the the session: this will all be handled by Spring's {@link ReactiveCqlTemplate}.
|
||||
* <p>
|
||||
* Allows for returning a result object created within the callback, i.e. a domain object or a collection of domain
|
||||
* objects. Note that there's special support for single step actions: see
|
||||
* {@link ReactiveCqlTemplate#queryForObject(String, Class, Object...)} etc. A thrown RuntimeException is treated as
|
||||
* application exception, it gets propagated to the caller of the template.
|
||||
*
|
||||
* @param session active Cassandra session.
|
||||
* @param stmt CQL Statement
|
||||
* @return a result object publisher
|
||||
* @throws DriverException if thrown by a session method, to be auto-converted to a DataAccessException
|
||||
* @throws DataAccessException in case of custom exceptions
|
||||
* @see ReactiveCqlTemplate#queryForObject(String, Class)
|
||||
* @see ReactiveCqlTemplate#queryForResultSet(String)
|
||||
*/
|
||||
Publisher<T> doInStatement(ReactiveSession session, Statement stmt) throws DriverException;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2016 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.springframework.cassandra.support.RowUtils;
|
||||
import org.springframework.cassandra.support.exception.IncorrectResultSetColumnCountException;
|
||||
import org.springframework.dao.TypeMismatchDataAccessException;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.NumberUtils;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* {@link RowMapper} implementation that converts a single column into a single result value per row. Expects to operate
|
||||
* on a {@link com.datastax.driver.core.Row} that just contains a single column.
|
||||
* <p>
|
||||
* The type of the result value for each row can be specified. The value for the single column will be extracted from a
|
||||
* {@link Row} and converted into the specified target type.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCqlTemplate#queryForFlux(String, Class)
|
||||
* @see ReactiveCqlTemplate#queryForObject(String, Class)
|
||||
*/
|
||||
public class SingleColumnRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
private Class<?> requiredType;
|
||||
|
||||
/**
|
||||
* Create a new {@link SingleColumnRowMapper} for bean-style configuration.
|
||||
*
|
||||
* @see #setRequiredType
|
||||
*/
|
||||
public SingleColumnRowMapper() {}
|
||||
|
||||
/**
|
||||
* Create a new {@code SingleColumnRowMapper}.
|
||||
* <p>
|
||||
* Consider using the {@link #newInstance} factory method instead, which allows for specifying the required type once
|
||||
* only.
|
||||
*
|
||||
* @param requiredType the type that each result object is expected to match
|
||||
*/
|
||||
public SingleColumnRowMapper(Class<T> requiredType) {
|
||||
setRequiredType(requiredType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type that each result object is expected to match.
|
||||
* <p>
|
||||
* If not specified, the column value will be exposed as returned by the {@link Row}.
|
||||
*/
|
||||
public void setRequiredType(Class<T> requiredType) {
|
||||
this.requiredType = ClassUtils.resolvePrimitiveIfNecessary(requiredType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a value for the single column in the current row.
|
||||
* <p>
|
||||
* Validates that there is only one column selected, then delegates to {@code getColumnValue()} and also
|
||||
* {@code convertValueToRequiredType}, if necessary.
|
||||
*
|
||||
* @see ColumnDefinitions#size()
|
||||
* @see #getColumnValue(Row, int, Class)
|
||||
* @see #convertValueToRequiredType(Object, Class)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T mapRow(Row row, int rowNum) throws DriverException {
|
||||
|
||||
// Validate column count.
|
||||
ColumnDefinitions definitions = row.getColumnDefinitions();
|
||||
int nrOfColumns = definitions.size();
|
||||
if (nrOfColumns != 1) {
|
||||
throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
|
||||
}
|
||||
|
||||
// Extract column value from CQL ResultSet.
|
||||
Object result = getColumnValue(row, 0, this.requiredType);
|
||||
if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
|
||||
// Extracted value does not match already: try to convert it.
|
||||
try {
|
||||
return (T) convertValueToRequiredType(result, this.requiredType);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new TypeMismatchDataAccessException(
|
||||
String.format("Type mismatch affecting row number %d and column type '%s': %s", rowNum,
|
||||
definitions.getType(0), ex.getMessage()));
|
||||
}
|
||||
}
|
||||
return (T) result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a CQL object value for the specified column.
|
||||
* <p>
|
||||
* The default implementation calls {@link RowUtils#getRowValue(Row, int, Class)}. If no required type has been
|
||||
* specified, this method delegates to {@code getColumnValue(rs, index)}, which basically calls
|
||||
* {@link Row#getObject(int)} but applies some additional default conversion to appropriate value types.
|
||||
*
|
||||
* @param row is the {@link Row} holding the data, must not be {@literal null}.
|
||||
* @param index is the column index
|
||||
* @param requiredType the type that each result object is expected to match (or {@code null} if none specified).
|
||||
* @return the Object value.
|
||||
* @throws DriverException in case of extraction failure
|
||||
* @see RowUtils#getRowValue(Row, int, Class)
|
||||
* @see #getColumnValue(Row, int)
|
||||
*/
|
||||
protected Object getColumnValue(Row row, int index, Class<?> requiredType) throws DriverException {
|
||||
|
||||
if (requiredType != null) {
|
||||
return RowUtils.getRowValue(row, index, requiredType);
|
||||
} else {
|
||||
// No required type specified -> perform default extraction.
|
||||
return getColumnValue(row, index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a object value for the specified column, using the most appropriate value type. Called if no required type
|
||||
* has been specified.
|
||||
* <p>
|
||||
* The default implementation delegates to {@link RowUtils#getRowValue(Row, int, Class)}, which uses the
|
||||
* {@link Row#getObject(int)} method.
|
||||
*
|
||||
* @param row is the {@link Row} holding the data, must not be {@literal null}.
|
||||
* @param index is the column index
|
||||
* @return the Object value.
|
||||
* @throws DriverException in case of extraction failure.
|
||||
* @see RowUtils#getRowValue(Row, int, Class)
|
||||
*/
|
||||
protected Object getColumnValue(Row row, int index) {
|
||||
return RowUtils.getRowValue(row, index, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given column value to the specified required type. Only called if the extracted column value does not
|
||||
* match already.
|
||||
* <p>
|
||||
* If the required type is String, the value will simply get stringified via {@code toString()}. In case of a Number,
|
||||
* the value will be converted into a Number, either through number conversion or through String parsing (depending on
|
||||
* the value type).
|
||||
*
|
||||
* @param value the column value as extracted from {@code getColumnValue()} (never {@code null})
|
||||
* @param requiredType the type that each result object is expected to match (never {@code null})
|
||||
* @return the converted value
|
||||
* @see #getColumnValue(Row, int, Class)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
|
||||
|
||||
if (String.class == requiredType) {
|
||||
return value.toString();
|
||||
} else if (Number.class.isAssignableFrom(requiredType)) {
|
||||
|
||||
if (value instanceof Number) {
|
||||
// Convert original Number to target Number class.
|
||||
return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
|
||||
} else {
|
||||
// Convert stringified value to target Number class.
|
||||
return NumberUtils.parseNumber(value.toString(), (Class<Number>) requiredType);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Value [%s] is of type [%s] and cannot be converted to required type [%s]", value,
|
||||
value.getClass().getName(), requiredType.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create a new {@code SingleColumnRowMapper} (with the required type specified only once).
|
||||
*
|
||||
* @param requiredType the type that each result object is expected to match
|
||||
*/
|
||||
public static <T> SingleColumnRowMapper<T> newInstance(Class<T> requiredType) {
|
||||
return new SingleColumnRowMapper<>(requiredType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2016 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.support;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Strategy interface for translating between {@link DriverException DriverExceptios} and Spring's data access
|
||||
* strategy-agnostic {@link DataAccessException} hierarchy.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see org.springframework.dao.DataAccessException
|
||||
* @see 2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CQLExceptionTranslator extends PersistenceExceptionTranslator {
|
||||
|
||||
/**
|
||||
* Translate the given {@link DriverException} into a generic {@link DataAccessException}.
|
||||
* <p>
|
||||
* The returned {@link DataAccessException} is supposed to contain the original {@code DriverException} as root cause.
|
||||
* However, client code may not generally rely on this due to {@link DataAccessException}s possibly being caused by
|
||||
* other resource APIs as well. That said, a {@code getRootCause() instanceof DataAccessException} check (and
|
||||
* subsequent cast) is considered reliable when expecting Cassandra-based access to have happened.
|
||||
*
|
||||
* @param task readable text describing the task being attempted
|
||||
* @param cql CQL query or update that caused the problem (may be {@code null})
|
||||
* @param ex the offending {@link DriverException}
|
||||
* @return the DataAccessException, wrapping the {@code DriverException}
|
||||
* @see org.springframework.dao.DataAccessException#getRootCause()
|
||||
*/
|
||||
default DataAccessException translate(String task, String cql, DriverException ex) {
|
||||
return translateExceptionIfPossible(ex);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import com.datastax.driver.core.Session;
|
||||
*/
|
||||
public class CassandraAccessor implements InitializingBean {
|
||||
|
||||
CassandraExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator();
|
||||
protected CassandraExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator();
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.cassandra.support;
|
||||
|
||||
import org.springframework.cassandra.core.support.CQLExceptionTranslator;
|
||||
import org.springframework.cassandra.support.exception.CassandraAuthenticationException;
|
||||
import org.springframework.cassandra.support.exception.CassandraConnectionFailureException;
|
||||
import org.springframework.cassandra.support.exception.CassandraInsufficientReplicasAvailableException;
|
||||
@@ -33,6 +34,7 @@ import org.springframework.cassandra.support.exception.CassandraUncategorizedExc
|
||||
import org.springframework.cassandra.support.exception.CassandraWriteTimeoutException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.datastax.driver.core.WriteType;
|
||||
import com.datastax.driver.core.exceptions.AlreadyExistsException;
|
||||
@@ -52,80 +54,110 @@ import com.datastax.driver.core.exceptions.UnavailableException;
|
||||
import com.datastax.driver.core.exceptions.WriteTimeoutException;
|
||||
|
||||
/**
|
||||
* Simple {@link PersistenceExceptionTranslator} for Cassandra. Convert the given runtime exception to an appropriate
|
||||
* exception from the {@code org.springframework.dao} hierarchy. Return {@literal null} if no translation is
|
||||
* appropriate: any other exception may have resulted from user code, and should not be translated.
|
||||
* Simple {@link PersistenceExceptionTranslator} for Cassandra.
|
||||
* <p>
|
||||
* Convert the given runtime exception to an appropriate exception from the {@code org.springframework.dao} hierarchy.
|
||||
* Return {@literal null} if no translation is appropriate: any other exception may have resulted from user code, and
|
||||
* should not be translated.
|
||||
*
|
||||
* @author Alex Shvid
|
||||
* @author Matthew T. Adams
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
|
||||
public class CassandraExceptionTranslator implements PersistenceExceptionTranslator {
|
||||
public class CassandraExceptionTranslator implements CQLExceptionTranslator {
|
||||
|
||||
@Override
|
||||
public DataAccessException translateExceptionIfPossible(RuntimeException x) {
|
||||
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
|
||||
|
||||
if (x instanceof DataAccessException) {
|
||||
return (DataAccessException) x;
|
||||
if (ex instanceof DataAccessException) {
|
||||
return (DataAccessException) ex;
|
||||
}
|
||||
|
||||
if (!(x instanceof DriverException)) {
|
||||
if (!(ex instanceof DriverException)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return translate(null, null, (DriverException) ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataAccessException translate(String task, String cql, DriverException ex) {
|
||||
|
||||
String message = buildMessage(task, cql, ex);
|
||||
|
||||
// Remember: subclasses must come before superclasses, otherwise the
|
||||
// superclass would match before the subclass!
|
||||
|
||||
if (x instanceof AuthenticationException) {
|
||||
return new CassandraAuthenticationException(((AuthenticationException) x).getHost(), x.getMessage(), x);
|
||||
if (ex instanceof AuthenticationException) {
|
||||
return new CassandraAuthenticationException(((AuthenticationException) ex).getHost(), message, ex);
|
||||
}
|
||||
if (x instanceof DriverInternalError) {
|
||||
return new CassandraInternalException(x.getMessage(), x);
|
||||
if (ex instanceof DriverInternalError) {
|
||||
return new CassandraInternalException(message, ex);
|
||||
}
|
||||
if (x instanceof InvalidTypeException) {
|
||||
return new CassandraTypeMismatchException(x.getMessage(), x);
|
||||
if (ex instanceof InvalidTypeException) {
|
||||
return new CassandraTypeMismatchException(message, ex);
|
||||
}
|
||||
if (x instanceof NoHostAvailableException) {
|
||||
return new CassandraConnectionFailureException(((NoHostAvailableException) x).getErrors(), x.getMessage(), x);
|
||||
if (ex instanceof NoHostAvailableException) {
|
||||
return new CassandraConnectionFailureException(((NoHostAvailableException) ex).getErrors(), message, ex);
|
||||
}
|
||||
if (x instanceof ReadTimeoutException) {
|
||||
return new CassandraReadTimeoutException(((ReadTimeoutException) x).wasDataRetrieved(), x.getMessage(), x);
|
||||
if (ex instanceof ReadTimeoutException) {
|
||||
return new CassandraReadTimeoutException(((ReadTimeoutException) ex).wasDataRetrieved(), message, ex);
|
||||
}
|
||||
if (x instanceof WriteTimeoutException) {
|
||||
WriteType writeType = ((WriteTimeoutException) x).getWriteType();
|
||||
return new CassandraWriteTimeoutException(writeType == null ? null : writeType.name(), x.getMessage(), x);
|
||||
if (ex instanceof WriteTimeoutException) {
|
||||
WriteType writeType = ((WriteTimeoutException) ex).getWriteType();
|
||||
return new CassandraWriteTimeoutException(writeType == null ? null : writeType.name(), message, ex);
|
||||
}
|
||||
if (x instanceof TruncateException) {
|
||||
return new CassandraTruncateException(x.getMessage(), x);
|
||||
if (ex instanceof TruncateException) {
|
||||
return new CassandraTruncateException(message, ex);
|
||||
}
|
||||
if (x instanceof UnavailableException) {
|
||||
UnavailableException ux = (UnavailableException) x;
|
||||
if (ex instanceof UnavailableException) {
|
||||
UnavailableException ux = (UnavailableException) ex;
|
||||
return new CassandraInsufficientReplicasAvailableException(ux.getRequiredReplicas(), ux.getAliveReplicas(),
|
||||
x.getMessage(), x);
|
||||
message, ex);
|
||||
}
|
||||
if (x instanceof AlreadyExistsException) {
|
||||
AlreadyExistsException aex = (AlreadyExistsException) x;
|
||||
if (ex instanceof AlreadyExistsException) {
|
||||
AlreadyExistsException aex = (AlreadyExistsException) ex;
|
||||
|
||||
return aex.wasTableCreation() ? new CassandraTableExistsException(aex.getTable(), x.getMessage(), x)
|
||||
: new CassandraKeyspaceExistsException(aex.getKeyspace(), x.getMessage(), x);
|
||||
return aex.wasTableCreation() ? new CassandraTableExistsException(aex.getTable(), message, ex)
|
||||
: new CassandraKeyspaceExistsException(aex.getKeyspace(), message, ex);
|
||||
}
|
||||
if (x instanceof InvalidConfigurationInQueryException) {
|
||||
return new CassandraInvalidConfigurationInQueryException(x.getMessage(), x);
|
||||
if (ex instanceof InvalidConfigurationInQueryException) {
|
||||
return new CassandraInvalidConfigurationInQueryException(message, ex);
|
||||
}
|
||||
if (x instanceof InvalidQueryException) {
|
||||
return new CassandraInvalidQueryException(x.getMessage(), x);
|
||||
if (ex instanceof InvalidQueryException) {
|
||||
return new CassandraInvalidQueryException(message, ex);
|
||||
}
|
||||
if (x instanceof SyntaxError) {
|
||||
return new CassandraQuerySyntaxException(x.getMessage(), x);
|
||||
if (ex instanceof SyntaxError) {
|
||||
return new CassandraQuerySyntaxException(message, ex);
|
||||
}
|
||||
if (x instanceof UnauthorizedException) {
|
||||
return new CassandraUnauthorizedException(x.getMessage(), x);
|
||||
if (ex instanceof UnauthorizedException) {
|
||||
return new CassandraUnauthorizedException(message, ex);
|
||||
}
|
||||
if (x instanceof TraceRetrievalException) {
|
||||
return new CassandraTraceRetrievalException(x.getMessage(), x);
|
||||
if (ex instanceof TraceRetrievalException) {
|
||||
return new CassandraTraceRetrievalException(message, ex);
|
||||
}
|
||||
|
||||
// unknown or unhandled exception
|
||||
return new CassandraUncategorizedException(x.getMessage(), x);
|
||||
return new CassandraUncategorizedException(message, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a message {@code String} for the given {@link DriverException}.
|
||||
* <p>
|
||||
* To be called by translator subclasses when creating an instance of a generic
|
||||
* {@link org.springframework.dao.DataAccessException} class.
|
||||
*
|
||||
* @param task readable text describing the task being attempted
|
||||
* @param cql the CQL statement that caused the problem (may be {@code null})
|
||||
* @param ex the offending {@code DriverException}
|
||||
* @return the message {@code String} to use
|
||||
*/
|
||||
protected String buildMessage(String task, String cql, DriverException ex) {
|
||||
|
||||
if (StringUtils.hasText(task) || StringUtils.hasText(cql)) {
|
||||
return task + "; CQL [" + cql + "]; " + ex.getMessage();
|
||||
}
|
||||
|
||||
return ex.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2016 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.support;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.cassandra.core.ReactiveSessionFactory;
|
||||
import org.springframework.cassandra.core.support.CQLExceptionTranslator;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.exceptions.DriverException;
|
||||
|
||||
/**
|
||||
* Base class for {@link org.springframework.cassandra.core.ReactiveCqlTemplate} and other CQL-accessing DAO helpers,
|
||||
* defining common properties such as {@link org.springframework.cassandra.core.ReactiveSessionFactory} and exception
|
||||
* translator.
|
||||
* <p>
|
||||
* Not intended to be used directly.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see InitializingBean
|
||||
* @see org.springframework.cassandra.core.ReactiveSession
|
||||
* @see org.springframework.cassandra.core.ReactiveCqlTemplate
|
||||
*/
|
||||
public abstract class ReactiveCassandraAccessor implements InitializingBean {
|
||||
|
||||
/** Logger available to subclasses */
|
||||
protected final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private CQLExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator();
|
||||
|
||||
private ReactiveSessionFactory sessionFactory;
|
||||
|
||||
/**
|
||||
* Sets the {@link ReactiveSessionFactory} to use.
|
||||
*
|
||||
* @param sessionFactory must not be {@literal null}.
|
||||
*/
|
||||
public void setSessionFactory(ReactiveSessionFactory sessionFactory) {
|
||||
|
||||
Assert.notNull(sessionFactory, "ReactiveSessionFactory must not be null");
|
||||
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured {@link ReactiveSessionFactory}.
|
||||
*
|
||||
* @return the configured {@link ReactiveSessionFactory}.
|
||||
*/
|
||||
public ReactiveSessionFactory getSessionFactory() {
|
||||
return sessionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the exception translator used by this template to translate Cassandra specific exceptions into Spring DAO's
|
||||
* Exception Hierarchy.
|
||||
*
|
||||
* @param exceptionTranslator exception translator to set; must not be {@literal null}.
|
||||
* @see CassandraExceptionTranslator
|
||||
* @see DataAccessException
|
||||
*/
|
||||
public void setExceptionTranslator(CQLExceptionTranslator exceptionTranslator) {
|
||||
|
||||
Assert.notNull(exceptionTranslator, "CQLExceptionTranslator must not be null");
|
||||
|
||||
this.exceptionTranslator = exceptionTranslator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exception translator for this instance.
|
||||
*
|
||||
* @return the Cassandra exception translator.
|
||||
* @see CassandraExceptionTranslator
|
||||
*/
|
||||
public CQLExceptionTranslator getExceptionTranslator() {
|
||||
return this.exceptionTranslator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the Cassandra {@link ReactiveSessionFactory} and exception translator has been properly set.
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
Assert.notNull(sessionFactory != null, "ReactiveSessionFactory must not be null");
|
||||
Assert.notNull(exceptionTranslator != null, "CassandraExceptionTranslator must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given {@link DriverException} into a generic {@link DataAccessException}.
|
||||
* <p>
|
||||
* The returned {@link DataAccessException} is supposed to contain the original {@code DriverException} as root cause.
|
||||
* However, client code may not generally rely on this due to {@link DataAccessException}s possibly being caused by
|
||||
* other resource APIs as well. That said, a {@code getRootCause() instanceof DataAccessException} check (and
|
||||
* subsequent cast) is considered reliable when expecting Cassandra-based access to have happened.
|
||||
*
|
||||
* @param ex the offending {@link DriverException}
|
||||
* @return the DataAccessException, wrapping the {@code DriverException}
|
||||
* @see <a href=
|
||||
* "http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#dao-exceptions">Consistent
|
||||
* exception hierarchy</a>
|
||||
* @see DataAccessException
|
||||
*/
|
||||
protected DataAccessException translateExceptionIfPossible(DriverException ex) {
|
||||
|
||||
Assert.notNull(ex, "DriverException must not be null");
|
||||
|
||||
return getExceptionTranslator().translateExceptionIfPossible(ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given {@link DriverException} into a generic {@link DataAccessException}.
|
||||
* <p>
|
||||
* The returned {@link DataAccessException} is supposed to contain the original {@code DriverException} as root cause.
|
||||
* However, client code may not generally rely on this due to {@link DataAccessException}s possibly being caused by
|
||||
* other resource APIs as well. That said, a {@code getRootCause() instanceof DataAccessException} check (and
|
||||
* subsequent cast) is considered reliable when expecting Cassandra-based access to have happened.
|
||||
*
|
||||
* @param task readable text describing the task being attempted
|
||||
* @param cql CQL query or update that caused the problem (may be {@code null})
|
||||
* @param ex the offending {@link DriverException}
|
||||
* @return the DataAccessException, wrapping the {@code DriverException}
|
||||
* @see org.springframework.dao.DataAccessException#getRootCause()
|
||||
* @see <a href=
|
||||
* "http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#dao-exceptions">Consistent
|
||||
* exception hierarchy</a>
|
||||
*/
|
||||
protected DataAccessException translate(String task, String cql, DriverException ex) {
|
||||
|
||||
Assert.notNull(ex, "DriverException must not be null");
|
||||
|
||||
return getExceptionTranslator().translate(task, cql, ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2016 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.support;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.datastax.driver.core.LocalDate;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.TupleValue;
|
||||
import com.datastax.driver.core.UDTValue;
|
||||
|
||||
/**
|
||||
* Generic utility methods for working with Cassandra. Mainly for internal use within the framework, but also useful for
|
||||
* custom CQL access code.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public abstract class RowUtils {
|
||||
|
||||
/**
|
||||
* Retrieve a CQL column value from a {@link Row}, using the specified value type.
|
||||
* <p>
|
||||
* Uses the specifically typed {@link Row} accessor methods, falling back to {@link Row#getObject(int)} for unknown
|
||||
* types.
|
||||
* <p>
|
||||
* Note that the returned value may not be assignable to the specified required type, in case of an unknown type.
|
||||
* Calling code needs to deal with this case appropriately, e.g. throwing a corresponding exception.
|
||||
*
|
||||
* @param row is the {@link Row} holding the data
|
||||
* @param index is the column index
|
||||
* @param requiredType the required value type (may be {@code null})
|
||||
* @return the value object
|
||||
*/
|
||||
public static Object getRowValue(Row row, int index, Class<?> requiredType) {
|
||||
|
||||
if (requiredType == null) {
|
||||
return row.getObject(index);
|
||||
}
|
||||
|
||||
Object value;
|
||||
|
||||
// Explicitly extract typed value, as far as possible.
|
||||
if (String.class == requiredType) {
|
||||
return row.getString(index);
|
||||
} else if (boolean.class == requiredType || Boolean.class == requiredType) {
|
||||
value = row.getBool(index);
|
||||
} else if (byte.class == requiredType || Byte.class == requiredType) {
|
||||
value = row.getByte(index);
|
||||
} else if (short.class == requiredType || Short.class == requiredType) {
|
||||
value = row.getShort(index);
|
||||
} else if (int.class == requiredType || Integer.class == requiredType) {
|
||||
value = row.getInt(index);
|
||||
} else if (long.class == requiredType || Long.class == requiredType) {
|
||||
value = row.getLong(index);
|
||||
} else if (float.class == requiredType || Float.class == requiredType) {
|
||||
value = row.getFloat(index);
|
||||
} else if (double.class == requiredType || Double.class == requiredType || Number.class == requiredType) {
|
||||
value = row.getDouble(index);
|
||||
} else if (BigDecimal.class == requiredType) {
|
||||
return row.getDecimal(index);
|
||||
} else if (LocalDate.class == requiredType) {
|
||||
return row.getDate(index);
|
||||
} else if (java.util.Date.class == requiredType) {
|
||||
return row.getTimestamp(index);
|
||||
} else if (ByteBuffer.class == requiredType) {
|
||||
return row.getBytes(index);
|
||||
} else if (TupleValue.class == requiredType) {
|
||||
return row.getTupleValue(index);
|
||||
} else if (UDTValue.class == requiredType) {
|
||||
return row.getUDTValue(index);
|
||||
} else if (UUID.class == requiredType) {
|
||||
return row.getUUID(index);
|
||||
} else {
|
||||
// Some unknown type desired -> rely on getObject.
|
||||
return row.getObject(index);
|
||||
}
|
||||
|
||||
return (row.isNull(index) ? null : value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.springframework.cassandra.support.exception;
|
||||
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
|
||||
/**
|
||||
* Data access exception thrown when a result set did not have the correct column count, for example when expecting a
|
||||
* single column but getting 0 or more than 1 columns.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see org.springframework.dao.IncorrectResultSizeDataAccessException
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class IncorrectResultSetColumnCountException extends DataRetrievalFailureException {
|
||||
|
||||
private int expectedCount;
|
||||
|
||||
private int actualCount;
|
||||
|
||||
/**
|
||||
* Constructor for IncorrectResultSetColumnCountException.
|
||||
*
|
||||
* @param expectedCount the expected column count
|
||||
* @param actualCount the actual column count
|
||||
*/
|
||||
public IncorrectResultSetColumnCountException(int expectedCount, int actualCount) {
|
||||
|
||||
super("Incorrect column count: expected " + expectedCount + ", actual " + actualCount);
|
||||
|
||||
this.expectedCount = expectedCount;
|
||||
this.actualCount = actualCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for IncorrectResultCountDataAccessException.
|
||||
*
|
||||
* @param msg the detail message
|
||||
* @param expectedCount the expected column count
|
||||
* @param actualCount the actual column count
|
||||
*/
|
||||
public IncorrectResultSetColumnCountException(String msg, int expectedCount, int actualCount) {
|
||||
|
||||
super(msg);
|
||||
|
||||
this.expectedCount = expectedCount;
|
||||
this.actualCount = actualCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the expected column count.
|
||||
*/
|
||||
public int getExpectedCount() {
|
||||
return this.expectedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual column count.
|
||||
*/
|
||||
public int getActualCount() {
|
||||
return this.actualCount;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
|
||||
|
||||
import com.datastax.driver.core.KeyspaceMetadata;
|
||||
import com.datastax.driver.core.PreparedStatement;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.exceptions.SyntaxError;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link DefaultBridgedReactiveSession}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class DefaultBridgedReactiveSessionIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest {
|
||||
|
||||
private DefaultBridgedReactiveSession reactiveSession;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
|
||||
this.session.execute("DROP TABLE IF EXISTS users;");
|
||||
|
||||
this.reactiveSession = new DefaultBridgedReactiveSession(this.session, Schedulers.elastic());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeShouldExecuteDeferred() throws Exception {
|
||||
|
||||
Mono<ReactiveResultSet> execution = reactiveSession
|
||||
.execute("CREATE TABLE users (\n" + " userid text PRIMARY KEY,\n" + " first_name text\n" + ");");
|
||||
|
||||
KeyspaceMetadata keyspace = getKeyspaceMetadata();
|
||||
|
||||
assertThat(keyspace.getTable("users")).isNull();
|
||||
|
||||
ReactiveResultSet resultSet = execution.block();
|
||||
assertThat(resultSet.wasApplied()).isTrue();
|
||||
assertThat(keyspace.getTable("users")).isNotNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeShouldTransportExceptionsInMono() throws Exception {
|
||||
|
||||
Mono<ReactiveResultSet> execution = reactiveSession.execute("INSERT INTO dummy;");
|
||||
|
||||
try {
|
||||
execution.block();
|
||||
fail("Missing SyntaxError");
|
||||
} catch (SyntaxError e) {
|
||||
assertThat(e).isInstanceOf(SyntaxError.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeShouldReturnRows() throws Exception {
|
||||
|
||||
session.execute("CREATE TABLE users (\n" + " userid text PRIMARY KEY,\n" + " first_name text\n" + ");");
|
||||
session.execute("INSERT INTO users (userid, first_name) VALUES ('White', 'Walter');");
|
||||
|
||||
Mono<ReactiveResultSet> execution = reactiveSession.execute("SELECT * FROM users;");
|
||||
ReactiveResultSet resultSet = execution.block();
|
||||
Row row = resultSet.rows().blockFirst();
|
||||
|
||||
assertThat(row).isNotNull();
|
||||
assertThat(row.getString("userid")).isEqualTo("White");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeShouldPrepareStatement() throws Exception {
|
||||
|
||||
session.execute("CREATE TABLE users (\n" + " userid text PRIMARY KEY,\n" + " first_name text\n" + ");");
|
||||
|
||||
Mono<PreparedStatement> execution = reactiveSession
|
||||
.prepare("INSERT INTO users (userid, first_name) VALUES (?, ?);");
|
||||
PreparedStatement preparedStatement = execution.block();
|
||||
|
||||
assertThat(preparedStatement).isNotNull();
|
||||
assertThat(preparedStatement.getQueryString()).isEqualTo("INSERT INTO users (userid, first_name) VALUES (?, ?);");
|
||||
}
|
||||
|
||||
private KeyspaceMetadata getKeyspaceMetadata() {
|
||||
return cluster.getMetadata().getKeyspace(this.session.getLoggedKeyspace());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.hamcrest.core.IsEqual;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Matchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultBridgedReactiveSession}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DefaultBridgedReactiveSessionUnitTests {
|
||||
|
||||
@Mock private Session sessionMock;
|
||||
|
||||
private DefaultBridgedReactiveSession reactiveSession;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
reactiveSession = new DefaultBridgedReactiveSession(sessionMock, Schedulers.immediate());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeStatementShouldForwardStatementToSession() throws Exception {
|
||||
|
||||
SimpleStatement statement = new SimpleStatement("SELECT *");
|
||||
reactiveSession.execute(statement).subscribe();
|
||||
|
||||
verify(sessionMock).executeAsync(statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeShouldForwardStatementToSession() throws Exception {
|
||||
|
||||
reactiveSession.execute("SELECT *").subscribe();
|
||||
|
||||
verify(sessionMock).executeAsync(eq(new SimpleStatement("SELECT *")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeWithValuesShouldForwardStatementToSession() throws Exception {
|
||||
|
||||
reactiveSession.execute("SELECT * WHERE a = ? and b = ?", "A", "B").subscribe();
|
||||
|
||||
verify(sessionMock).executeAsync(eq(new SimpleStatement("SELECT * WHERE a = ? and b = ?", "A", "B")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeWithValueMapShouldForwardStatementToSession() throws Exception {
|
||||
|
||||
reactiveSession.execute("SELECT * WHERE a = ?", Collections.singletonMap("a", "value")).subscribe();
|
||||
|
||||
verify(sessionMock)
|
||||
.executeAsync(eq(new SimpleStatement("SELECT * WHERE a = ?", Collections.singletonMap("a", "value"))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void testPrepareQuery() throws Exception {
|
||||
|
||||
reactiveSession.prepare("SELECT *").subscribe();
|
||||
|
||||
verify(sessionMock).prepareAsync(eq(new SimpleStatement("SELECT *")));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void testPrepareStatement() throws Exception {
|
||||
|
||||
SimpleStatement statement = new SimpleStatement("SELECT *");
|
||||
reactiveSession.prepare(statement).subscribe();
|
||||
|
||||
verify(sessionMock).prepareAsync(statement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void testClose() throws Exception {
|
||||
|
||||
reactiveSession.close();
|
||||
|
||||
verify(sessionMock).close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void testIsClosed() throws Exception {
|
||||
|
||||
when(reactiveSession.isClosed()).thenReturn(true);
|
||||
|
||||
boolean result = reactiveSession.isClosed();
|
||||
|
||||
assertThat(result).isTrue();
|
||||
verify(sessionMock).isClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void testGetCluster() throws Exception {
|
||||
|
||||
Cluster clusterMock = mock(Cluster.class);
|
||||
when(sessionMock.getCluster()).thenReturn(clusterMock);
|
||||
|
||||
Cluster result = reactiveSession.getCluster();
|
||||
|
||||
assertThat(result).isSameAs(clusterMock);
|
||||
}
|
||||
|
||||
private static <T extends Statement> T eq(T value) {
|
||||
|
||||
return Matchers.argThat(new IsEqual<T>(value) {
|
||||
|
||||
@Override
|
||||
public boolean matches(Object actualValue) {
|
||||
|
||||
if (actualValue instanceof Statement) {
|
||||
return value.toString().equals(actualValue.toString());
|
||||
}
|
||||
|
||||
return super.matches(actualValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
|
||||
|
||||
import com.datastax.driver.core.querybuilder.QueryBuilder;
|
||||
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveCqlTemplate}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class ReactiveCqlTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest {
|
||||
|
||||
private static final AtomicBoolean initialized = new AtomicBoolean();
|
||||
private ReactiveSession reactiveSession;
|
||||
private ReactiveCqlTemplate template;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
|
||||
reactiveSession = new DefaultBridgedReactiveSession(getSession(), Schedulers.elastic());
|
||||
|
||||
if (initialized.compareAndSet(false, true)) {
|
||||
getSession().execute("CREATE TABLE IF NOT EXISTS user (id text PRIMARY KEY, username text);");
|
||||
} else {
|
||||
getSession().execute("TRUNCATE user;");
|
||||
}
|
||||
|
||||
getSession().execute("INSERT INTO user (id, username) VALUES ('WHITE', 'Walter');");
|
||||
|
||||
template = new ReactiveCqlTemplate(new DefaultReactiveSessionFactory(reactiveSession));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeShouldRemoveRecords() throws Exception {
|
||||
|
||||
template.execute("DELETE FROM user WHERE id = 'WHITE'").block();
|
||||
|
||||
assertThat(getSession().execute("SELECT * FROM user").one()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void queryForObjectShouldReturnFirstColumn() throws Exception {
|
||||
|
||||
String id = template.queryForObject("SELECT id FROM user;", String.class).block();
|
||||
|
||||
assertThat(id).isEqualTo("WHITE");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void queryForObjectShouldReturnMap() throws Exception {
|
||||
|
||||
Map<String, Object> map = template.queryForMap("SELECT * FROM user;").block();
|
||||
|
||||
assertThat(map).containsEntry("id", "WHITE").containsEntry("username", "Walter");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeStatementShouldRemoveRecords() throws Exception {
|
||||
|
||||
template.execute(QueryBuilder.delete().from("user").where(QueryBuilder.eq("id", "WHITE"))).block();
|
||||
|
||||
assertThat(getSession().execute("SELECT * FROM user").one()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void queryForObjectStatementShouldReturnFirstColumn() throws Exception {
|
||||
|
||||
String id = template.queryForObject(QueryBuilder.select("id").from("user"), String.class).block();
|
||||
|
||||
assertThat(id).isEqualTo("WHITE");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void queryForObjectStatementShouldReturnMap() throws Exception {
|
||||
|
||||
Map<String, Object> map = template.queryForMap(QueryBuilder.select().from("user")).block();
|
||||
|
||||
assertThat(map).containsEntry("id", "WHITE").containsEntry("username", "Walter");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void executeWithArgsShouldRemoveRecords() throws Exception {
|
||||
|
||||
template.execute("DELETE FROM user WHERE id = ?", "WHITE").block();
|
||||
|
||||
assertThat(getSession().execute("SELECT * FROM user").one()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void queryForObjectWithArgsShouldReturnFirstColumn() throws Exception {
|
||||
|
||||
String id = template.queryForObject("SELECT id FROM user WHERE id = ?;", String.class, "WHITE").block();
|
||||
|
||||
assertThat(id).isEqualTo("WHITE");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void queryForObjectWithArgsShouldReturnMap() throws Exception {
|
||||
|
||||
Map<String, Object> map = template.queryForMap("SELECT * FROM user WHERE id = ?;", "WHITE").block();
|
||||
|
||||
assertThat(map).containsEntry("id", "WHITE").containsEntry("username", "Walter");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2016 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 static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.cassandra.support.exception.IncorrectResultSetColumnCountException;
|
||||
import org.springframework.dao.TypeMismatchDataAccessException;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
import com.datastax.driver.core.DataType;
|
||||
import com.datastax.driver.core.Row;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SingleColumnRowMapper}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @soundtrack Kos Vs Michael Buffer - Go For It All (Rubberboot Mix)
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SingleColumnRowMapperUnitTests {
|
||||
|
||||
@Mock private Row row;
|
||||
@Mock private ColumnDefinitions columnDefinitions;
|
||||
|
||||
private SingleColumnRowMapper rowMapper;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
when(row.getColumnDefinitions()).thenReturn(columnDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void getColumnValueWithType() {
|
||||
|
||||
when(row.getDouble(2)).thenReturn(42d);
|
||||
|
||||
rowMapper = new SingleColumnRowMapper();
|
||||
|
||||
assertThat(rowMapper.getColumnValue(row, 2, Number.class)).isEqualTo(42d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void getColumnValue() {
|
||||
|
||||
when(row.getObject(2)).thenReturn(42d);
|
||||
|
||||
rowMapper = new SingleColumnRowMapper();
|
||||
|
||||
assertThat(rowMapper.getColumnValue(row, 2)).isEqualTo(42d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void convertValueToRequiredTypeForNumber() {
|
||||
|
||||
rowMapper = new SingleColumnRowMapper<Number>();
|
||||
|
||||
assertThat(rowMapper.convertValueToRequiredType(1234, Integer.class)).isEqualTo(1234);
|
||||
assertThat(rowMapper.convertValueToRequiredType(1234.2, Integer.class)).isEqualTo(1234);
|
||||
assertThat(rowMapper.convertValueToRequiredType(1234.2, Double.class)).isEqualTo(1234.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void convertValueToRequiredTypeForString() {
|
||||
|
||||
rowMapper = new SingleColumnRowMapper<Number>();
|
||||
|
||||
assertThat(rowMapper.convertValueToRequiredType("1234", Integer.class)).isEqualTo(1234);
|
||||
assertThat(rowMapper.convertValueToRequiredType("1234.2", Double.class)).isEqualTo(1234.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void convertValueToRequiredTypeShouldFail() {
|
||||
|
||||
rowMapper = new SingleColumnRowMapper<>();
|
||||
|
||||
rowMapper.convertValueToRequiredType("1234", Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void mapRowSingleColumn() {
|
||||
|
||||
when(columnDefinitions.size()).thenReturn(1);
|
||||
when(row.getInt(0)).thenReturn(42);
|
||||
|
||||
rowMapper = SingleColumnRowMapper.newInstance(Integer.class);
|
||||
|
||||
assertThat(rowMapper.mapRow(row, 2)).isEqualTo(42);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void mapRowSingleColumnNullValue() {
|
||||
|
||||
when(columnDefinitions.size()).thenReturn(1);
|
||||
when(row.getObject(0)).thenReturn(null);
|
||||
|
||||
rowMapper = SingleColumnRowMapper.newInstance(Object.class);
|
||||
|
||||
assertThat(rowMapper.mapRow(row, 2)).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test(expected = TypeMismatchDataAccessException.class)
|
||||
public void mapRowSingleColumnWrongType() {
|
||||
|
||||
when(columnDefinitions.size()).thenReturn(1);
|
||||
when(columnDefinitions.getType(0)).thenReturn(DataType.blob());
|
||||
when(row.getObject(0)).thenReturn("hello");
|
||||
|
||||
rowMapper = SingleColumnRowMapper.newInstance(ColumnDefinitions.class);
|
||||
rowMapper.mapRow(row, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test(expected = IncorrectResultSetColumnCountException.class)
|
||||
public void tooManyColumns() {
|
||||
|
||||
when(columnDefinitions.size()).thenReturn(2);
|
||||
|
||||
rowMapper = SingleColumnRowMapper.newInstance(ColumnDefinitions.class);
|
||||
rowMapper.mapRow(row, 1);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
package org.springframework.cassandra.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.cassandra.support.exception.CassandraInvalidConfigurationInQueryException;
|
||||
@@ -33,8 +34,9 @@ import com.datastax.driver.core.exceptions.InvalidQueryException;
|
||||
* Unit tests for {@link CassandraExceptionTranslator}
|
||||
*
|
||||
* @author Matthew T. Adams
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class CassandraExceptionTranslatorTest {
|
||||
public class CassandraExceptionTranslatorUnitTests {
|
||||
|
||||
CassandraExceptionTranslator tx = new CassandraExceptionTranslator();
|
||||
|
||||
@@ -85,4 +87,17 @@ public class CassandraExceptionTranslatorTest {
|
||||
assertThat(dax instanceof CassandraInvalidQueryException).isTrue();
|
||||
assertThat(dax.getCause()).isEqualTo(cx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldTranslateWithCqlMessage() {
|
||||
|
||||
InvalidQueryException cx = new InvalidConfigurationInQueryException(null, "err");
|
||||
DataAccessException dax = tx.translate("Query", "SELECT * FROM person", cx);
|
||||
|
||||
assertThat(dax).hasRootCauseInstanceOf(InvalidQueryException.class).hasMessage(
|
||||
"Query; CQL [SELECT * FROM person]; err; nested exception is com.datastax.driver.core.exceptions.InvalidConfigurationInQueryException: err");
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,13 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<version>${rxjava}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- CDI -->
|
||||
<dependency>
|
||||
<groupId>javax.enterprise</groupId>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.config.java;
|
||||
|
||||
import org.springframework.cassandra.core.DefaultBridgedReactiveSession;
|
||||
import org.springframework.cassandra.core.DefaultReactiveSessionFactory;
|
||||
import org.springframework.cassandra.core.ReactiveCqlTemplate;
|
||||
import org.springframework.cassandra.core.ReactiveSession;
|
||||
import org.springframework.cassandra.core.ReactiveSessionFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.cassandra.core.CassandraAdminTemplate;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
|
||||
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Extension to {@link AbstractCassandraConfiguration} providing Spring Data Cassandra configuration for Spring Data's
|
||||
* Reactive Cassandra support using JavaConfig.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public abstract class AbstractReactiveCassandraConfiguration extends AbstractCassandraConfiguration {
|
||||
|
||||
/**
|
||||
* Creates a {@link ReactiveSession} object. This wraps a {@link com.datastax.driver.core.Session} to expose Cassandra
|
||||
* access in a reactive style.
|
||||
*
|
||||
* @return
|
||||
* @see #session()
|
||||
* @see DefaultBridgedReactiveSession
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveSession reactiveSession() throws Exception {
|
||||
return new DefaultBridgedReactiveSession(session().getObject(), Schedulers.elastic());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ReactiveSessionFactory} to be used by the {@link ReactiveCassandraTemplate}. Will use the
|
||||
* {@link ReactiveSession} instance configured in {@link #reactiveSession()}.
|
||||
*
|
||||
* @return
|
||||
* @see #reactiveSession()
|
||||
* @see #reactiveCassandraTemplate()
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveSessionFactory reactiveSessionFactory() throws Exception {
|
||||
return new DefaultReactiveSessionFactory(reactiveSession());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CassandraAdminTemplate}.
|
||||
*
|
||||
* @return
|
||||
* @see #reactiveSessionFactory()
|
||||
* @see #cassandraConverter()
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveCassandraTemplate reactiveCassandraTemplate() throws Exception {
|
||||
return new ReactiveCassandraTemplate(reactiveSessionFactory(), cassandraConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ReactiveCqlTemplate} using the configured {@link ReactiveSessionFactory}.
|
||||
*
|
||||
* @return
|
||||
* @see #reactiveSessionFactory()
|
||||
*/
|
||||
@Bean
|
||||
public ReactiveCqlTemplate reactiveCqlTemplate() throws Exception {
|
||||
return new ReactiveCqlTemplate(reactiveSessionFactory());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.cassandra.core.QueryOptions;
|
||||
import org.springframework.cassandra.core.ReactiveCqlOperations;
|
||||
import org.springframework.cassandra.core.WriteOptions;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.cassandra.convert.CassandraConverter;
|
||||
|
||||
import com.datastax.driver.core.Statement;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Interface specifying a basic set of reactive Cassandra operations. Implemented by {@link ReactiveCassandraTemplate}.
|
||||
* Not often used directly, but a useful option to enhance testability, as it can easily be mocked or stubbed.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see ReactiveCassandraTemplate
|
||||
* @see Flux
|
||||
* @see Mono
|
||||
*/
|
||||
public interface ReactiveCassandraOperations {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with static CQL
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting items to a stream of entities.
|
||||
*
|
||||
* @param cql must not be {@literal null}.
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @return the converted results
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> select(String cql, Class<T> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting item to an entity.
|
||||
*
|
||||
* @param cql must not be {@literal null}.
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @return the result object returned by the action or {@link Mono#empty()}
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> selectOne(String cql, Class<T> entityClass) throws DataAccessException;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with com.datastax.driver.core.Statement
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting items to a stream of entities.
|
||||
*
|
||||
* @param statement must not be {@literal null}.
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @return the result objects returned by the action.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> select(Statement statement, Class<T> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting item to an entity.
|
||||
*
|
||||
* @param statement must not be {@literal null}.
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @return the result object returned by the action or {@link Mono#empty()}
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> selectOne(Statement statement, Class<T> entityClass) throws DataAccessException;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with entities
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Execute the Select by {@code id} for the given {@code entityClass}.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @return the result object returned by the action or {@link Mono#empty()}
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> selectOneById(Object id, Class<T> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Determine whether the row {@code entityClass} with the given {@code id} exists.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @param entityClass must not be {@literal null}.
|
||||
* @return {@literal true} if the object exists.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Mono<Boolean> exists(Object id, Class<?> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Returns the number of rows for the given entity class.
|
||||
*
|
||||
* @param entityClass must not be {@literal null}.
|
||||
* @return the number of existing entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Mono<Long> count(Class<?> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Insert the given entity and emit the entity if the insert was applied.
|
||||
*
|
||||
* @param entity The entity to insert, must not be {@literal null}.
|
||||
* @return the inserted entity.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> insert(T entity) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Insert the given entity applying {@link WriteOptions} and emit the entity if the insert was applied.
|
||||
*
|
||||
* @param entity The entity to insert, must not be {@literal null}.
|
||||
* @param options may be {@literal null}.
|
||||
* @@return the inserted entity.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> insert(T entity, WriteOptions options) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Insert the given entities and emit the entity if the insert was applied.
|
||||
*
|
||||
* @param entities The entities to insert, must not be {@literal null}.
|
||||
* @return the inserted entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> insert(Publisher<? extends T> entities) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Insert the given entities applying {@link WriteOptions} and emit the entity if the insert was applied.
|
||||
*
|
||||
* @param entities The entities to insert, must not be {@literal null}.
|
||||
* @param options may be {@literal null}.
|
||||
* @return the inserted entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> insert(Publisher<? extends T> entities, WriteOptions options) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Update the given entity and emit the entity if the update was applied.
|
||||
*
|
||||
* @param entity The entity to update, must not be {@literal null}.
|
||||
* @return the updated entity.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> update(T entity) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Update the given entity applying {@link WriteOptions} and emit the entity if the update was applied.
|
||||
*
|
||||
* @param entity The entity to update, must not be {@literal null}.
|
||||
* @param options may be {@literal null}.
|
||||
* @return the updated entity.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> update(T entity, WriteOptions options) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Update the given entities and emit the entity if the update was applied.
|
||||
*
|
||||
* @param entities The entities to update, must not be {@literal null}.
|
||||
* @return the updated entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> update(Publisher<? extends T> entities) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Update the given entities applying {@link WriteOptions} and emit the entity if the update was applied.
|
||||
*
|
||||
* @param entities The entities to update.
|
||||
* @param options may be {@literal null}.
|
||||
* @return the updated entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> update(Publisher<? extends T> entities, WriteOptions options) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Remove the given object from the table by id.
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @return {@literal true} if the deletion was applied.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Mono<Boolean> deleteById(Object id, Class<?> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Delete the given entity and emit the entity if the delete was applied.
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @return the deleted entity.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> delete(T entity) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Delete the given entity applying {@link QueryOptions} and emit the entity if the delete was applied.
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @param options may be {@literal null}.
|
||||
* @return the deleted entity.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Mono<T> delete(T entity, QueryOptions options) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Delete the given entities and emit the entity if the delete was applied.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
* @return the deleted entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> delete(Publisher<? extends T> entities) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Delete the given entities applying {@link QueryOptions} and emit the entity if the delete was applied.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
* @param options may be {@literal null}.
|
||||
* @return the deleted entities.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
<T> Flux<T> delete(Publisher<? extends T> entities, QueryOptions options) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Execute a {@code TRUNCATE} query to remove all entities of a given class.
|
||||
*
|
||||
* @param entityClass The entity type must not be {@literal null}.
|
||||
* @throws DataAccessException if there is any problem issuing the execution.
|
||||
*/
|
||||
Mono<Void> truncate(Class<?> entityClass) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Returns the underlying {@link CassandraConverter}.
|
||||
*
|
||||
* @return the underlying {@link CassandraConverter}.
|
||||
*/
|
||||
CassandraConverter getConverter();
|
||||
|
||||
/**
|
||||
* Expose the underlying {@link ReactiveCqlOperations} to allow CQL operations.
|
||||
*
|
||||
* @return the underlying {@link ReactiveCqlOperations}.
|
||||
* @see ReactiveCqlOperations
|
||||
*/
|
||||
ReactiveCqlOperations getReactiveCqlOperations();
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import static org.springframework.data.cassandra.core.CassandraTemplate.*;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.cassandra.core.DefaultReactiveSessionFactory;
|
||||
import org.springframework.cassandra.core.QueryOptions;
|
||||
import org.springframework.cassandra.core.ReactiveCqlOperations;
|
||||
import org.springframework.cassandra.core.ReactiveCqlTemplate;
|
||||
import org.springframework.cassandra.core.ReactiveResultSet;
|
||||
import org.springframework.cassandra.core.ReactiveSession;
|
||||
import org.springframework.cassandra.core.ReactiveSessionCallback;
|
||||
import org.springframework.cassandra.core.ReactiveSessionFactory;
|
||||
import org.springframework.cassandra.core.WriteOptions;
|
||||
import org.springframework.cassandra.core.cql.CqlIdentifier;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.cassandra.convert.CassandraConverter;
|
||||
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraPersistentEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.querybuilder.Delete;
|
||||
import com.datastax.driver.core.querybuilder.Insert;
|
||||
import com.datastax.driver.core.querybuilder.QueryBuilder;
|
||||
import com.datastax.driver.core.querybuilder.Select;
|
||||
import com.datastax.driver.core.querybuilder.Truncate;
|
||||
import com.datastax.driver.core.querybuilder.Update;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Primary implementation of {@link ReactiveCassandraOperations}. It simplifies the use of Reactive Cassandra usage and
|
||||
* helps to avoid common errors. It executes core Cassandra workflow. This class executes CQL queries or updates,
|
||||
* initiating iteration over {@link ReactiveResultSet} and catching Cassandra exceptions and translating them to the
|
||||
* generic, more informative exception hierarchy defined in the {@code org.springframework.dao} package.
|
||||
* <p>
|
||||
* Can be used within a service implementation via direct instantiation with a {@link ReactiveSessionFactory} reference,
|
||||
* or get prepared in an application context and given to services as bean reference.
|
||||
* <p>
|
||||
* Note: The {@link ReactiveSessionFactory} should always be configured as a bean in the application context, in the
|
||||
* first case given to the service directly, in the second case to the prepared template.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ReactiveCassandraTemplate implements ReactiveCassandraOperations {
|
||||
|
||||
private final CassandraConverter converter;
|
||||
private final CassandraMappingContext mappingContext;
|
||||
private final ReactiveCqlOperations cqlOperations;
|
||||
|
||||
/**
|
||||
* Creates an instance of {@link ReactiveCassandraTemplate} initialized with the given {@link ReactiveSession} and a
|
||||
* default {@link MappingCassandraConverter}.
|
||||
*
|
||||
* @param session {@link ReactiveSession} used to interact with Cassandra; must not be {@literal null}.
|
||||
* @see CassandraConverter
|
||||
* @see Session
|
||||
*/
|
||||
public ReactiveCassandraTemplate(ReactiveSession session) {
|
||||
this(session, newConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of {@link CassandraTemplate} initialized with the given {@link ReactiveSession} and
|
||||
* {@link CassandraConverter}.
|
||||
*
|
||||
* @param session {@link ReactiveSession} used to interact with Cassandra; must not be {@literal null}.
|
||||
* @param converter {@link CassandraConverter} used to convert between Java and Cassandra types; must not be
|
||||
* {@literal null}.
|
||||
* @see org.springframework.data.cassandra.convert.CassandraConverter
|
||||
* @see com.datastax.driver.core.Session
|
||||
*/
|
||||
public ReactiveCassandraTemplate(ReactiveSession session, CassandraConverter converter) {
|
||||
this(new DefaultReactiveSessionFactory(session), converter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of {@link ReactiveCassandraTemplate} initialized with the given {@link ReactiveSessionFactory}
|
||||
* and {@link CassandraConverter}.
|
||||
*
|
||||
* @param sessionFactory {@link ReactiveSessionFactory} used to interact with Cassandra; must not be {@literal null}.
|
||||
* @param converter {@link CassandraConverter} used to convert between Java and Cassandra types; must not be
|
||||
* {@literal null}.
|
||||
* @see org.springframework.data.cassandra.convert.CassandraConverter
|
||||
* @see com.datastax.driver.core.Session
|
||||
*/
|
||||
public ReactiveCassandraTemplate(ReactiveSessionFactory sessionFactory, CassandraConverter converter) {
|
||||
|
||||
Assert.notNull(sessionFactory, "ReactiveSessionFactory must not be null");
|
||||
Assert.notNull(converter, "CassandraConverter must not be null");
|
||||
|
||||
this.converter = converter;
|
||||
this.mappingContext = this.converter.getMappingContext();
|
||||
this.cqlOperations = new ReactiveCqlTemplate(sessionFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of {@link ReactiveCassandraTemplate} initialized with the given {@link ReactiveCqlOperations}
|
||||
* and {@link CassandraConverter}.
|
||||
*
|
||||
* @param reactiveCqlOperations {@link ReactiveCqlOperations} used to interact with Cassandra; must not be
|
||||
* {@literal null}.
|
||||
* @param converter {@link CassandraConverter} used to convert between Java and Cassandra types; must not be
|
||||
* {@literal null}.
|
||||
* @see org.springframework.data.cassandra.convert.CassandraConverter
|
||||
* @see com.datastax.driver.core.Session
|
||||
*/
|
||||
public ReactiveCassandraTemplate(ReactiveCqlOperations reactiveCqlOperations, CassandraConverter converter) {
|
||||
|
||||
Assert.notNull(reactiveCqlOperations, "ReactiveCqlOperations must not be null");
|
||||
Assert.notNull(converter, "CassandraConverter must not be null");
|
||||
|
||||
this.converter = converter;
|
||||
this.mappingContext = this.converter.getMappingContext();
|
||||
this.cqlOperations = reactiveCqlOperations;
|
||||
}
|
||||
|
||||
private static MappingCassandraConverter newConverter() {
|
||||
|
||||
MappingCassandraConverter converter = new MappingCassandraConverter();
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
return converter;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with static CQL
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#select(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> select(String cql, Class<T> entityClass) {
|
||||
|
||||
Assert.hasText(cql, "Statement must not be empty");
|
||||
|
||||
return select(new SimpleStatement(cql), entityClass);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#selectOne(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> selectOne(String cql, Class<T> entityClass) {
|
||||
return select(cql, entityClass).next();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with com.datastax.driver.core.Statement
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#select(com.datastax.driver.core.Statement, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> select(Statement cql, Class<T> entityClass) {
|
||||
|
||||
Assert.notNull(cql, "Statement must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
return cqlOperations.query(cql, (row, rowNum) -> converter.read(entityClass, row));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#selectOne(com.datastax.driver.core.Statement, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> selectOne(Statement statement, Class<T> entityClass) {
|
||||
return select(statement, entityClass).next();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods dealing with entities
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#selectOneById(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> selectOneById(Object id, Class<T> entityClass) {
|
||||
|
||||
Assert.notNull(id, "Id must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
CassandraPersistentEntity<?> entity = getPersistentEntity(entityClass);
|
||||
Select select = QueryBuilder.select().all().from(entity.getTableName().toCql());
|
||||
|
||||
converter.write(id, select.where(), entity);
|
||||
|
||||
return selectOne(select, entityClass);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#exists(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> exists(Object id, Class<?> entityClass) {
|
||||
|
||||
Assert.notNull(id, "Id must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
CassandraPersistentEntity<?> entity = getPersistentEntity(entityClass);
|
||||
Select select = QueryBuilder.select().from(entity.getTableName().toCql());
|
||||
converter.write(id, select.where(), entity);
|
||||
|
||||
return cqlOperations.queryForRows(select).hasElements();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#count(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Long> count(Class<?> entityClass) {
|
||||
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
Select select = QueryBuilder.select().countAll().from(getPersistentEntity(entityClass).getTableName().toCql());
|
||||
|
||||
return cqlOperations.queryForObject(select, Long.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> insert(T entity) {
|
||||
return insert(entity, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(java.lang.Object, org.springframework.cassandra.core.WriteOptions)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> insert(T entity, WriteOptions options) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
|
||||
Insert insertQuery = createInsertQuery(tableName.toCql(), entity, options, converter);
|
||||
|
||||
return cqlOperations.execute((ReactiveSessionCallback<T>) session -> (Publisher<T>) session.execute(insertQuery)
|
||||
.flatMap(reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty())).next();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(org.reactivestreams.Publisher)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> insert(Publisher<? extends T> entities) {
|
||||
return insert(entities, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#insert(org.reactivestreams.Publisher, org.springframework.cassandra.core.WriteOptions)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> insert(Publisher<? extends T> entities, WriteOptions options) {
|
||||
|
||||
Assert.notNull(entities, "Entity publisher must not be null");
|
||||
return Flux.from(entities).flatMap(entity -> insert(entity, options));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> update(T entity) {
|
||||
return update(entity, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(java.lang.Object, org.springframework.cassandra.core.WriteOptions)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> update(T entity, WriteOptions options) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
|
||||
Update update = createUpdateQuery(tableName.toCql(), entity, options, converter);
|
||||
|
||||
return cqlOperations.execute((ReactiveSessionCallback<T>) session -> (Publisher<T>) session.execute(update)
|
||||
.flatMap(reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty())).next();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(org.reactivestreams.Publisher)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> update(Publisher<? extends T> entities) {
|
||||
return update(entities, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(org.reactivestreams.Publisher, org.springframework.cassandra.core.WriteOptions)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> update(Publisher<? extends T> entities, WriteOptions options) {
|
||||
|
||||
Assert.notNull(entities, "Entity publisher must not be null");
|
||||
return Flux.from(entities).flatMap(entity -> update(entity, options));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#deleteById(java.lang.Object, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Boolean> deleteById(Object id, Class<?> entityClass) {
|
||||
|
||||
Assert.notNull(id, "Id must not be null");
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
CassandraPersistentEntity<?> entity = getPersistentEntity(entityClass);
|
||||
Delete delete = QueryBuilder.delete().from(entity.getTableName().toCql());
|
||||
|
||||
converter.write(id, delete.where(), entity);
|
||||
|
||||
return cqlOperations.execute(delete);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#delete(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> delete(T entity) {
|
||||
return delete(entity, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#delete(java.lang.Object, org.springframework.cassandra.core.QueryOptions)
|
||||
*/
|
||||
@Override
|
||||
public <T> Mono<T> delete(T entity, QueryOptions options) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
CqlIdentifier tableName = getTableName(entity);
|
||||
|
||||
Delete delete = createDeleteQuery(tableName.toCql(), entity, options, converter);
|
||||
|
||||
return cqlOperations.execute((ReactiveSessionCallback<T>) session -> (Publisher<T>) session.execute(delete)
|
||||
.flatMap(reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty())).next();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#delete(org.reactivestreams.Publisher)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> delete(Publisher<? extends T> entities) {
|
||||
return delete(entities, null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#delete(org.reactivestreams.Publisher, org.springframework.cassandra.core.QueryOptions)
|
||||
*/
|
||||
@Override
|
||||
public <T> Flux<T> delete(Publisher<? extends T> entities, QueryOptions options) {
|
||||
|
||||
Assert.notNull(entities, "Entity publisher must not be null");
|
||||
return Flux.from(entities).flatMap(entity -> delete(entity, options));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#truncate(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> truncate(Class<?> entityClass) {
|
||||
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
Truncate truncate = QueryBuilder.truncate(getPersistentEntity(entityClass).getTableName().toCql());
|
||||
|
||||
return cqlOperations.execute(truncate).then();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getConverter()
|
||||
*/
|
||||
@Override
|
||||
public CassandraConverter getConverter() {
|
||||
return converter;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getReactiveCqlOperations()
|
||||
*/
|
||||
@Override
|
||||
public ReactiveCqlOperations getReactiveCqlOperations() {
|
||||
return cqlOperations;
|
||||
}
|
||||
|
||||
private <T> CassandraPersistentEntity<?> getPersistentEntity(Class<T> entityClass) {
|
||||
|
||||
Assert.notNull(entityClass, "Entity type must not be null");
|
||||
|
||||
CassandraPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
|
||||
|
||||
if (entity == null) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
String.format("No Persistent Entity information found for the class [%s]", entityClass.getName()));
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private CqlIdentifier getTableName(Object entity) {
|
||||
return getPersistentEntity(ClassUtils.getUserClass(entity)).getTableName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.data.repository.NoRepositoryBean;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Cassandra specific {@link org.springframework.data.repository.Repository} interface with reactive support.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
@NoRepositoryBean
|
||||
public interface ReactiveCassandraRepository<T, ID extends Serializable> extends ReactiveCrudRepository<T, ID> {
|
||||
|
||||
/**
|
||||
* Inserts the given entity. Assumes the instance to be new to be able to apply insertion optimizations. Use the
|
||||
* returned instance for further operations as the save operation might have changed the entity instance completely.
|
||||
* Prefer using {@link #save(Object)} instead to avoid the usage of store-specific API.
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @return the saved entity
|
||||
*/
|
||||
<S extends T> Mono<S> insert(S entity);
|
||||
|
||||
/**
|
||||
* Inserts the given entities. Assumes the instance to be new to be able to apply insertion optimizations. Use the
|
||||
* returned instance for further operations as the save operation might have changed the entity instance completely.
|
||||
* Prefer using {@link #save(Object)} instead to avoid the usage of store-specific API.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
* @return the saved entity
|
||||
*/
|
||||
<S extends T> Flux<S> insert(Iterable<S> entities);
|
||||
|
||||
/**
|
||||
* Inserts the given a given entities. Assumes the instance to be new to be able to apply insertion optimizations. Use
|
||||
* the returned instance for further operations as the save operation might have changed the entity instance
|
||||
* completely. Prefer using {@link #save(Object)} instead to avoid the usage of store-specific API.
|
||||
*
|
||||
* @param entities must not be {@literal null}.
|
||||
* @return the saved entity
|
||||
*/
|
||||
<S extends T> Flux<S> insert(Publisher<S> entities);
|
||||
}
|
||||
@@ -18,18 +18,23 @@ package org.springframework.data.cassandra.repository.config;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.cassandra.config.xml.ParsingUtils;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.data.cassandra.config.DefaultBeanNames;
|
||||
import org.springframework.data.cassandra.mapping.Table;
|
||||
import org.springframework.data.cassandra.repository.CassandraRepository;
|
||||
import org.springframework.data.cassandra.repository.support.CassandraRepositoryFactoryBean;
|
||||
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.config.RepositoryConfiguration;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.query.ReactiveWrappers;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
@@ -44,6 +49,15 @@ public class CassandraRepositoryConfigurationExtension extends RepositoryConfigu
|
||||
|
||||
private static final String CASSANDRA_TEMPLATE_REF = "cassandra-template-ref";
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModuleName()
|
||||
*/
|
||||
@Override
|
||||
public String getModuleName() {
|
||||
return "Reactive Cassandra";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getModulePrefix() {
|
||||
return "cassandra";
|
||||
@@ -92,4 +106,24 @@ public class CassandraRepositoryConfigurationExtension extends RepositoryConfigu
|
||||
return Collections.<Class<?>> singleton(CassandraRepository.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends RepositoryConfigurationSource> Collection<RepositoryConfiguration<T>> getRepositoryConfigurations(
|
||||
T configSource, ResourceLoader loader, boolean strictMatchesOnly) {
|
||||
|
||||
Collection<RepositoryConfiguration<T>> repositoryConfigurations = super.getRepositoryConfigurations(configSource,
|
||||
loader, strictMatchesOnly);
|
||||
|
||||
if (ReactiveWrappers.isAvailable()) {
|
||||
|
||||
return repositoryConfigurations.stream().filter(configuration -> {
|
||||
|
||||
Class<?> repositoryInterface = super.loadRepositoryInterface(configuration, loader);
|
||||
return !RepositoryType.isReactiveRepository(repositoryInterface);
|
||||
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return repositoryConfigurations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.config;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.cassandra.repository.support.CassandraRepositoryFactoryBean;
|
||||
import org.springframework.data.cassandra.repository.support.ReactiveCassandraRepositoryFactoryBean;
|
||||
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
|
||||
/**
|
||||
* Annotation to activate reactive Cassandra repositories. If no base package is configured through either
|
||||
* {@link #value()}, {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of
|
||||
* annotated class.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@Import(ReactiveCassandraRepositoriesRegistrar.class)
|
||||
public @interface EnableReactiveCassandraRepositories {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
|
||||
* {@code @EnableCassandraRepositories("org.my.pkg")} instead of
|
||||
* {@code @EnableCassandraRepositories(basePackages="org.my.pkg")}.
|
||||
*/
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this
|
||||
* attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
|
||||
*/
|
||||
String[] basePackages() default {};
|
||||
|
||||
/**
|
||||
* Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
|
||||
* package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
|
||||
* each package that serves no purpose other than being referenced by this attribute.
|
||||
*/
|
||||
Class<?>[] basePackageClasses() default {};
|
||||
|
||||
/**
|
||||
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
|
||||
* everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters.
|
||||
*/
|
||||
Filter[] includeFilters() default {};
|
||||
|
||||
/**
|
||||
* Specifies which types are not eligible for component scanning.
|
||||
*/
|
||||
Filter[] excludeFilters() default {};
|
||||
|
||||
/**
|
||||
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
|
||||
* for a repository named {@code UserRepository} the corresponding implementation class will be looked up scanning for
|
||||
* {@code UserRepositoryImpl}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String repositoryImplementationPostfix() default "Impl";
|
||||
|
||||
/**
|
||||
* Configures the location of where to find the Spring Data named queries properties file. Will default to
|
||||
* {@code META-INF/cassandra-named-queries.properties}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String namedQueriesLocation() default "";
|
||||
|
||||
/**
|
||||
* Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
|
||||
* {@link Key#CREATE_IF_NOT_FOUND}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
|
||||
|
||||
/**
|
||||
* Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
|
||||
* {@link ReactiveCassandraRepositoryFactoryBean}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<?> repositoryFactoryBeanClass() default ReactiveCassandraRepositoryFactoryBean.class;
|
||||
|
||||
/**
|
||||
* Configure the repository base class to be used to create repository proxies for this particular configuration.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link org.springframework.data.cassandra.core.ReactiveCassandraTemplate} bean to be
|
||||
* used with the repositories detected.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String reactiveCassandraTemplateRef() default "reactiveCassandraTemplate";
|
||||
|
||||
/**
|
||||
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
|
||||
* repositories infrastructure.
|
||||
*/
|
||||
boolean considerNestedRepositories() default false;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.config;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
|
||||
|
||||
/**
|
||||
* {@link ImportBeanDefinitionRegistrar} to setup Cassandra repositories via
|
||||
* {@link EnableReactiveCassandraRepositories}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
class ReactiveCassandraRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation()
|
||||
*/
|
||||
@Override
|
||||
protected Class<? extends Annotation> getAnnotation() {
|
||||
return EnableReactiveCassandraRepositories.class;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension()
|
||||
*/
|
||||
@Override
|
||||
protected RepositoryConfigurationExtension getExtension() {
|
||||
return new ReactiveCassandraRepositoryConfigurationExtension();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.config;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.data.cassandra.mapping.Table;
|
||||
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
|
||||
import org.springframework.data.cassandra.repository.support.ReactiveCassandraRepositoryFactoryBean;
|
||||
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.config.RepositoryConfiguration;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link RepositoryConfigurationExtension} for Cassandra.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ReactiveCassandraRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModuleName()
|
||||
*/
|
||||
@Override
|
||||
public String getModuleName() {
|
||||
return "Reactive Cassandra";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix()
|
||||
*/
|
||||
@Override
|
||||
protected String getModulePrefix() {
|
||||
return "cassandra";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getRepositoryFactoryClassName()
|
||||
*/
|
||||
@Override
|
||||
public String getRepositoryFactoryClassName() {
|
||||
return ReactiveCassandraRepositoryFactoryBean.class.getName();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.XmlRepositoryConfigurationSource)
|
||||
*/
|
||||
@Override
|
||||
public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) {}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource)
|
||||
*/
|
||||
@Override
|
||||
public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) {
|
||||
|
||||
AnnotationAttributes attributes = config.getAttributes();
|
||||
|
||||
String reactiveCassandraTemplateRef = attributes.getString("reactiveCassandraTemplateRef");
|
||||
if (StringUtils.hasText(reactiveCassandraTemplateRef)) {
|
||||
builder.addPropertyReference("reactiveCassandraOperations", reactiveCassandraTemplateRef);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingAnnotations()
|
||||
*/
|
||||
@Override
|
||||
protected Collection<Class<? extends Annotation>> getIdentifyingAnnotations() {
|
||||
return Collections.<Class<? extends Annotation>>singleton(Table.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingTypes()
|
||||
*/
|
||||
@Override
|
||||
protected Collection<Class<?>> getIdentifyingTypes() {
|
||||
return Collections.<Class<?>>singleton(ReactiveCassandraRepository.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getRepositoryConfigurations(T, org.springframework.core.io.ResourceLoader, boolean)
|
||||
*/
|
||||
@Override
|
||||
public <T extends RepositoryConfigurationSource> Collection<RepositoryConfiguration<T>> getRepositoryConfigurations(
|
||||
T configSource, ResourceLoader loader, boolean strictMatchesOnly) {
|
||||
|
||||
Collection<RepositoryConfiguration<T>> repositoryConfigurations = super.getRepositoryConfigurations(configSource,
|
||||
loader, strictMatchesOnly);
|
||||
|
||||
return repositoryConfigurations.stream().filter(configuration -> {
|
||||
|
||||
Class<?> repositoryInterface = super.loadRepositoryInterface(configuration, loader);
|
||||
return RepositoryType.isReactiveRepository(repositoryInterface);
|
||||
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.config;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.data.repository.query.ReactiveWrappers;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
/**
|
||||
* Utility class to discover whether a repository interface uses reactive wrapper types.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
@UtilityClass
|
||||
class RepositoryType {
|
||||
|
||||
/**
|
||||
* Check whether {@code repositoryInterface} uses reactive wrapper types as return type or parameter types in its
|
||||
* methods.
|
||||
*
|
||||
* @param repositoryInterface must not be {@literal null}.
|
||||
* @return {@literal true} if the {@code repositoryInterface} uses reactive wrapper types.
|
||||
* @see ReactiveWrappers
|
||||
* @see ReactiveWrappers#isAvailable()
|
||||
*/
|
||||
public static boolean isReactiveRepository(Class<?> repositoryInterface) {
|
||||
|
||||
if (!ReactiveWrappers.isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Method[] methods = repositoryInterface.getMethods();
|
||||
|
||||
for (Method method : methods) {
|
||||
|
||||
if (usesReactiveWrappers(method)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean usesReactiveWrappers(Method method) {
|
||||
|
||||
if (ReactiveWrappers.supports(method.getReturnType())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Class<?> parameterType : method.getParameterTypes()) {
|
||||
if (ReactiveWrappers.supports(parameterType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.cassandra.core.CassandraOperations;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.CollectionExecution;
|
||||
import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.ResultProcessingConverter;
|
||||
import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.ResultProcessingExecution;
|
||||
import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution.SingleEntityExecution;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.ReactiveWrapperConverters;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Base class for reactive {@link RepositoryQuery} implementations for Cassandra.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public abstract class AbstractReactiveCassandraQuery implements RepositoryQuery {
|
||||
|
||||
protected static Logger log = LoggerFactory.getLogger(AbstractReactiveCassandraQuery.class);
|
||||
|
||||
private final CassandraQueryMethod method;
|
||||
private final ReactiveCassandraOperations operations;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AbstractReactiveCassandraQuery} from the given {@link CassandraQueryMethod} and
|
||||
* {@link CassandraOperations}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param operations must not be {@literal null}.
|
||||
*/
|
||||
public AbstractReactiveCassandraQuery(CassandraQueryMethod method, ReactiveCassandraOperations operations) {
|
||||
|
||||
Assert.notNull(method, "CassandraQueryMethod must not be null");
|
||||
Assert.notNull(operations, "ReactiveCassandraOperations must not be null");
|
||||
|
||||
this.method = method;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
|
||||
*/
|
||||
@Override
|
||||
public CassandraQueryMethod getQueryMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
|
||||
*/
|
||||
@Override
|
||||
public Object execute(Object[] parameters) {
|
||||
|
||||
if (hasReactiveWrapperParameter()) {
|
||||
return executeDeferred(parameters);
|
||||
}
|
||||
|
||||
return execute(new ReactiveCassandraParameterAccessor(method, parameters));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object executeDeferred(Object[] parameters) {
|
||||
|
||||
ReactiveCassandraParameterAccessor accessor = new ReactiveCassandraParameterAccessor(method, parameters);
|
||||
|
||||
if (getQueryMethod().isCollectionQuery()) {
|
||||
return Flux.defer(() -> (Publisher<Object>) execute(accessor));
|
||||
}
|
||||
|
||||
return Mono.defer(() -> (Mono<Object>) execute(accessor));
|
||||
}
|
||||
|
||||
private Object execute(CassandraParameterAccessor parameterAccessor) {
|
||||
|
||||
CassandraParameterAccessor convertingParameterAccessor = new ConvertingParameterAccessor(operations.getConverter(),
|
||||
parameterAccessor);
|
||||
|
||||
String query = createQuery(convertingParameterAccessor);
|
||||
|
||||
ResultProcessor resultProcessor = method.getResultProcessor().withDynamicProjection(convertingParameterAccessor);
|
||||
|
||||
ReactiveCassandraQueryExecution queryExecution = getExecution(query, convertingParameterAccessor,
|
||||
new ResultProcessingConverter(resultProcessor));
|
||||
|
||||
CassandraReturnedType returnedType = new CassandraReturnedType(resultProcessor.getReturnedType(),
|
||||
operations.getConverter().getCustomConversions());
|
||||
|
||||
Class<?> resultType = (returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType());
|
||||
|
||||
return queryExecution.execute(query, resultType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the execution instance to use.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param accessor must not be {@literal null}.
|
||||
* @param resultProcessing must not be {@literal null}. @return
|
||||
*/
|
||||
private ReactiveCassandraQueryExecution getExecution(String query, CassandraParameterAccessor accessor,
|
||||
Converter<Object, Object> resultProcessing) {
|
||||
|
||||
return new ResultProcessingExecution(getExecutionToWrap(accessor, resultProcessing), resultProcessing);
|
||||
}
|
||||
|
||||
private ReactiveCassandraQueryExecution getExecutionToWrap(CassandraParameterAccessor accessor,
|
||||
Converter<Object, Object> resultProcessing) {
|
||||
|
||||
if (method.isCollectionQuery()) {
|
||||
return new CollectionExecution(operations);
|
||||
} else {
|
||||
return new SingleEntityExecution(operations);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasReactiveWrapperParameter() {
|
||||
|
||||
for (CassandraParameters.CassandraParameter cassandraParameter : method.getParameters()) {
|
||||
if (ReactiveWrapperConverters.supports(cassandraParameter.getType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string query using the given {@link ParameterAccessor}
|
||||
*
|
||||
* @param accessor must not be {@literal null}.
|
||||
*/
|
||||
protected abstract String createQuery(CassandraParameterAccessor accessor);
|
||||
}
|
||||
@@ -19,10 +19,13 @@ import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.data.cassandra.mapping.CassandraType;
|
||||
import org.springframework.data.cassandra.repository.query.CassandraParameters.CassandraParameter;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.ReactiveWrappers;
|
||||
import org.springframework.data.repository.util.QueryExecutionConverters;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -67,9 +70,10 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class CassandraParameter extends Parameter {
|
||||
static class CassandraParameter extends Parameter {
|
||||
|
||||
private final CassandraType cassandraType;
|
||||
private final Class<?> parameterType;
|
||||
|
||||
protected CassandraParameter(MethodParameter parameter) {
|
||||
|
||||
@@ -78,23 +82,75 @@ public class CassandraParameters extends Parameters<CassandraParameters, Cassand
|
||||
if (parameter.hasParameterAnnotation(CassandraType.class)) {
|
||||
CassandraType cassandraType = parameter.getParameterAnnotation(CassandraType.class);
|
||||
|
||||
Assert.notNull(cassandraType.type(), String.format(
|
||||
"You must specify the type() when annotating method parameters with @%s",
|
||||
CassandraType.class.getSimpleName()));
|
||||
Assert.notNull(cassandraType.type(),
|
||||
String.format("You must specify the type() when annotating method parameters with @%s",
|
||||
CassandraType.class.getSimpleName()));
|
||||
|
||||
this.cassandraType = cassandraType;
|
||||
} else {
|
||||
this.cassandraType = null;
|
||||
}
|
||||
|
||||
parameterType = potentiallyUnwrapParameterType(parameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link CassandraType} for the declared parameter if specified using {@link org.springframework.data.cassandra.mapping.CassandraType}.
|
||||
* Returns the {@link CassandraType} for the declared parameter if specified using
|
||||
* {@link org.springframework.data.cassandra.mapping.CassandraType}.
|
||||
*
|
||||
* @return the {@link CassandraType} or {@literal null}.
|
||||
*/
|
||||
public CassandraType getCassandraType() {
|
||||
return cassandraType;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.Parameter#getType()
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getType() {
|
||||
return parameterType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component type if the given {@link MethodParameter} is a wrapper type and the wrapper should be
|
||||
* unwrapped.
|
||||
*
|
||||
* @param parameter must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static Class<?> potentiallyUnwrapParameterType(MethodParameter parameter) {
|
||||
|
||||
Class<?> originalType = parameter.getParameterType();
|
||||
|
||||
if (isWrapped(parameter) && shouldUnwrap(parameter)) {
|
||||
return ResolvableType.forMethodParameter(parameter).getGeneric(0).getRawClass();
|
||||
}
|
||||
|
||||
return originalType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link MethodParameter} is wrapped in a wrapper type.
|
||||
*
|
||||
* @param parameter must not be {@literal null}.
|
||||
* @return
|
||||
* @see QueryExecutionConverters
|
||||
*/
|
||||
private static boolean isWrapped(MethodParameter parameter) {
|
||||
return QueryExecutionConverters.supports(parameter.getParameterType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link MethodParameter} should be unwrapped.
|
||||
*
|
||||
* @param parameter must not be {@literal null}.
|
||||
* @return
|
||||
* @see QueryExecutionConverters
|
||||
*/
|
||||
private static boolean shouldUnwrap(MethodParameter parameter) {
|
||||
return QueryExecutionConverters.supportsUnwrapping(parameter.getParameterType())
|
||||
|| ReactiveWrappers.supports(parameter.getParameterType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ public class CassandraParametersParameterAccessor extends ParametersParameterAcc
|
||||
|
||||
CassandraType cassandraType = findCassandraType(index);
|
||||
|
||||
return (cassandraType != null ? CassandraSimpleTypeHolder.getDataTypeFor(cassandraType.type())
|
||||
: CassandraSimpleTypeHolder.getDataTypeFor(getParameterType(index)));
|
||||
return (cassandraType != null ? CassandraSimpleTypeHolder.getDataTypeFor(cassandraType.type())
|
||||
: CassandraSimpleTypeHolder.getDataTypeFor(getParameterType(index)));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -58,8 +58,8 @@ public class CassandraQueryMethod extends QueryMethod {
|
||||
* @param projectionFactory must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
public CassandraQueryMethod(Method method, RepositoryMetadata repositoryMetadata,
|
||||
ProjectionFactory projectionFactory, CassandraMappingContext mappingContext) {
|
||||
public CassandraQueryMethod(Method method, RepositoryMetadata repositoryMetadata, ProjectionFactory projectionFactory,
|
||||
CassandraMappingContext mappingContext) {
|
||||
|
||||
super(method, repositoryMetadata, projectionFactory);
|
||||
|
||||
@@ -83,6 +83,10 @@ public class CassandraQueryMethod extends QueryMethod {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryMethod#getEntityInformation()
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public CassandraEntityMetadata<?> getEntityInformation() {
|
||||
@@ -93,21 +97,21 @@ public class CassandraQueryMethod extends QueryMethod {
|
||||
|
||||
if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) {
|
||||
this.entityMetadata = new SimpleCassandraEntityMetadata<Object>((Class<Object>) domainClass,
|
||||
mappingContext.getPersistentEntity(domainClass));
|
||||
mappingContext.getPersistentEntity(domainClass));
|
||||
|
||||
} else {
|
||||
CassandraPersistentEntity<?> returnedEntity = mappingContext.getPersistentEntity(returnedObjectType);
|
||||
CassandraPersistentEntity<?> managedEntity = mappingContext.getPersistentEntity(domainClass);
|
||||
|
||||
returnedEntity = (returnedEntity == null || returnedEntity.getType().isInterface()
|
||||
? managedEntity : returnedEntity);
|
||||
returnedEntity = (returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity
|
||||
: returnedEntity);
|
||||
|
||||
// TODO collectionEntity?
|
||||
CassandraPersistentEntity<?> collectionEntity = domainClass.isAssignableFrom(returnedObjectType)
|
||||
? returnedEntity : managedEntity;
|
||||
? returnedEntity : managedEntity;
|
||||
|
||||
this.entityMetadata = new SimpleCassandraEntityMetadata<Object>(
|
||||
(Class<Object>) returnedEntity.getType(), collectionEntity);
|
||||
this.entityMetadata = new SimpleCassandraEntityMetadata<Object>((Class<Object>) returnedEntity.getType(),
|
||||
collectionEntity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.cassandra.convert.CustomConversions;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Represents a {@link ReturnedType} in the context of Spring Data Cassandra.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class CassandraReturnedType {
|
||||
|
||||
private final ReturnedType returnedType;
|
||||
private final CustomConversions customConversions;
|
||||
|
||||
CassandraReturnedType(ReturnedType returnedType, CustomConversions customConversions) {
|
||||
this.returnedType = returnedType;
|
||||
this.customConversions = customConversions;
|
||||
}
|
||||
|
||||
boolean isProjecting() {
|
||||
|
||||
if (!returnedType.isProjecting()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spring Data Cassandra allows List<Map<String, Object> and Map<String, Object> declarations
|
||||
// on query methods so we don't want to let projection kick in
|
||||
if (ClassUtils.isAssignable(Map.class, returnedType.getReturnedType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type conversion using registered conversions is handled on template level
|
||||
if (customConversions.hasCustomWriteTarget(returnedType.getReturnedType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't apply projection on Cassandra simple types
|
||||
return !customConversions.isSimpleType(returnedType.getReturnedType());
|
||||
}
|
||||
|
||||
Class<?> getDomainType() {
|
||||
return returnedType.getDomainType();
|
||||
}
|
||||
|
||||
Class<?> getReturnedType() {
|
||||
return returnedType.getReturnedType();
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,9 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor {
|
||||
return potentiallyConvert(index, delegate.getBindableValue(index), null);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.CassandraParameterAccessor#findCassandraType(int)
|
||||
*/
|
||||
@Override
|
||||
public CassandraType findCassandraType(int index) {
|
||||
return delegate.findCassandraType(index);
|
||||
@@ -324,6 +327,9 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor {
|
||||
delegate.remove();
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator#nextConverted(org.springframework.data.cassandra.mapping.CassandraPersistentProperty)
|
||||
*/
|
||||
@Override
|
||||
public Object nextConverted(CassandraPersistentProperty property) {
|
||||
return potentiallyConvert(index++, delegate.next(), property);
|
||||
|
||||
@@ -19,7 +19,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBinding;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
@@ -183,4 +182,60 @@ class ExpressionEvaluatingParameterBinder {
|
||||
return queryMethod;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic parameter binding with name or position information.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
static class ParameterBinding {
|
||||
|
||||
private final boolean quoted;
|
||||
private final int parameterIndex;
|
||||
private final String expression;
|
||||
private final String parameterName;
|
||||
|
||||
private ParameterBinding(int parameterIndex, boolean quoted, String expression, String parameterName) {
|
||||
this.parameterIndex = parameterIndex;
|
||||
this.quoted = quoted;
|
||||
this.expression = expression;
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
public static ParameterBinding expression(String expression, boolean quoted) {
|
||||
return new ParameterBinding(-1, quoted, expression, null);
|
||||
}
|
||||
|
||||
public static ParameterBinding indexed(int parameterIndex) {
|
||||
return new ParameterBinding(parameterIndex, false, null, null);
|
||||
}
|
||||
|
||||
public static ParameterBinding named(String name) {
|
||||
return new ParameterBinding(-1, false, null, name);
|
||||
}
|
||||
|
||||
public boolean isNamed() {
|
||||
return (parameterName != null);
|
||||
}
|
||||
|
||||
public int getParameterIndex() {
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
public String getParameter() {
|
||||
return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
|
||||
}
|
||||
|
||||
public String getExpression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
public boolean isExpression() {
|
||||
return (this.expression != null);
|
||||
}
|
||||
|
||||
public String getParameterName() {
|
||||
return parameterName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import org.springframework.data.repository.query.ReactiveWrapperConverters;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoProcessor;
|
||||
|
||||
/**
|
||||
* Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes
|
||||
* to reactive parameter wrapper types upon creation. This class performs synchronization when acessing parameters.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class ReactiveCassandraParameterAccessor extends CassandraParametersParameterAccessor {
|
||||
|
||||
private final Object[] values;
|
||||
private final MonoProcessor<?>[] subscriptions;
|
||||
|
||||
public ReactiveCassandraParameterAccessor(CassandraQueryMethod method, Object[] values) {
|
||||
|
||||
super(method, values);
|
||||
|
||||
this.values = values;
|
||||
this.subscriptions = new MonoProcessor<?>[values.length];
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
|
||||
Object value = values[i];
|
||||
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ReactiveWrapperConverters.supports(value.getClass())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ReactiveWrapperConverters.isSingleLike(value.getClass())) {
|
||||
subscriptions[i] = ReactiveWrapperConverters.toWrapper(value, Mono.class).subscribe();
|
||||
} else {
|
||||
subscriptions[i] = ReactiveWrapperConverters.toWrapper(value, Flux.class).collectList().subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParametersParameterAccessor#getValue(int)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected <T> T getValue(int index) {
|
||||
|
||||
if (subscriptions[index] != null) {
|
||||
return (T) subscriptions[index].block();
|
||||
}
|
||||
|
||||
return super.getValue(index);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.CassandraParametersParameterAccessor#getValues()
|
||||
*/
|
||||
@Override
|
||||
public Object[] getValues() {
|
||||
|
||||
Object[] result = new Object[values.length];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = getValue(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.ParametersParameterAccessor#getBindableValue(int)
|
||||
*/
|
||||
public Object getBindableValue(int index) {
|
||||
return getValue(getParameters().getBindableParameter(index).getIndex());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.cassandra.core.CassandraOperations;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.data.util.StreamUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Reactive query executions for Cassandra.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
interface ReactiveCassandraQueryExecution {
|
||||
|
||||
Object execute(String query, Class<?> type);
|
||||
|
||||
/**
|
||||
* {@link ReactiveCassandraQueryExecution} for collection returning queries.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
final class CollectionExecution implements ReactiveCassandraQueryExecution {
|
||||
|
||||
private final @NonNull ReactiveCassandraOperations operations;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Object execute(String query, Class<?> type) {
|
||||
return operations.select(query, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ReactiveCassandraQueryExecution} to return a single entity.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
final class SingleEntityExecution implements ReactiveCassandraQueryExecution {
|
||||
|
||||
private final @NonNull ReactiveCassandraOperations operations;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Object execute(String query, Class<?> type) {
|
||||
return operations.selectOne(query, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link ReactiveCassandraQueryExecution} that wraps the results of the given delegate with the given result processing.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
final class ResultProcessingExecution implements ReactiveCassandraQueryExecution {
|
||||
|
||||
private final @NonNull ReactiveCassandraQueryExecution delegate;
|
||||
private final @NonNull Converter<Object, Object> converter;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryExecution#execute(java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public Object execute(String query, Class<?> type) {
|
||||
return converter.convert(delegate.execute(query, type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
final class ResultProcessingConverter implements Converter<Object, Object> {
|
||||
|
||||
private final @NonNull ResultProcessor processor;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public Object convert(Object source) {
|
||||
|
||||
ReturnedType returnedType = processor.getReturnedType();
|
||||
|
||||
if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) {
|
||||
return source;
|
||||
}
|
||||
|
||||
return processor.processResult(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.springframework.data.repository.query.ReactiveWrappers.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
|
||||
/**
|
||||
* Reactive specific implementation of {@link CassandraQueryMethod}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ReactiveCassandraQueryMethod extends CassandraQueryMethod {
|
||||
|
||||
private final Method method;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ReactiveCassandraQueryMethod} from the given {@link Method}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param projectionFactory must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
public ReactiveCassandraQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory,
|
||||
CassandraMappingContext mappingContext) {
|
||||
|
||||
super(method, metadata, projectionFactory, mappingContext);
|
||||
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery()
|
||||
*/
|
||||
@Override
|
||||
public boolean isCollectionQuery() {
|
||||
return !(isPageQuery() || isSliceQuery()) && isMultiType(method.getReturnType());
|
||||
}
|
||||
|
||||
/*
|
||||
* All reactive query methods are streaming queries.
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryMethod#isStreamQuery()
|
||||
*/
|
||||
@Override
|
||||
public boolean isStreamQuery() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* Reactive PartTree {@link RepositoryQuery} implementation for Cassandra.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQuery {
|
||||
|
||||
private final CassandraMappingContext mappingContext;
|
||||
|
||||
private final PartTree tree;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ReactivePartTreeCassandraQuery} from the given {@link QueryMethod} and
|
||||
* {@link ReactiveCassandraOperations}.
|
||||
*
|
||||
* @param queryMethod must not be {@literal null}.
|
||||
* @param operations must not be {@literal null}.
|
||||
*/
|
||||
public ReactivePartTreeCassandraQuery(CassandraQueryMethod queryMethod, ReactiveCassandraOperations operations) {
|
||||
|
||||
super(queryMethod, operations);
|
||||
|
||||
this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType());
|
||||
this.mappingContext = operations.getConverter().getMappingContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link PartTree} backing the query.
|
||||
*
|
||||
* @return the tree
|
||||
*/
|
||||
public PartTree getTree() {
|
||||
return tree;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected String createQuery(CassandraParameterAccessor parameterAccessor) {
|
||||
|
||||
CassandraQueryCreator queryCreator = new CassandraQueryCreator(tree, parameterAccessor, mappingContext,
|
||||
getQueryMethod().getEntityInformation());
|
||||
|
||||
return queryCreator.createQuery().toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cassandra.core.ReactiveSessionCallback;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryCreationException;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.CodecRegistry;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* String-based {@link AbstractCassandraQuery} implementation.
|
||||
* <p>
|
||||
* A {@link ReactiveStringBasedCassandraQuery} expects a query method to be annotated with
|
||||
* {@link org.springframework.data.cassandra.repository.Query} with a CQL query. String-based queries support named,
|
||||
* index-based and expression parameters that are resolved during query execution.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see org.springframework.data.cassandra.repository.Query
|
||||
*/
|
||||
public class ReactiveStringBasedCassandraQuery extends AbstractReactiveCassandraQuery {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReactiveStringBasedCassandraQuery.class);
|
||||
|
||||
private final StringBasedQuery stringBasedQuery;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ReactiveStringBasedCassandraQuery} for the given {@link CassandraQueryMethod},
|
||||
* {@link ReactiveCassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}.
|
||||
*
|
||||
* @param queryMethod {@link CassandraQueryMethod} on which this query is based.
|
||||
* @param operations {@link ReactiveCassandraOperations} used to perform data access in Cassandra.
|
||||
* @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query.
|
||||
* @param evaluationContextProvider {@link EvaluationContextProvider} used to access the potentially shared
|
||||
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}.
|
||||
*/
|
||||
public ReactiveStringBasedCassandraQuery(CassandraQueryMethod queryMethod, ReactiveCassandraOperations operations,
|
||||
SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) {
|
||||
this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ReactiveStringBasedCassandraQuery} for the given {@code query}, {@link CassandraQueryMethod},
|
||||
* {@link ReactiveCassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}.
|
||||
*
|
||||
* @param queryMethod {@link CassandraQueryMethod} on which this query is based.
|
||||
* @param operations {@link ReactiveCassandraOperations} used to perform data access in Cassandra.
|
||||
* @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query.
|
||||
* @param evaluationContextProvider {@link EvaluationContextProvider} used to access the potentially shared
|
||||
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}.
|
||||
*/
|
||||
public ReactiveStringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod,
|
||||
ReactiveCassandraOperations operations, SpelExpressionParser expressionParser,
|
||||
EvaluationContextProvider evaluationContextProvider) {
|
||||
|
||||
super(queryMethod, operations);
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
|
||||
// this blocking operation is to retrieve the underlying Cluster and does not include any I/O here.
|
||||
Cluster cluster = operations.getReactiveCqlOperations()
|
||||
.execute((ReactiveSessionCallback<Cluster>) session -> Flux.just(session.getCluster())).blockFirst();
|
||||
|
||||
CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry();
|
||||
|
||||
this.stringBasedQuery = new StringBasedQuery(query,
|
||||
new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider), codecRegistry);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor)
|
||||
*/
|
||||
@Override
|
||||
public String createQuery(CassandraParameterAccessor parameterAccessor) {
|
||||
|
||||
try {
|
||||
String boundQuery = stringBasedQuery.bindQuery(parameterAccessor, getQueryMethod());
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Created query [%s].", boundQuery));
|
||||
}
|
||||
|
||||
return boundQuery;
|
||||
} catch (RuntimeException e) {
|
||||
throw QueryCreationException.create(getQueryMethod(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,28 +15,14 @@
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.cassandra.core.CassandraOperations;
|
||||
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryCreationException;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.datastax.driver.core.CodecRegistry;
|
||||
import com.datastax.driver.core.TypeCodec;
|
||||
import com.datastax.driver.core.querybuilder.BindMarker;
|
||||
|
||||
/**
|
||||
* String-based {@link AbstractCassandraQuery} implementation.
|
||||
@@ -52,15 +38,8 @@ import com.datastax.driver.core.querybuilder.BindMarker;
|
||||
public class StringBasedCassandraQuery extends AbstractCassandraQuery {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StringBasedCassandraQuery.class);
|
||||
private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE;
|
||||
|
||||
private final CodecRegistry codecRegistry;
|
||||
|
||||
private final ExpressionEvaluatingParameterBinder parameterBinder;
|
||||
|
||||
private final List<ParameterBinding> queryParameterBindings;
|
||||
|
||||
private final String query;
|
||||
private final StringBasedQuery stringBasedQuery;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod},
|
||||
@@ -70,7 +49,7 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
|
||||
* @param operations {@link CassandraOperations} used to perform data access in Cassandra.
|
||||
* @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query.
|
||||
* @param evaluationContextProvider {@link EvaluationContextProvider} used to access the potentially shared
|
||||
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}.
|
||||
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}.
|
||||
*/
|
||||
public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations,
|
||||
SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) {
|
||||
@@ -83,21 +62,20 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
|
||||
* {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}.
|
||||
*
|
||||
* @param query
|
||||
* @param queryMethod
|
||||
* @param operations
|
||||
* @param expressionParser
|
||||
* @param evaluationContextProvider
|
||||
* @param queryMethod {@link CassandraQueryMethod} on which this query is based.
|
||||
* @param operations {@link CassandraOperations} used to perform data access in Cassandra.
|
||||
* @param expressionParser {@link SpelExpressionParser} used to parse expressions in the query.
|
||||
* @param evaluationContextProvider {@link EvaluationContextProvider} used to access the potentially shared
|
||||
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}.
|
||||
*/
|
||||
public StringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod, CassandraOperations operations,
|
||||
SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) {
|
||||
|
||||
super(queryMethod, operations);
|
||||
|
||||
this.queryParameterBindings = new ArrayList<ParameterBinding>();
|
||||
this.query = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
this.queryParameterBindings);
|
||||
this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider);
|
||||
this.codecRegistry = operations.getSession().getCluster().getConfiguration().getCodecRegistry();
|
||||
CodecRegistry codecRegistry = operations.getSession().getCluster().getConfiguration().getCodecRegistry();
|
||||
this.stringBasedQuery = new StringBasedQuery(query,
|
||||
new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider), codecRegistry);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -107,10 +85,7 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
|
||||
public String createQuery(CassandraParameterAccessor parameterAccessor) {
|
||||
|
||||
try {
|
||||
List<Object> arguments = this.parameterBinder.bind(parameterAccessor,
|
||||
new BindingContext(getQueryMethod(), queryParameterBindings));
|
||||
|
||||
String boundQuery = bind(query, arguments);
|
||||
String boundQuery = stringBasedQuery.bindQuery(parameterAccessor, getQueryMethod());
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Created query [%s].", boundQuery));
|
||||
@@ -121,356 +96,4 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery {
|
||||
throw QueryCreationException.create(getQueryMethod(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String bind(String query, List<Object> arguments) {
|
||||
return ParameterBinder.INSTANCE.bind(query, codecRegistry, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that extracts the parameter bindings from a given query string.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
enum ParameterBinder {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
|
||||
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
|
||||
|
||||
public String bind(String input, CodecRegistry codecRegistry, List<Object> parameters) {
|
||||
|
||||
if (parameters.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
int startIndex = 0;
|
||||
int currentPosition = 0;
|
||||
int parameterIndex = 0;
|
||||
|
||||
Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input);
|
||||
|
||||
while (currentPosition < input.length()) {
|
||||
|
||||
if (!matcher.find()) {
|
||||
break;
|
||||
}
|
||||
|
||||
int exprStart = matcher.start();
|
||||
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
result = appendValue(parameters.get(parameterIndex++), codecRegistry, result);
|
||||
|
||||
currentPosition = matcher.end();
|
||||
startIndex = currentPosition;
|
||||
}
|
||||
|
||||
return result.append(input.subSequence(currentPosition, input.length())).toString();
|
||||
}
|
||||
|
||||
static StringBuilder appendValue(Object value, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
if (value == null) {
|
||||
builder.append("null");
|
||||
} else if (value instanceof BindMarker) {
|
||||
builder.append(value);
|
||||
} else if (value instanceof List && isSerializable(value)) {
|
||||
// bind variables are not supported inside collection literals
|
||||
appendList((List<?>) value, codecRegistry, builder);
|
||||
} else if (value instanceof Set && isSerializable(value)) {
|
||||
// bind variables are not supported inside collection literals
|
||||
appendSet((Set<?>) value, codecRegistry, builder);
|
||||
} else if (value instanceof Map && isSerializable(value)) {
|
||||
// bind variables are not supported inside collection literals
|
||||
appendMap((Map<?, ?>) value, codecRegistry, builder);
|
||||
} else if (isSerializable(value)) {
|
||||
TypeCodec<Object> codec = codecRegistry.codecFor(value);
|
||||
builder.append(codec.format(value));
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("Argument value [%s] is not serializable", value.toString()));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder appendList(List<?> list, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
for (int index = 0, size = list.size(); index < size; index++) {
|
||||
builder.append(index > 0 ? "," : "");
|
||||
appendValue(list.get(index), codecRegistry, builder);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder appendSet(Set<?> set, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
boolean first = true;
|
||||
|
||||
for (Object element : set) {
|
||||
builder.append(first ? "" : ",");
|
||||
appendValue(element, codecRegistry, builder);
|
||||
first = false;
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder appendMap(Map<?, ?> map, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
builder.append('{');
|
||||
|
||||
boolean first = true;
|
||||
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
builder.append(first ? "" : ",");
|
||||
appendValue(entry.getKey(), codecRegistry, builder);
|
||||
builder.append(':');
|
||||
appendValue(entry.getValue(), codecRegistry, builder);
|
||||
first = false;
|
||||
}
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given value is likely to find a suitable codec to be serialized as a query parameter. If the
|
||||
* value is not serializable, it must be included in the query string. Non serializable values include special
|
||||
* values such as function calls, column names and bind markers, and collections thereof. We also don't serialize
|
||||
* fixed size number types. The reason is that if we do it, we will force a particular size (4 bytes for ints, ...)
|
||||
* and for the query builder, we don't want users to have to bother with that.
|
||||
*
|
||||
* @param value the value to inspect.
|
||||
* @return true if the value is serializable, false otherwise.
|
||||
*/
|
||||
static boolean isSerializable(Object value) {
|
||||
|
||||
if (containsSpecialValue(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value instanceof Collection) {
|
||||
for (Object element : (Collection) value) {
|
||||
if (!isSerializable(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof Map) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
||||
if (!isSerializable(entry.getKey()) || !isSerializable(entry.getValue())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean containsSpecialValue(Object value) {
|
||||
|
||||
if (value instanceof BindMarker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value instanceof Collection) {
|
||||
for (Object element : (Collection) value) {
|
||||
if (containsSpecialValue(element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof Map) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
||||
if (containsSpecialValue(entry.getKey()) || containsSpecialValue(entry.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that extracts the parameter bindings from a given query string.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
enum ParameterBindingParser {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final char CURRLY_BRACE_OPEN = '{';
|
||||
private static final char CURRLY_BRACE_CLOSE = '}';
|
||||
private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
|
||||
private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)");
|
||||
|
||||
private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{");
|
||||
private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{");
|
||||
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
|
||||
|
||||
/**
|
||||
* Returns a list of {@link ParameterBinding}s found in the given {@code input}.
|
||||
*
|
||||
* @param input can be {@literal null} or empty.
|
||||
* @param bindings must not be {@literal null}.
|
||||
* @return a list of {@link ParameterBinding}s found in the given {@code input}.
|
||||
*/
|
||||
public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input,
|
||||
List<ParameterBinding> bindings) {
|
||||
|
||||
if (!StringUtils.hasText(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
Assert.notNull(bindings, "Parameter bindings must not be null");
|
||||
|
||||
return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
|
||||
}
|
||||
|
||||
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
|
||||
List<ParameterBinding> bindings) {
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
int startIndex = 0;
|
||||
int currentPosition = 0;
|
||||
|
||||
while (currentPosition < input.length()) {
|
||||
|
||||
Matcher matcher = findNextBindingOrExpression(input, currentPosition);
|
||||
|
||||
// no expression parameter found
|
||||
if (matcher == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
int exprStart = matcher.start();
|
||||
currentPosition = exprStart;
|
||||
|
||||
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
|
||||
// eat parameter expression
|
||||
int curlyBraceOpenCount = 1;
|
||||
currentPosition += 3;
|
||||
|
||||
while (curlyBraceOpenCount > 0 && currentPosition < input.length()) {
|
||||
switch (input.charAt(currentPosition++)) {
|
||||
case CURRLY_BRACE_OPEN:
|
||||
curlyBraceOpenCount++;
|
||||
break;
|
||||
case CURRLY_BRACE_CLOSE:
|
||||
curlyBraceOpenCount--;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
} else {
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
}
|
||||
|
||||
result.append(ARGUMENT_PLACEHOLDER);
|
||||
|
||||
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
|
||||
bindings.add(ParameterBinding.expression(input.substring(exprStart + 3, currentPosition - 1), true));
|
||||
} else {
|
||||
if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
|
||||
bindings.add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
|
||||
} else {
|
||||
bindings.add(ParameterBinding.named(matcher.group(1)));
|
||||
}
|
||||
|
||||
currentPosition = matcher.end();
|
||||
}
|
||||
|
||||
startIndex = currentPosition;
|
||||
}
|
||||
|
||||
return result.append(input.subSequence(currentPosition, input.length())).toString();
|
||||
}
|
||||
|
||||
private static Matcher findNextBindingOrExpression(String input, int position) {
|
||||
|
||||
List<Matcher> matchers = new ArrayList<Matcher>();
|
||||
|
||||
matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input));
|
||||
matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input));
|
||||
matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input));
|
||||
matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input));
|
||||
|
||||
TreeMap<Integer, Matcher> matcherMap = new TreeMap<Integer, Matcher>();
|
||||
|
||||
for (Matcher matcher : matchers) {
|
||||
if (matcher.find(position)) {
|
||||
matcherMap.put(matcher.start(), matcher);
|
||||
}
|
||||
}
|
||||
|
||||
return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic parameter binding with name or position information.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
static class ParameterBinding {
|
||||
|
||||
private final boolean quoted;
|
||||
private final int parameterIndex;
|
||||
private final String expression;
|
||||
private final String parameterName;
|
||||
|
||||
private ParameterBinding(int parameterIndex, boolean quoted, String expression, String parameterName) {
|
||||
this.parameterIndex = parameterIndex;
|
||||
this.quoted = quoted;
|
||||
this.expression = expression;
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
public static ParameterBinding expression(String expression, boolean quoted) {
|
||||
return new ParameterBinding(-1, quoted, expression, null);
|
||||
}
|
||||
|
||||
public static ParameterBinding indexed(int parameterIndex) {
|
||||
return new ParameterBinding(parameterIndex, false, null, null);
|
||||
}
|
||||
|
||||
public static ParameterBinding named(String name) {
|
||||
return new ParameterBinding(-1, false, null, name);
|
||||
}
|
||||
|
||||
public boolean isNamed() {
|
||||
return (parameterName != null);
|
||||
}
|
||||
|
||||
public int getParameterIndex() {
|
||||
return parameterIndex;
|
||||
}
|
||||
|
||||
public String getParameter() {
|
||||
return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
|
||||
}
|
||||
|
||||
public String getExpression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
public boolean isExpression() {
|
||||
return (this.expression != null);
|
||||
}
|
||||
|
||||
public String getParameterName() {
|
||||
return parameterName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
|
||||
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.datastax.driver.core.CodecRegistry;
|
||||
import com.datastax.driver.core.TypeCodec;
|
||||
import com.datastax.driver.core.querybuilder.BindMarker;
|
||||
|
||||
/**
|
||||
* String-based Query abstracting a CQL query with parameter bindings.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
class StringBasedQuery {
|
||||
|
||||
private final CodecRegistry codecRegistry;
|
||||
private final ExpressionEvaluatingParameterBinder parameterBinder;
|
||||
private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();
|
||||
private final String query;
|
||||
|
||||
/**
|
||||
* Creates a new {@link StringBasedQuery} given {@code query}, {@link ExpressionEvaluatingParameterBinder} and
|
||||
* {@link CodecRegistry}.
|
||||
*
|
||||
* @param query must not be empty.
|
||||
* @param parameterBinder must not be {@literal null}.
|
||||
* @param codecRegistry must not be {@literal null}.
|
||||
*/
|
||||
public StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder,
|
||||
CodecRegistry codecRegistry) {
|
||||
|
||||
Assert.hasText(query, "Query must not be empty");
|
||||
Assert.notNull(parameterBinder, "ExpressionEvaluatingParameterBinder must not be null");
|
||||
Assert.notNull(codecRegistry, "CodecRegistry must not be null");
|
||||
|
||||
this.codecRegistry = codecRegistry;
|
||||
this.parameterBinder = parameterBinder;
|
||||
|
||||
this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
this.queryParameterBindings);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the query to actual parameters using {@link CassandraParameterAccessor},
|
||||
*
|
||||
* @param parameterAccessor must not be {@literal null}.
|
||||
* @param queryMethod must not be {@literal null}.
|
||||
* @return the bound String query containing formatted parameters.
|
||||
*/
|
||||
public String bindQuery(CassandraParameterAccessor parameterAccessor, CassandraQueryMethod queryMethod) {
|
||||
|
||||
Assert.notNull(parameterAccessor, "CassandraParameterAccessor must not be null");
|
||||
Assert.notNull(queryMethod, "CassandraQueryMethod must not be null");
|
||||
|
||||
List<Object> arguments = parameterBinder.bind(parameterAccessor,
|
||||
new BindingContext(queryMethod, queryParameterBindings));
|
||||
|
||||
return ParameterBinder.INSTANCE.bind(query, codecRegistry, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that extracts the parameter bindings from a given query string.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
enum ParameterBinder {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
|
||||
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
|
||||
|
||||
public String bind(String input, CodecRegistry codecRegistry, List<Object> parameters) {
|
||||
|
||||
if (parameters.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
int startIndex = 0;
|
||||
int currentPosition = 0;
|
||||
int parameterIndex = 0;
|
||||
|
||||
Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input);
|
||||
|
||||
while (currentPosition < input.length()) {
|
||||
|
||||
if (!matcher.find()) {
|
||||
break;
|
||||
}
|
||||
|
||||
int exprStart = matcher.start();
|
||||
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
result = appendValue(parameters.get(parameterIndex++), codecRegistry, result);
|
||||
|
||||
currentPosition = matcher.end();
|
||||
startIndex = currentPosition;
|
||||
}
|
||||
|
||||
return result.append(input.subSequence(currentPosition, input.length())).toString();
|
||||
}
|
||||
|
||||
static StringBuilder appendValue(Object value, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
if (value == null) {
|
||||
builder.append("null");
|
||||
} else if (value instanceof BindMarker) {
|
||||
builder.append(value);
|
||||
} else if (value instanceof List && isSerializable(value)) {
|
||||
// bind variables are not supported inside collection literals
|
||||
appendList((List<?>) value, codecRegistry, builder);
|
||||
} else if (value instanceof Set && isSerializable(value)) {
|
||||
// bind variables are not supported inside collection literals
|
||||
appendSet((Set<?>) value, codecRegistry, builder);
|
||||
} else if (value instanceof Map && isSerializable(value)) {
|
||||
// bind variables are not supported inside collection literals
|
||||
appendMap((Map<?, ?>) value, codecRegistry, builder);
|
||||
} else if (isSerializable(value)) {
|
||||
TypeCodec<Object> codec = codecRegistry.codecFor(value);
|
||||
builder.append(codec.format(value));
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("Argument value [%s] is not serializable", value.toString()));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder appendList(List<?> list, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
for (int index = 0, size = list.size(); index < size; index++) {
|
||||
builder.append(index > 0 ? "," : "");
|
||||
appendValue(list.get(index), codecRegistry, builder);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder appendSet(Set<?> set, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
boolean first = true;
|
||||
|
||||
for (Object element : set) {
|
||||
builder.append(first ? "" : ",");
|
||||
appendValue(element, codecRegistry, builder);
|
||||
first = false;
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder appendMap(Map<?, ?> map, CodecRegistry codecRegistry, StringBuilder builder) {
|
||||
|
||||
builder.append('{');
|
||||
|
||||
boolean first = true;
|
||||
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
builder.append(first ? "" : ",");
|
||||
appendValue(entry.getKey(), codecRegistry, builder);
|
||||
builder.append(':');
|
||||
appendValue(entry.getValue(), codecRegistry, builder);
|
||||
first = false;
|
||||
}
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given value is likely to find a suitable codec to be serialized as a query parameter. If the
|
||||
* value is not serializable, it must be included in the query string. Non serializable values include special
|
||||
* values such as function calls, column names and bind markers, and collections thereof. We also don't serialize
|
||||
* fixed size number types. The reason is that if we do it, we will force a particular size (4 bytes for ints, ...)
|
||||
* and for the query builder, we don't want users to have to bother with that.
|
||||
*
|
||||
* @param value the value to inspect.
|
||||
* @return true if the value is serializable, false otherwise.
|
||||
*/
|
||||
static boolean isSerializable(Object value) {
|
||||
|
||||
if (containsSpecialValue(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value instanceof Collection) {
|
||||
for (Object element : (Collection) value) {
|
||||
if (!isSerializable(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof Map) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
||||
if (!isSerializable(entry.getKey()) || !isSerializable(entry.getValue())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean containsSpecialValue(Object value) {
|
||||
|
||||
if (value instanceof BindMarker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value instanceof Collection) {
|
||||
for (Object element : (Collection) value) {
|
||||
if (containsSpecialValue(element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof Map) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
||||
if (containsSpecialValue(entry.getKey()) || containsSpecialValue(entry.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that extracts the parameter bindings from a given query string.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
enum ParameterBindingParser {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private static final char CURRLY_BRACE_OPEN = '{';
|
||||
private static final char CURRLY_BRACE_CLOSE = '}';
|
||||
private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
|
||||
private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)");
|
||||
|
||||
private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{");
|
||||
private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{");
|
||||
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
|
||||
|
||||
/**
|
||||
* Returns a list of {@link ParameterBinding}s found in the given {@code input}.
|
||||
*
|
||||
* @param input can be {@literal null} or empty.
|
||||
* @param bindings must not be {@literal null}.
|
||||
* @return a list of {@link ParameterBinding}s found in the given {@code input}.
|
||||
*/
|
||||
public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List<ParameterBinding> bindings) {
|
||||
|
||||
if (!StringUtils.hasText(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
Assert.notNull(bindings, "Parameter bindings must not be null");
|
||||
|
||||
return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
|
||||
}
|
||||
|
||||
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
|
||||
List<ParameterBinding> bindings) {
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
int startIndex = 0;
|
||||
int currentPosition = 0;
|
||||
|
||||
while (currentPosition < input.length()) {
|
||||
|
||||
Matcher matcher = findNextBindingOrExpression(input, currentPosition);
|
||||
|
||||
// no expression parameter found
|
||||
if (matcher == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
int exprStart = matcher.start();
|
||||
currentPosition = exprStart;
|
||||
|
||||
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
|
||||
// eat parameter expression
|
||||
int curlyBraceOpenCount = 1;
|
||||
currentPosition += 3;
|
||||
|
||||
while (curlyBraceOpenCount > 0 && currentPosition < input.length()) {
|
||||
switch (input.charAt(currentPosition++)) {
|
||||
case CURRLY_BRACE_OPEN:
|
||||
curlyBraceOpenCount++;
|
||||
break;
|
||||
case CURRLY_BRACE_CLOSE:
|
||||
curlyBraceOpenCount--;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
} else {
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
}
|
||||
|
||||
result.append(ARGUMENT_PLACEHOLDER);
|
||||
|
||||
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
|
||||
bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding
|
||||
.expression(input.substring(exprStart + 3, currentPosition - 1), true));
|
||||
} else {
|
||||
if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
|
||||
bindings
|
||||
.add(ExpressionEvaluatingParameterBinder.ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
|
||||
} else {
|
||||
bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding.named(matcher.group(1)));
|
||||
}
|
||||
|
||||
currentPosition = matcher.end();
|
||||
}
|
||||
|
||||
startIndex = currentPosition;
|
||||
}
|
||||
|
||||
return result.append(input.subSequence(currentPosition, input.length())).toString();
|
||||
}
|
||||
|
||||
private static Matcher findNextBindingOrExpression(String input, int position) {
|
||||
|
||||
List<Matcher> matchers = new ArrayList<Matcher>();
|
||||
|
||||
matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input));
|
||||
matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input));
|
||||
matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input));
|
||||
matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input));
|
||||
|
||||
TreeMap<Integer, Matcher> matcherMap = new TreeMap<Integer, Matcher>();
|
||||
|
||||
for (Matcher matcher : matchers) {
|
||||
if (matcher.find(position)) {
|
||||
matcherMap.put(matcher.start(), matcher);
|
||||
}
|
||||
}
|
||||
|
||||
return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.support;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraPersistentEntity;
|
||||
import org.springframework.data.cassandra.repository.query.CassandraEntityInformation;
|
||||
import org.springframework.data.cassandra.repository.query.CassandraQueryMethod;
|
||||
import org.springframework.data.cassandra.repository.query.ReactiveCassandraQueryMethod;
|
||||
import org.springframework.data.cassandra.repository.query.ReactivePartTreeCassandraQuery;
|
||||
import org.springframework.data.cassandra.repository.query.ReactiveStringBasedCassandraQuery;
|
||||
import org.springframework.data.mapping.model.MappingException;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.util.QueryExecutionConverters;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Factory to create {@link org.springframework.data.cassandra.repository.ReactiveCassandraRepository} instances.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ReactiveCassandraRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
|
||||
|
||||
private final ReactiveCassandraOperations operations;
|
||||
private final CassandraMappingContext mappingContext;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ReactiveCassandraRepositoryFactory} with the given {@link ReactiveCassandraOperations}.
|
||||
*
|
||||
* @param cassandraOperations must not be {@literal null}.
|
||||
*/
|
||||
public ReactiveCassandraRepositoryFactory(ReactiveCassandraOperations cassandraOperations) {
|
||||
|
||||
Assert.notNull(cassandraOperations);
|
||||
|
||||
this.operations = cassandraOperations;
|
||||
this.mappingContext = cassandraOperations.getConverter().getMappingContext();
|
||||
|
||||
DefaultConversionService conversionService = new DefaultConversionService();
|
||||
QueryExecutionConverters.registerConvertersIn(conversionService);
|
||||
|
||||
this.conversionService = conversionService;
|
||||
setConversionService(conversionService);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
|
||||
*/
|
||||
@Override
|
||||
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
|
||||
return SimpleReactiveCassandraRepository.class;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation)
|
||||
*/
|
||||
@Override
|
||||
protected Object getTargetRepository(RepositoryInformation information) {
|
||||
|
||||
CassandraEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType(),
|
||||
information);
|
||||
return getTargetRepositoryViaReflection(information, entityInformation, operations);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
|
||||
*/
|
||||
@Override
|
||||
protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) {
|
||||
return new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext, conversionService);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class)
|
||||
*/
|
||||
public <T, ID extends Serializable> CassandraEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
|
||||
return getEntityInformation(domainClass, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T, ID extends Serializable> CassandraEntityInformation<T, ID> getEntityInformation(Class<T> domainClass,
|
||||
RepositoryInformation information) {
|
||||
|
||||
CassandraPersistentEntity<?> entity = mappingContext.getPersistentEntity(domainClass);
|
||||
|
||||
if (entity == null) {
|
||||
throw new MappingException(
|
||||
String.format("Could not lookup mapping metadata for domain class %s!", domainClass.getName()));
|
||||
}
|
||||
|
||||
return new MappingCassandraEntityInformation<T, ID>((CassandraPersistentEntity<T>) entity,
|
||||
operations.getConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link QueryLookupStrategy} to create
|
||||
* {@link org.springframework.data.cassandra.repository.query.PartTreeCassandraQuery} instances.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
private static class CassandraQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private final EvaluationContextProvider evaluationContextProvider;
|
||||
private final ReactiveCassandraOperations operations;
|
||||
private final CassandraMappingContext mappingContext;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
CassandraQueryLookupStrategy(ReactiveCassandraOperations operations, EvaluationContextProvider evaluationContextProvider, CassandraMappingContext mappingContext,
|
||||
ConversionService conversionService) {
|
||||
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.operations = operations;
|
||||
this.mappingContext = mappingContext;
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
|
||||
*/
|
||||
@Override
|
||||
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
|
||||
NamedQueries namedQueries) {
|
||||
|
||||
CassandraQueryMethod queryMethod = new ReactiveCassandraQueryMethod(method, metadata, factory, mappingContext);
|
||||
String namedQueryName = queryMethod.getNamedQueryName();
|
||||
|
||||
if (namedQueries.hasQuery(namedQueryName)) {
|
||||
String namedQuery = namedQueries.getQuery(namedQueryName);
|
||||
return new ReactiveStringBasedCassandraQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER,
|
||||
evaluationContextProvider);
|
||||
} else if (queryMethod.hasAnnotatedQuery()) {
|
||||
return new ReactiveStringBasedCassandraQuery(queryMethod, operations, EXPRESSION_PARSER,
|
||||
evaluationContextProvider);
|
||||
} else {
|
||||
return new ReactivePartTreeCassandraQuery(queryMethod, operations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.cassandra.repository.support;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.cassandra.core.CassandraTemplate;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.beans.factory.FactoryBean} to create
|
||||
* {@link org.springframework.data.cassandra.repository.ReactiveCassandraRepository} instances.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
* @see org.springframework.data.repository.reactive.ReactivePagingAndSortingRepository
|
||||
* @see org.springframework.data.repository.reactive.RxJavaPagingAndSortingRepository
|
||||
*/
|
||||
public class ReactiveCassandraRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
|
||||
extends RepositoryFactoryBeanSupport<T, S, ID> {
|
||||
|
||||
private ReactiveCassandraOperations operations;
|
||||
private boolean mappingContextConfigured = false;
|
||||
|
||||
/**
|
||||
* Configures the {@link ReactiveCassandraOperations} used for Cassandra data access operations.
|
||||
*
|
||||
* @param operations {@link ReactiveCassandraOperations} used to perform CRUD, Query and general data access operations
|
||||
* on Apache Cassandra.
|
||||
*/
|
||||
public void setReactiveCassandraOperations(ReactiveCassandraOperations operations) {
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext)
|
||||
*/
|
||||
@Override
|
||||
protected void setMappingContext(MappingContext<?, ?> mappingContext) {
|
||||
|
||||
super.setMappingContext(mappingContext);
|
||||
this.mappingContextConfigured = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.data.repository.support.RepositoryFactoryBeanSupport
|
||||
* #createRepositoryFactory()
|
||||
*/
|
||||
@Override
|
||||
protected final RepositoryFactorySupport createRepositoryFactory() {
|
||||
return getFactoryInstance(operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a {@link RepositoryFactorySupport} instance.
|
||||
*
|
||||
* @param operations
|
||||
* @return
|
||||
*/
|
||||
protected RepositoryFactorySupport getFactoryInstance(ReactiveCassandraOperations operations) {
|
||||
return new ReactiveCassandraRepositoryFactory(operations);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.support.RepositoryFactoryBeanSupport#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
|
||||
super.afterPropertiesSet();
|
||||
Assert.notNull(operations, "ReactiveCassandraOperations must not be null!");
|
||||
|
||||
if (!mappingContextConfigured) {
|
||||
setMappingContext(operations.getConverter().getMappingContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.support;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
|
||||
import org.springframework.data.cassandra.repository.query.CassandraEntityInformation;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.datastax.driver.core.querybuilder.QueryBuilder;
|
||||
import com.datastax.driver.core.querybuilder.Select;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Reactive repository base implementation for Cassandra.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class SimpleReactiveCassandraRepository<T, ID extends Serializable>
|
||||
implements ReactiveCassandraRepository<T, ID> {
|
||||
|
||||
protected ReactiveCassandraOperations operations;
|
||||
protected CassandraEntityInformation<T, ID> entityInformation;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SimpleReactiveCassandraRepository} for the given {@link CassandraEntityInformation} and
|
||||
* {@link ReactiveCassandraOperations}.
|
||||
*
|
||||
* @param metadata must not be {@literal null}.
|
||||
* @param operations must not be {@literal null}.
|
||||
*/
|
||||
public SimpleReactiveCassandraRepository(CassandraEntityInformation<T, ID> metadata,
|
||||
ReactiveCassandraOperations operations) {
|
||||
|
||||
Assert.notNull(metadata, "CassandraEntityInformation must not be null");
|
||||
Assert.notNull(operations, "ReactiveCassandraOperations must not be null");
|
||||
|
||||
this.entityInformation = metadata;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T> Mono<S> save(S entity) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
if (entityInformation.isNew(entity)) {
|
||||
return operations.insert(entity);
|
||||
}
|
||||
|
||||
return operations.update(entity);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T> Flux<S> save(Iterable<S> entities) {
|
||||
|
||||
Assert.notNull(entities, "The given Iterable of entities must not be null");
|
||||
|
||||
return save(Flux.fromIterable(entities));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T> Flux<S> save(Publisher<S> entityStream) {
|
||||
|
||||
Assert.notNull(entityStream, "The given Publisher of entities must not be null");
|
||||
|
||||
return Flux.from(entityStream).flatMap(entity -> {
|
||||
|
||||
if (entityInformation.isNew(entity)) {
|
||||
return operations.insert(entity);
|
||||
}
|
||||
|
||||
return operations.update(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T> Mono<S> insert(S entity) {
|
||||
|
||||
Assert.notNull(entity, "Entity must not be null");
|
||||
|
||||
return operations.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T> Flux<S> insert(Iterable<S> entities) {
|
||||
|
||||
Assert.notNull(entities, "The given Iterable of entities must not be null");
|
||||
|
||||
return operations.insert(Flux.fromIterable(entities));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T> Flux<S> insert(Publisher<S> entityStream) {
|
||||
|
||||
Assert.notNull(entityStream, "The given Publisher of entities must not be null");
|
||||
|
||||
return operations.insert(entityStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<T> findOne(ID id) {
|
||||
|
||||
Assert.notNull(id, "The given id must not be null");
|
||||
|
||||
return operations.selectOneById(id, entityInformation.getJavaType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<T> findOne(Mono<ID> mono) {
|
||||
|
||||
Assert.notNull(mono, "The given id must not be null");
|
||||
|
||||
return mono.then(id -> operations.selectOneById(id, entityInformation.getJavaType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> exists(ID id) {
|
||||
|
||||
Assert.notNull(id, "The given id must not be null");
|
||||
|
||||
return operations.exists(id, entityInformation.getJavaType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> exists(Mono<ID> mono) {
|
||||
|
||||
Assert.notNull(mono, "The given id must not be null");
|
||||
|
||||
return mono.then(id -> operations.exists(id, entityInformation.getJavaType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<T> findAll() {
|
||||
|
||||
Select select = QueryBuilder.select().from(entityInformation.getTableName().toCql());
|
||||
return operations.select(select, entityInformation.getJavaType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<T> findAll(Iterable<ID> iterable) {
|
||||
|
||||
Assert.notNull(iterable, "The given Iterable of id's must not be null");
|
||||
|
||||
return findAll(Flux.fromIterable(iterable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<T> findAll(Publisher<ID> idStream) {
|
||||
|
||||
Assert.notNull(idStream, "The given Publisher of id's must not be null");
|
||||
|
||||
return Flux.from(idStream).flatMap(id -> operations.selectOneById(id, entityInformation.getJavaType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count() {
|
||||
return operations.count(entityInformation.getJavaType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(ID id) {
|
||||
|
||||
Assert.notNull(id, "The given id must not be null");
|
||||
|
||||
return operations.deleteById(id, entityInformation.getJavaType()).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(T entity) {
|
||||
|
||||
Assert.notNull(entity, "The given entity must not be null");
|
||||
|
||||
return operations.delete(entity).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(Iterable<? extends T> entities) {
|
||||
|
||||
Assert.notNull(entities, "The given Iterable of entities must not be null");
|
||||
|
||||
return operations.delete(Flux.fromIterable(entities)).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(Publisher<? extends T> entityStream) {
|
||||
|
||||
Assert.notNull(entityStream, "The given Publisher of entities must not be null");
|
||||
|
||||
return operations.delete(entityStream).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteAll() {
|
||||
return operations.truncate(entityInformation.getJavaType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.cassandra.core.DefaultBridgedReactiveSession;
|
||||
import org.springframework.cassandra.core.ReactiveCqlTemplate;
|
||||
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
|
||||
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.test.integration.support.SchemaTestUtils;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveCassandraTemplate}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class ReactiveCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest {
|
||||
|
||||
private ReactiveCassandraTemplate template;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
MappingCassandraConverter converter = new MappingCassandraConverter();
|
||||
CassandraTemplate cassandraTemplate = new CassandraTemplate(session, converter);
|
||||
|
||||
DefaultBridgedReactiveSession session = new DefaultBridgedReactiveSession(this.session, Schedulers.elastic());
|
||||
template = new ReactiveCassandraTemplate(new ReactiveCqlTemplate(session), converter);
|
||||
|
||||
SchemaTestUtils.potentiallyCreateTableFor(Person.class, cassandraTemplate);
|
||||
SchemaTestUtils.truncate(Person.class, cassandraTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertShouldInsertEntity() {
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
|
||||
Mono<Person> insert = template.insert(person);
|
||||
|
||||
Mono<Person> oneById = template.selectOneById(person.getId(), Person.class);
|
||||
assertThat(oneById.hasElement().block()).isFalse();
|
||||
|
||||
Person saved = insert.block();
|
||||
assertThat(saved).isNotNull().isEqualTo(person);
|
||||
assertThat(oneById.block()).isNotNull().isEqualTo(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldInsertAndCountEntities() {
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
|
||||
template.insert(person).block();
|
||||
|
||||
Mono<Long> count = template.count(Person.class);
|
||||
assertThat(count.block()).isEqualTo(1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void updateShouldUpdateEntity() {
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
template.insert(person).block();
|
||||
|
||||
person.setFirstname("Walter Hartwell");
|
||||
Person updated = template.update(person).block();
|
||||
assertThat(updated).isNotNull();
|
||||
|
||||
Mono<Person> oneById = template.selectOneById(person.getId(), Person.class);
|
||||
assertThat(oneById.block()).isEqualTo(person);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteShouldRemoveEntity() {
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
template.insert(person).block();
|
||||
|
||||
Person deleted = template.delete(person).block();
|
||||
assertThat(deleted).isNotNull();
|
||||
|
||||
Mono<Person> oneById = template.selectOneById(person.getId(), Person.class);
|
||||
assertThat(oneById.block()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteByIdShouldRemoveEntity() {
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
template.insert(person).block();
|
||||
|
||||
Boolean deleted = template.deleteById(person.getId(), Person.class).block();
|
||||
assertThat(deleted).isTrue();
|
||||
|
||||
Mono<Person> oneById = template.selectOneById(person.getId(), Person.class);
|
||||
assertThat(oneById.block()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.anyInt;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.cassandra.core.ReactiveResultSet;
|
||||
import org.springframework.cassandra.core.ReactiveSession;
|
||||
import org.springframework.cassandra.support.exception.CassandraConnectionFailureException;
|
||||
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
|
||||
import com.datastax.driver.core.ColumnDefinitions;
|
||||
import com.datastax.driver.core.DataType;
|
||||
import com.datastax.driver.core.Row;
|
||||
import com.datastax.driver.core.Statement;
|
||||
import com.datastax.driver.core.exceptions.NoHostAvailableException;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveCassandraTemplate}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ReactiveCassandraTemplateUnitTests {
|
||||
|
||||
@Mock ReactiveSession session;
|
||||
@Mock ReactiveResultSet reactiveResultSet;
|
||||
@Mock Row row;
|
||||
@Mock ColumnDefinitions columnDefinitions;
|
||||
@Captor ArgumentCaptor<Statement> statementCaptor;
|
||||
|
||||
private ReactiveCassandraTemplate template;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
template = new ReactiveCassandraTemplate(session);
|
||||
when(session.execute(anyString())).thenReturn(Mono.just(reactiveResultSet));
|
||||
when(session.execute(any(Statement.class))).thenReturn(Mono.just(reactiveResultSet));
|
||||
when(reactiveResultSet.getColumnDefinitions()).thenReturn(columnDefinitions);
|
||||
when(row.getColumnDefinitions()).thenReturn(columnDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void selectUsingCqlShouldReturnMappedResults() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
when(columnDefinitions.contains(anyString())).thenReturn(true);
|
||||
when(columnDefinitions.getType(anyInt())).thenReturn(DataType.ascii());
|
||||
|
||||
when(columnDefinitions.getIndexOf("id")).thenReturn(0);
|
||||
when(columnDefinitions.getIndexOf("firstname")).thenReturn(1);
|
||||
when(columnDefinitions.getIndexOf("lastname")).thenReturn(2);
|
||||
|
||||
when(row.getObject(0)).thenReturn("myid");
|
||||
when(row.getObject(1)).thenReturn("Walter");
|
||||
when(row.getObject(2)).thenReturn("White");
|
||||
|
||||
Flux<Person> flux = template.select("SELECT * FROM person", Person.class);
|
||||
|
||||
assertThat(flux.collectList().block()).hasSize(1).contains(new Person("myid", "Walter", "White"));
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM person");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void selectShouldTranslateException() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenThrow(new NoHostAvailableException(Collections.emptyMap()));
|
||||
|
||||
Flux<Person> flux = template.select("SELECT * FROM person", Person.class);
|
||||
|
||||
try {
|
||||
flux.last().block();
|
||||
|
||||
fail("Missing CassandraConnectionFailureException");
|
||||
} catch (CassandraConnectionFailureException e) {
|
||||
assertThat(e).hasRootCauseInstanceOf(NoHostAvailableException.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void selectOneByIdShouldReturnMappedResults() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
when(columnDefinitions.contains(anyString())).thenReturn(true);
|
||||
when(columnDefinitions.getType(anyInt())).thenReturn(DataType.ascii());
|
||||
|
||||
when(columnDefinitions.getIndexOf("id")).thenReturn(0);
|
||||
when(columnDefinitions.getIndexOf("firstname")).thenReturn(1);
|
||||
when(columnDefinitions.getIndexOf("lastname")).thenReturn(2);
|
||||
|
||||
when(row.getObject(0)).thenReturn("myid");
|
||||
when(row.getObject(1)).thenReturn("Walter");
|
||||
when(row.getObject(2)).thenReturn("White");
|
||||
|
||||
Mono<Person> mono = template.selectOneById("myid", Person.class);
|
||||
|
||||
assertThat(mono.block()).isEqualTo(new Person("myid", "Walter", "White"));
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM person WHERE id='myid';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsShouldReturnExistingElement() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
when(columnDefinitions.contains(anyString())).thenReturn(true);
|
||||
when(columnDefinitions.getType(anyInt())).thenReturn(DataType.ascii());
|
||||
|
||||
Mono<Boolean> mono = template.exists("myid", Person.class);
|
||||
|
||||
assertThat(mono.block()).isTrue();
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM person WHERE id='myid';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsShouldReturnNonExistingElement() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.empty());
|
||||
|
||||
Mono<Boolean> mono = template.exists("myid", Person.class);
|
||||
|
||||
assertThat(mono.block()).isFalse();
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM person WHERE id='myid';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void countShouldExecuteCountQueryElement() {
|
||||
|
||||
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
|
||||
when(row.getLong(0)).thenReturn(42L);
|
||||
when(columnDefinitions.size()).thenReturn(1);
|
||||
|
||||
Mono<Long> mono = template.count(Person.class);
|
||||
|
||||
assertThat(mono.block()).isEqualTo(42L);
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT count(*) FROM person;");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertShouldInsertEntity() {
|
||||
|
||||
when(reactiveResultSet.wasApplied()).thenReturn(true);
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
Mono<Person> mono = template.insert(person);
|
||||
|
||||
assertThat(mono.block()).isEqualTo(person);
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("INSERT INTO person (firstname,id,lastname) VALUES ('Walter','heisenberg','White');");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertShouldTranslateException() {
|
||||
|
||||
reset(session);
|
||||
when(session.execute(any(Statement.class)))
|
||||
.thenReturn(Mono.error(new NoHostAvailableException(Collections.emptyMap())));
|
||||
|
||||
Mono<Person> mono = template.insert(new Person("heisenberg", "Walter", "White"));
|
||||
|
||||
try {
|
||||
mono.block();
|
||||
|
||||
fail("Missing CassandraConnectionFailureException");
|
||||
} catch (CassandraConnectionFailureException e) {
|
||||
assertThat(e).hasRootCauseInstanceOf(NoHostAvailableException.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertShouldNotApplyInsert() {
|
||||
|
||||
when(reactiveResultSet.wasApplied()).thenReturn(false);
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
Mono<Person> mono = template.insert(person);
|
||||
|
||||
assertThat(mono.block()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void updateShouldUpdateEntity() {
|
||||
|
||||
when(reactiveResultSet.wasApplied()).thenReturn(true);
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
Mono<Person> mono = template.update(person);
|
||||
|
||||
assertThat(mono.block()).isEqualTo(person);
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString())
|
||||
.isEqualTo("UPDATE person SET firstname='Walter',lastname='White' WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void updateShouldTranslateException() {
|
||||
|
||||
reset(session);
|
||||
when(session.execute(any(Statement.class)))
|
||||
.thenReturn(Mono.error(new NoHostAvailableException(Collections.emptyMap())));
|
||||
|
||||
Mono<Person> mono = template.update(new Person("heisenberg", "Walter", "White"));
|
||||
|
||||
try {
|
||||
mono.block();
|
||||
|
||||
fail("Missing CassandraConnectionFailureException");
|
||||
} catch (CassandraConnectionFailureException e) {
|
||||
assertThat(e).hasRootCauseInstanceOf(NoHostAvailableException.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void updateShouldNotApplyUpdate() {
|
||||
|
||||
when(reactiveResultSet.wasApplied()).thenReturn(false);
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
Mono<Person> mono = template.update(person);
|
||||
|
||||
assertThat(mono.block()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteShouldRemoveEntity() {
|
||||
|
||||
when(reactiveResultSet.wasApplied()).thenReturn(true);
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
|
||||
Mono<Person> mono = template.delete(person);
|
||||
|
||||
assertThat(mono.block()).isEqualTo(person);
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("DELETE FROM person WHERE id='heisenberg';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteShouldTranslateException() {
|
||||
|
||||
reset(session);
|
||||
when(session.execute(any(Statement.class)))
|
||||
.thenReturn(Mono.error(new NoHostAvailableException(Collections.emptyMap())));
|
||||
|
||||
Mono<Person> mono = template.delete(new Person("heisenberg", "Walter", "White"));
|
||||
|
||||
try {
|
||||
mono.block();
|
||||
|
||||
fail("Missing CassandraConnectionFailureException");
|
||||
} catch (CassandraConnectionFailureException e) {
|
||||
assertThat(e).hasRootCauseInstanceOf(NoHostAvailableException.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteShouldNotApplyRemoval() {
|
||||
|
||||
when(reactiveResultSet.wasApplied()).thenReturn(false);
|
||||
|
||||
Person person = new Person("heisenberg", "Walter", "White");
|
||||
Mono<Person> mono = template.delete(person);
|
||||
|
||||
assertThat(mono.block()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void truncateShouldRemoveEntities() {
|
||||
|
||||
template.truncate(Person.class).block();
|
||||
|
||||
verify(session).execute(statementCaptor.capture());
|
||||
assertThat(statementCaptor.getValue().toString()).isEqualTo("TRUNCATE person;");
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,17 @@ package org.springframework.data.cassandra.domain;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.cassandra.mapping.Table;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Table
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Person {
|
||||
|
||||
@Id String id;
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
|
||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.repository.config.EnableReactiveCassandraRepositories;
|
||||
import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.data.repository.reactive.RxJavaCrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.datastax.driver.core.KeyspaceMetadata;
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.TableMetadata;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.TestSubscriber;
|
||||
import rx.Observable;
|
||||
import rx.Single;
|
||||
|
||||
/**
|
||||
* Test for {@link ReactiveCassandraRepository} using reactive wrapper type conversion.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @soundtrack Dj Marc - Euromix 97 Part 1
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = ConvertingReactiveCassandraRepositoryTests.Config.class)
|
||||
public class ConvertingReactiveCassandraRepositoryTests extends AbstractKeyspaceCreatingIntegrationTest {
|
||||
|
||||
@EnableReactiveCassandraRepositories(includeFilters = @Filter(value = Repository.class),
|
||||
considerNestedRepositories = true)
|
||||
@Configuration
|
||||
public static class Config extends IntegrationTestConfig {
|
||||
|
||||
@Override
|
||||
public String[] getEntityBasePackages() {
|
||||
return new String[] { Person.class.getPackage().getName() };
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired Session session;
|
||||
@Autowired ReactiveCassandraTemplate template;
|
||||
@Autowired MixedPersonRepostitory reactiveRepository;
|
||||
@Autowired PersonRepostitory reactivePersonRepostitory;
|
||||
@Autowired RxJavaPersonRepostitory rxJavaPersonRepostitory;
|
||||
|
||||
Person dave, oliver, carter, boyd;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
KeyspaceMetadata keyspace = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace());
|
||||
TableMetadata person = keyspace.getTable("person");
|
||||
|
||||
if (person.getIndex("IX_person_lastname") == null) {
|
||||
|
||||
session.execute("CREATE INDEX IX_person_lastname ON person (lastname);");
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
reactiveRepository.deleteAll().block();
|
||||
|
||||
dave = new Person("42", "Dave", "Matthews");
|
||||
oliver = new Person("4", "Oliver August", "Matthews");
|
||||
carter = new Person("49", "Carter", "Beauford");
|
||||
boyd = new Person("45", "Boyd", "Tinsley");
|
||||
|
||||
TestSubscriber<Person> subscriber = TestSubscriber.create();
|
||||
reactiveRepository.save(Arrays.asList(oliver, dave, carter, boyd)).subscribe(subscriber);
|
||||
|
||||
subscriber.await().assertComplete().assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void reactiveStreamsMethodsShouldWork() throws InterruptedException {
|
||||
|
||||
TestSubscriber<Boolean> subscriber = TestSubscriber.subscribe(reactivePersonRepostitory.exists(dave.getId()));
|
||||
|
||||
subscriber.awaitAndAssertNextValueCount(1).assertNoError().assertValues(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void reactiveStreamsQueryMethodsShouldWork() {
|
||||
|
||||
TestSubscriber<Person> subscriber = TestSubscriber
|
||||
.subscribe(reactivePersonRepostitory.findByLastname(boyd.getLastname()));
|
||||
|
||||
subscriber.awaitAndAssertNextValueCount(1).assertValues(boyd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void simpleRxJavaMethodsShouldWork() {
|
||||
|
||||
rx.observers.TestSubscriber<Boolean> subscriber = new rx.observers.TestSubscriber<>();
|
||||
rxJavaPersonRepostitory.exists(dave.getId()).subscribe(subscriber);
|
||||
|
||||
subscriber.awaitTerminalEvent();
|
||||
subscriber.assertCompleted();
|
||||
subscriber.assertNoErrors();
|
||||
subscriber.assertValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsWithSingleRxJavaIdMethodsShouldWork() {
|
||||
|
||||
rx.observers.TestSubscriber<Boolean> subscriber = new rx.observers.TestSubscriber<>();
|
||||
rxJavaPersonRepostitory.exists(Single.just(dave.getId())).subscribe(subscriber);
|
||||
|
||||
subscriber.awaitTerminalEvent();
|
||||
subscriber.assertCompleted();
|
||||
subscriber.assertNoErrors();
|
||||
subscriber.assertValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void singleRxJavaQueryMethodShouldWork() {
|
||||
|
||||
rx.observers.TestSubscriber<Person> subscriber = new rx.observers.TestSubscriber<>();
|
||||
rxJavaPersonRepostitory.findManyByLastname(dave.getLastname()).subscribe(subscriber);
|
||||
|
||||
subscriber.awaitTerminalEvent();
|
||||
subscriber.assertNoErrors();
|
||||
subscriber.assertCompleted();
|
||||
subscriber.assertValueCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void singleProjectedRxJavaQueryMethodShouldWork() {
|
||||
|
||||
rx.observers.TestSubscriber<ProjectedPerson> subscriber = new rx.observers.TestSubscriber<>();
|
||||
rxJavaPersonRepostitory.findProjectedByLastname(carter.getLastname()).subscribe(subscriber);
|
||||
|
||||
subscriber.awaitTerminalEvent();
|
||||
subscriber.assertCompleted();
|
||||
subscriber.assertNoErrors();
|
||||
|
||||
ProjectedPerson projectedPerson = subscriber.getOnNextEvents().get(0);
|
||||
assertThat(projectedPerson.getFirstname()).isEqualTo(carter.getFirstname());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void observableRxJavaQueryMethodShouldWork() {
|
||||
|
||||
rx.observers.TestSubscriber<Person> subscriber = new rx.observers.TestSubscriber<>();
|
||||
rxJavaPersonRepostitory.findByLastname(boyd.getLastname()).subscribe(subscriber);
|
||||
|
||||
subscriber.awaitTerminalEvent();
|
||||
subscriber.assertCompleted();
|
||||
subscriber.assertNoErrors();
|
||||
subscriber.assertValue(boyd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void mixedRepositoryShouldWork() {
|
||||
|
||||
Person value = reactiveRepository.findByLastname(boyd.getLastname()).toBlocking().value();
|
||||
|
||||
assertThat(value).isEqualTo(boyd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldFindOneByPublisherOfLastName() {
|
||||
|
||||
Person carter = reactiveRepository.findByLastname(Single.just(this.carter.getLastname())).block();
|
||||
|
||||
assertThat(carter.getFirstname()).isEqualTo(this.carter.getFirstname());
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface PersonRepostitory extends ReactiveCrudRepository<Person, String> {
|
||||
|
||||
Publisher<Person> findByLastname(String lastname);
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface RxJavaPersonRepostitory extends RxJavaCrudRepository<Person, String> {
|
||||
|
||||
Observable<Person> findManyByLastname(String lastname);
|
||||
|
||||
Single<Person> findByLastname(String lastname);
|
||||
|
||||
Single<ProjectedPerson> findProjectedByLastname(String lastname);
|
||||
}
|
||||
|
||||
@Repository
|
||||
interface MixedPersonRepostitory extends ReactiveCassandraRepository<Person, String> {
|
||||
|
||||
Single<Person> findByLastname(String lastname);
|
||||
|
||||
Mono<Person> findByLastname(Single<String> lastname);
|
||||
}
|
||||
|
||||
interface ProjectedPerson {
|
||||
|
||||
String getId();
|
||||
|
||||
String getFirstname();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.domain.Group;
|
||||
import org.springframework.data.cassandra.domain.GroupKey;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.repository.support.ReactiveCassandraRepositoryFactory;
|
||||
import org.springframework.data.cassandra.repository.support.SimpleReactiveCassandraRepository;
|
||||
import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.datastax.driver.core.KeyspaceMetadata;
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.TableMetadata;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Test for {@link ReactiveCassandraRepository} query methods.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
public class ReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest
|
||||
implements BeanClassLoaderAware, BeanFactoryAware {
|
||||
|
||||
@Configuration
|
||||
public static class Config extends IntegrationTestConfig {
|
||||
|
||||
@Override
|
||||
public String[] getEntityBasePackages() {
|
||||
return new String[] { Person.class.getPackage().getName() };
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired ReactiveCassandraOperations operations;
|
||||
@Autowired Session session;
|
||||
|
||||
ReactiveCassandraRepositoryFactory factory;
|
||||
ClassLoader classLoader;
|
||||
BeanFactory beanFactory;
|
||||
PersonRepository repository;
|
||||
GroupRepository groupRepostitory;
|
||||
|
||||
Person dave, oliver, carter, boyd;
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
KeyspaceMetadata keyspace = session.getCluster().getMetadata().getKeyspace(session.getLoggedKeyspace());
|
||||
TableMetadata person = keyspace.getTable("person");
|
||||
|
||||
if (person.getIndex("IX_lastname") == null) {
|
||||
session.execute("CREATE INDEX IX_lastname ON person (lastname);");
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
factory = new ReactiveCassandraRepositoryFactory(operations);
|
||||
factory.setRepositoryBaseClass(SimpleReactiveCassandraRepository.class);
|
||||
factory.setBeanClassLoader(classLoader);
|
||||
factory.setBeanFactory(beanFactory);
|
||||
factory.setEvaluationContextProvider(DefaultEvaluationContextProvider.INSTANCE);
|
||||
|
||||
repository = factory.getRepository(PersonRepository.class);
|
||||
groupRepostitory = factory.getRepository(GroupRepository.class);
|
||||
|
||||
repository.deleteAll().block();
|
||||
groupRepostitory.deleteAll().block();
|
||||
|
||||
dave = new Person("42", "Dave", "Matthews");
|
||||
oliver = new Person("4", "Oliver August", "Matthews");
|
||||
carter = new Person("49", "Carter", "Beauford");
|
||||
boyd = new Person("45", "Boyd", "Tinsley");
|
||||
|
||||
repository.save(Arrays.asList(oliver, dave, carter, boyd)).last().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldFindByLastName() {
|
||||
|
||||
List<Person> list = repository.findByLastname("Matthews").collectList().block();
|
||||
|
||||
assertThat(list).hasSize(2).contains(dave, oliver);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldFindOneByLastName() {
|
||||
|
||||
Person carter = repository.findOneByLastname("Beauford").block();
|
||||
|
||||
assertThat(carter.getFirstname()).isEqualTo("Carter");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldFindOneByPublisherOfLastName() {
|
||||
|
||||
Person carter = repository.findByLastname(Mono.just("Beauford")).block();
|
||||
|
||||
assertThat(carter.getFirstname()).isEqualTo("Carter");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldFindUsingPublishersInStringQuery() {
|
||||
|
||||
List<Person> persons = repository.findStringQuery(Mono.just("Matthews")).collectList().block();
|
||||
|
||||
assertThat(persons).contains(dave);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldFindByLastNameAndSort() {
|
||||
|
||||
GroupKey key1 = new GroupKey("Simpsons", "hash", "Bart");
|
||||
GroupKey key2 = new GroupKey("Simpsons", "hash", "Homer");
|
||||
|
||||
groupRepostitory.save(Flux.just(new Group(key1), new Group(key2))).blockLast();
|
||||
|
||||
List<Group> persons = groupRepostitory
|
||||
.findByIdGroupnameAndIdHashPrefix("Simpsons", "hash", new Sort(Direction.ASC, "id.username")).collectList()
|
||||
.block();
|
||||
assertThat(persons).containsSequence(new Group(key1), new Group(key2));
|
||||
|
||||
List<Group> reversed = groupRepostitory
|
||||
.findByIdGroupnameAndIdHashPrefix("Simpsons", "hash", new Sort(Direction.DESC, "id.username")).collectList()
|
||||
.block();
|
||||
assertThat(reversed).containsSequence(new Group(key2), new Group(key1));
|
||||
}
|
||||
|
||||
interface PersonRepository extends ReactiveCassandraRepository<Person, String> {
|
||||
|
||||
Flux<Person> findByLastname(String lastname);
|
||||
|
||||
Mono<Person> findOneByLastname(String lastname);
|
||||
|
||||
Mono<Person> findByLastname(Publisher<String> lastname);
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname = ?0")
|
||||
Flux<Person> findStringQuery(Mono<String> lastname);
|
||||
}
|
||||
|
||||
interface GroupRepository extends ReactiveCassandraRepository<Group, GroupKey> {
|
||||
|
||||
Flux<Group> findByIdGroupnameAndIdHashPrefix(String groupname, String hashPrefix, Sort sort);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class CassandraRepositoryConfigurationExtensionUnitTests {
|
||||
* @see DATACASS-257
|
||||
*/
|
||||
@Test
|
||||
public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() {
|
||||
public void isStrictMatchIfDomainTypeIsAnnotatedWithTable() {
|
||||
assertHasRepo(SampleRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true));
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class CassandraRepositoryConfigurationExtensionUnitTests {
|
||||
* @see DATACASS-257
|
||||
*/
|
||||
@Test
|
||||
public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() {
|
||||
public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithTable() {
|
||||
|
||||
assertDoesNotHaveRepo(UnannotatedRepository.class,
|
||||
extension.getRepositoryConfigurations(configurationSource, loader, true));
|
||||
@@ -101,8 +101,8 @@ public class CassandraRepositoryConfigurationExtensionUnitTests {
|
||||
}
|
||||
}
|
||||
|
||||
fail("Expected to find config for repository interface ".concat(repositoryInterface.getName()).concat(" but got ")
|
||||
.concat(configs.toString()));
|
||||
fail(String.format("Expected to find config for repository interface %s but got %s", repositoryInterface.getName(),
|
||||
configs.toString()));
|
||||
}
|
||||
|
||||
@EnableCassandraRepositories(considerNestedRepositories = true)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cassandra.core.ReactiveSession;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveCassandraRepositoriesRegistrar}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
public class ReactiveCassandraRepositoriesRegistrarUnitTests {
|
||||
|
||||
@Configuration
|
||||
@EnableReactiveCassandraRepositories(basePackages = "org.springframework.data.cassandra.repository.config",
|
||||
considerNestedRepositories = true,
|
||||
includeFilters = @Filter(pattern = ".*ReactivePersonRepository", type = FilterType.REGEX))
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public ReactiveCassandraTemplate reactiveCassandraTemplate() throws Exception {
|
||||
return new ReactiveCassandraTemplate(mock(ReactiveSession.class), new MappingCassandraConverter());
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired ReactivePersonRepository personRepository;
|
||||
@Autowired ApplicationContext context;
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void testConfiguration() {}
|
||||
|
||||
static interface ReactivePersonRepository extends ReactiveCassandraRepository<Person, String> {}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.cassandra.repository.config;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.type.StandardAnnotationMetadata;
|
||||
import org.springframework.data.cassandra.mapping.Table;
|
||||
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
|
||||
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.config.RepositoryConfiguration;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationSource;
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
import org.springframework.data.repository.reactive.RxJavaCrudRepository;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveCassandraRepositoryConfigurationExtension}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class ReactiveCassandraRepositoryConfigurationExtensionUnitTests {
|
||||
|
||||
StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true);
|
||||
ResourceLoader loader = new PathMatchingResourcePatternResolver();
|
||||
Environment environment = new StandardEnvironment();
|
||||
RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata,
|
||||
EnableReactiveCassandraRepositories.class, loader, environment);
|
||||
|
||||
ReactiveCassandraRepositoryConfigurationExtension extension;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
extension = new ReactiveCassandraRepositoryConfigurationExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void isStrictMatchIfDomainTypeIsAnnotatedWithTable() {
|
||||
assertHasRepo(SampleRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void isStrictMatchIfRepositoryExtendsStoreSpecificBase() {
|
||||
assertHasRepo(StoreRepository.class, extension.getRepositoryConfigurations(configurationSource, loader, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void isNotStrictMatchIfDomainTypeIsNotAnnotatedWithDocument() {
|
||||
|
||||
assertDoesNotHaveRepo(UnannotatedRepository.class,
|
||||
extension.getRepositoryConfigurations(configurationSource, loader, true));
|
||||
}
|
||||
|
||||
private static void assertDoesNotHaveRepo(Class<?> repositoryInterface,
|
||||
Collection<RepositoryConfiguration<RepositoryConfigurationSource>> configs) {
|
||||
|
||||
try {
|
||||
|
||||
assertHasRepo(repositoryInterface, configs);
|
||||
fail("Expected not to find config for repository interface " + repositoryInterface.getName());
|
||||
} catch (AssertionError error) {
|
||||
// repo not there. we're fine.
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertHasRepo(Class<?> repositoryInterface,
|
||||
Collection<RepositoryConfiguration<RepositoryConfigurationSource>> configs) {
|
||||
|
||||
for (RepositoryConfiguration<?> config : configs) {
|
||||
if (config.getRepositoryInterface().equals(repositoryInterface.getName())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fail(String.format("Expected to find config for repository interface %s but got %s", repositoryInterface.getName(),
|
||||
configs.toString()));
|
||||
}
|
||||
|
||||
@EnableReactiveCassandraRepositories(considerNestedRepositories = true)
|
||||
static class Config {
|
||||
|
||||
}
|
||||
|
||||
@Table
|
||||
static class Sample {}
|
||||
|
||||
interface SampleRepository extends RxJavaCrudRepository<Sample, Long> {}
|
||||
|
||||
interface UnannotatedRepository extends ReactiveCrudRepository<Object, Long> {}
|
||||
|
||||
interface StoreRepository extends ReactiveCassandraRepository<Object, Long> {}
|
||||
}
|
||||
@@ -16,17 +16,16 @@
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBindingParser.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBinding;
|
||||
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding;
|
||||
import org.springframework.data.cassandra.repository.query.StringBasedQuery.ParameterBindingParser;
|
||||
|
||||
/**
|
||||
* Unit tests for
|
||||
* {@link org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBindingParser}.
|
||||
* Unit tests for {@link ParameterBindingParser}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@@ -39,9 +38,10 @@ public class ParameterBindingParserUnitTests {
|
||||
public void parseWithoutParameters() {
|
||||
|
||||
String query = "SELECT * FROM hello_world";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
List<ParameterBinding> bindings = new ArrayList<>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo(query);
|
||||
assertThat(bindings).isEmpty();
|
||||
@@ -56,7 +56,8 @@ public class ParameterBindingParserUnitTests {
|
||||
String query = "SELECT * FROM hello_world WHERE a = 1 AND b = {'list'} AND c = {'key':'value'}";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo(query);
|
||||
assertThat(bindings).isEmpty();
|
||||
@@ -71,7 +72,8 @@ public class ParameterBindingParserUnitTests {
|
||||
String query = "SELECT * FROM hello_world WHERE a = ?0 and b = ?13";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?");
|
||||
assertThat(bindings).hasSize(2);
|
||||
@@ -89,7 +91,8 @@ public class ParameterBindingParserUnitTests {
|
||||
String query = "SELECT * FROM hello_world WHERE a = :hello and b = :world";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?");
|
||||
assertThat(bindings).hasSize(2);
|
||||
@@ -104,7 +107,8 @@ public class ParameterBindingParserUnitTests {
|
||||
String query = "SELECT * FROM hello_world WHERE a = ?#{[0]} and b = ?#{[2]}";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?");
|
||||
assertThat(bindings).hasSize(2);
|
||||
@@ -119,7 +123,8 @@ public class ParameterBindingParserUnitTests {
|
||||
String query = "SELECT * FROM hello_world WHERE a = :#{#a} and b = :#{#b}";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?");
|
||||
assertThat(bindings).hasSize(2);
|
||||
@@ -134,7 +139,8 @@ public class ParameterBindingParserUnitTests {
|
||||
String query = "SELECT * FROM hello_world WHERE (a = ?1 and b = :name) and c = (:#{#a}) and (d = ?#{[1]})";
|
||||
List<ParameterBinding> bindings = new ArrayList<ParameterBinding>();
|
||||
|
||||
String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings);
|
||||
String transformed = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
bindings);
|
||||
|
||||
assertThat(transformed).isEqualTo(
|
||||
"SELECT * FROM hello_world WHERE (a = ?_param_? and b = ?_param_?) and c = (?_param_?) and (d = ?_param_?)");
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.cassandra.domain.AllPossibleTypes;
|
||||
import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraType;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
import org.threeten.bp.LocalDateTime;
|
||||
|
||||
import com.datastax.driver.core.DataType;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import rx.Single;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveCassandraParameterAccessor}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @soundtrack Ace Of Base - Cruel Summer (Album Edit)
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ReactiveCassandraParameterAccessorUnitTests {
|
||||
|
||||
private ReactiveCassandraParameterAccessor accessor;
|
||||
private GenericConversionService conversionService = new GenericConversionService();
|
||||
|
||||
@Mock ProjectionFactory projectionFactory;
|
||||
|
||||
RepositoryMetadata metadata = new DefaultRepositoryMetadata(PossibleRepository.class);
|
||||
CassandraMappingContext context = new BasicCassandraMappingContext();
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void returnsCassandraSimpleType() throws Exception {
|
||||
|
||||
Method method = PossibleRepository.class.getMethod("findByFirstname", Flux.class);
|
||||
ReactiveCassandraParameterAccessor accessor = new ReactiveCassandraParameterAccessor(
|
||||
getCassandraQueryMethod(method), new Object[] { Flux.just("firstname") });
|
||||
|
||||
assertThat(accessor.getDataType(0)).isEqualTo(DataType.varchar());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnNoTypeForComplexTypes() throws Exception {
|
||||
|
||||
Method method = PossibleRepository.class.getMethod("findByLocalDateTime", Mono.class);
|
||||
ReactiveCassandraParameterAccessor accessor = new ReactiveCassandraParameterAccessor(
|
||||
getCassandraQueryMethod(method), new Object[] { Flux.just(LocalDateTime.of(2000, 10, 11, 12, 13, 14)) });
|
||||
|
||||
assertThat(accessor.getDataType(0)).isNull();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void returnTypeForAnnotatedParameter() throws Exception {
|
||||
|
||||
Method method = PossibleRepository.class.getMethod("findByAnnotatedByLocalDateTime", Single.class);
|
||||
ReactiveCassandraParameterAccessor accessor = new ReactiveCassandraParameterAccessor(
|
||||
getCassandraQueryMethod(method), new Object[] { Single.just(LocalDateTime.of(2000, 10, 11, 12, 13, 14)) });
|
||||
|
||||
assertThat(accessor.getDataType(0)).isEqualTo(DataType.date());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void returnTypeForAnnotatedParameterWhenUsingStringValue() throws Exception {
|
||||
|
||||
Method method = PossibleRepository.class.getMethod("findByAnnotatedObject", Mono.class);
|
||||
ReactiveCassandraParameterAccessor accessor = new ReactiveCassandraParameterAccessor(
|
||||
getCassandraQueryMethod(method), new Object[] { Mono.just("") });
|
||||
|
||||
assertThat(accessor.getDataType(0)).isEqualTo(DataType.date());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void returnTypeForAnnotatedParameterWhenUsingNullValue() throws Exception {
|
||||
|
||||
Method method = PossibleRepository.class.getMethod("findByAnnotatedObject", Mono.class);
|
||||
ReactiveCassandraParameterAccessor accessor = new ReactiveCassandraParameterAccessor(
|
||||
getCassandraQueryMethod(method), new Object[] { Mono.just("") });
|
||||
|
||||
assertThat(accessor.getDataType(0)).isEqualTo(DataType.date());
|
||||
}
|
||||
|
||||
private CassandraQueryMethod getCassandraQueryMethod(Method method) {
|
||||
return new ReactiveCassandraQueryMethod(method, metadata, projectionFactory, context);
|
||||
}
|
||||
|
||||
interface PossibleRepository extends Repository<AllPossibleTypes, Long> {
|
||||
|
||||
Flux<AllPossibleTypes> findByFirstname(Flux<String> firstname);
|
||||
|
||||
Flux<AllPossibleTypes> findByLocalDateTime(Mono<LocalDateTime> dateTime);
|
||||
|
||||
Flux<AllPossibleTypes> findByAnnotatedByLocalDateTime(
|
||||
@CassandraType(type = DataType.Name.DATE) Single<LocalDateTime> dateTime);
|
||||
|
||||
Flux<AllPossibleTypes> findByAnnotatedObject(@CassandraType(type = DataType.Name.DATE) Mono<Object> dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import rx.Single;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveCassandraQueryMethod}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class ReactiveCassandraQueryMethodUnitTests {
|
||||
|
||||
CassandraMappingContext context;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = new BasicCassandraMappingContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void considersMethodAsStreamQuery() throws Exception {
|
||||
|
||||
ReactiveCassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "method");
|
||||
|
||||
assertThat(queryMethod.isStreamQuery()).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void considersMethodAsCollectionQuery() throws Exception {
|
||||
|
||||
ReactiveCassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "method");
|
||||
|
||||
assertThat(queryMethod.isCollectionQuery()).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void considersMonoMethodAsEntityQuery() throws Exception {
|
||||
|
||||
ReactiveCassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "mono");
|
||||
|
||||
assertThat(queryMethod.isCollectionQuery()).isFalse();
|
||||
assertThat(queryMethod.isQueryForEntity()).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void considersSingleMethodAsEntityQuery() throws Exception {
|
||||
|
||||
ReactiveCassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "single");
|
||||
|
||||
assertThat(queryMethod.isCollectionQuery()).isFalse();
|
||||
assertThat(queryMethod.isQueryForEntity()).isTrue();
|
||||
}
|
||||
|
||||
private ReactiveCassandraQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters)
|
||||
throws Exception {
|
||||
|
||||
Method method = repository.getMethod(name, parameters);
|
||||
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
|
||||
return new ReactiveCassandraQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
interface SampleRepository extends Repository<Person, Long> {
|
||||
|
||||
Flux<Person> method();
|
||||
|
||||
Single<Person> single();
|
||||
|
||||
Mono<Person> mono();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.data.cassandra.convert.CassandraConverter;
|
||||
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.repository.CassandraRepository;
|
||||
import org.springframework.data.cassandra.repository.Query;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import rx.Single;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactivePartTreeCassandraQuery}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ReactivePartTreeCassandraQueryUnitTests {
|
||||
|
||||
@Rule public ExpectedException exception = ExpectedException.none();
|
||||
|
||||
@Mock ReactiveCassandraOperations mockCassandraOperations;
|
||||
|
||||
private CassandraMappingContext mappingContext;
|
||||
private CassandraConverter converter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mappingContext = new BasicCassandraMappingContext();
|
||||
converter = new MappingCassandraConverter(mappingContext);
|
||||
|
||||
when(mockCassandraOperations.getConverter()).thenReturn(converter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldDeriveSimpleQuery() {
|
||||
String query = deriveQueryFromMethod("findByLastname", "foo");
|
||||
|
||||
assertThat(query).isEqualTo("SELECT * FROM person WHERE lastname='foo';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldDeriveSimpleQueryWithoutNames() {
|
||||
String query = deriveQueryFromMethod("findPersonBy");
|
||||
|
||||
assertThat(query).isEqualTo("SELECT * FROM person;");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void shouldDeriveAndQuery() {
|
||||
String query = deriveQueryFromMethod("findByFirstnameAndLastname", "foo", "bar");
|
||||
|
||||
assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname='foo' AND lastname='bar';");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void usesDynamicProjection() {
|
||||
String query = deriveQueryFromMethod("findDynamicallyProjectedBy", PersonProjection.class);
|
||||
|
||||
assertThat(query).isEqualTo("SELECT * FROM person;");
|
||||
}
|
||||
|
||||
private String deriveQueryFromMethod(String method, Object... args) {
|
||||
Class<?>[] types = new Class<?>[args.length];
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
types[i] = args[i].getClass();
|
||||
}
|
||||
|
||||
ReactivePartTreeCassandraQuery partTreeQuery = createQueryForMethod(method, types);
|
||||
|
||||
CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(),
|
||||
args);
|
||||
|
||||
return partTreeQuery.createQuery(new ConvertingParameterAccessor(mockCassandraOperations.getConverter(), accessor));
|
||||
}
|
||||
|
||||
private ReactivePartTreeCassandraQuery createQueryForMethod(String methodName, Class<?>... paramTypes) {
|
||||
try {
|
||||
Method method = Repo.class.getMethod(methodName, paramTypes);
|
||||
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
|
||||
CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, new DefaultRepositoryMetadata(Repo.class),
|
||||
factory, mappingContext);
|
||||
|
||||
return new ReactivePartTreeCassandraQuery(queryMethod, mockCassandraOperations);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException(e.getMessage(), e);
|
||||
} catch (SecurityException e) {
|
||||
throw new IllegalArgumentException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
interface Repo extends CassandraRepository<Person> {
|
||||
|
||||
@Query()
|
||||
Flux<Person> findByLastname(String lastname);
|
||||
|
||||
Flux<Person> findByFirstnameAndLastname(String firstname, String lastname);
|
||||
|
||||
Flux<Person> findPersonByFirstnameAndLastname(String firstname, String lastname);
|
||||
|
||||
Flux<Person> findByAge(Integer age);
|
||||
|
||||
Flux<Person> findPersonBy();
|
||||
|
||||
Mono<PersonProjection> findPersonProjectedBy();
|
||||
|
||||
<T> Single<T> findDynamicallyProjectedBy(Class<T> type);
|
||||
|
||||
}
|
||||
|
||||
interface PersonProjection {
|
||||
|
||||
String getFirstname();
|
||||
|
||||
String getLastname();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.cassandra.core.ReactiveCqlOperations;
|
||||
import org.springframework.cassandra.core.ReactiveSession;
|
||||
import org.springframework.cassandra.core.ReactiveSessionCallback;
|
||||
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext;
|
||||
import org.springframework.data.cassandra.repository.Query;
|
||||
import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
|
||||
import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.CodecRegistry;
|
||||
import com.datastax.driver.core.Configuration;
|
||||
import com.datastax.driver.core.SimpleStatement;
|
||||
import com.datastax.driver.core.querybuilder.QueryBuilder;
|
||||
import com.datastax.driver.core.querybuilder.Select;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StringBasedCassandraQuery}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ReactiveStringBasedCassandraQueryUnitTests {
|
||||
|
||||
SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
@Mock ReactiveCassandraOperations operations;
|
||||
@Mock ReactiveCqlOperations cqlOperations;
|
||||
@Mock ReactiveSession reactiveSession;
|
||||
@Mock Cluster cluster;
|
||||
@Mock Configuration configuration;
|
||||
|
||||
RepositoryMetadata metadata;
|
||||
MappingCassandraConverter converter;
|
||||
ProjectionFactory factory;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
when(operations.getConverter()).thenReturn(converter);
|
||||
when(operations.getReactiveCqlOperations()).thenReturn(cqlOperations);
|
||||
when(cqlOperations.execute(any(ReactiveSessionCallback.class))).thenAnswer(
|
||||
invocation -> ((ReactiveSessionCallback) invocation.getArguments()[0]).doInSession(reactiveSession));
|
||||
when(reactiveSession.getCluster()).thenReturn(cluster);
|
||||
when(cluster.getConfiguration()).thenReturn(configuration);
|
||||
when(configuration.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT_INSTANCE);
|
||||
|
||||
this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class);
|
||||
this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext());
|
||||
this.factory = new SpelAwareProxyProjectionFactory();
|
||||
|
||||
this.converter.afterPropertiesSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void bindsSimplePropertyCorrectly() throws Exception {
|
||||
|
||||
ReactiveStringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class);
|
||||
CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(
|
||||
cassandraQuery.getQueryMethod(), "White");
|
||||
|
||||
String stringQuery = cassandraQuery.createQuery(accessor);
|
||||
SimpleStatement actual = new SimpleStatement(stringQuery);
|
||||
|
||||
String table = Person.class.getSimpleName().toLowerCase();
|
||||
Select expected = QueryBuilder.select().all().from(table);
|
||||
expected.setForceNoValues(true);
|
||||
expected.where(QueryBuilder.eq("lastname", "White"));
|
||||
|
||||
assertThat(actual.getQueryString()).isEqualTo(expected.getQueryString());
|
||||
}
|
||||
|
||||
private ReactiveStringBasedCassandraQuery getQueryMethod(String name, Class<?>... args) {
|
||||
|
||||
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
|
||||
CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory,
|
||||
converter.getMappingContext());
|
||||
return new ReactiveStringBasedCassandraQuery(queryMethod, operations, PARSER,
|
||||
new ExtensionAwareEvaluationContextProvider());
|
||||
}
|
||||
|
||||
private interface SampleRepository extends Repository<Person, String> {
|
||||
|
||||
@Query("SELECT * FROM person WHERE lastname=?0;")
|
||||
Person findByLastname(String lastname);
|
||||
}
|
||||
}
|
||||
@@ -382,6 +382,7 @@ public class StringBasedCassandraQueryUnitTests {
|
||||
}
|
||||
|
||||
private StringBasedCassandraQuery getQueryMethod(String name, Class<?>... args) {
|
||||
|
||||
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
|
||||
CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory,
|
||||
converter.getMappingContext());
|
||||
|
||||
@@ -83,11 +83,6 @@ class StubParameterAccessor implements CassandraParameterAccessor {
|
||||
return values[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getValues() {
|
||||
return new Object[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasBindableNullValue() {
|
||||
return false;
|
||||
@@ -102,4 +97,9 @@ class StubParameterAccessor implements CassandraParameterAccessor {
|
||||
public CassandraType findCassandraType(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getValues() {
|
||||
return new Object[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.springframework.data.cassandra.convert.CassandraConverter;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.mapping.CassandraPersistentEntity;
|
||||
import org.springframework.data.cassandra.repository.query.CassandraEntityInformation;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveCassandraRepositoryFactory}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public class ReactiveCassandraRepositoryFactoryUnitTests {
|
||||
|
||||
@Mock CassandraConverter converter;
|
||||
@Mock CassandraMappingContext mappingContext;
|
||||
@Mock CassandraPersistentEntity entity;
|
||||
@Mock ReactiveCassandraTemplate template;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(template.getConverter()).thenReturn(converter);
|
||||
when(converter.getMappingContext()).thenReturn(mappingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void usesMappingCassandraEntityInformationIfMappingContextSet() {
|
||||
|
||||
when(mappingContext.getPersistentEntity(Person.class)).thenReturn(entity);
|
||||
when(entity.getType()).thenReturn(Person.class);
|
||||
|
||||
ReactiveCassandraRepositoryFactory repositoryFactory = new ReactiveCassandraRepositoryFactory(template);
|
||||
|
||||
CassandraEntityInformation<Person, Serializable> entityInformation = repositoryFactory
|
||||
.getEntityInformation(Person.class);
|
||||
|
||||
assertThat(entityInformation).isInstanceOf(MappingCassandraEntityInformation.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void createsRepositoryWithIdTypeLong() {
|
||||
|
||||
when(mappingContext.getPersistentEntity(Person.class)).thenReturn(entity);
|
||||
when(entity.getType()).thenReturn(Person.class);
|
||||
|
||||
ReactiveCassandraRepositoryFactory repositoryFactory = new ReactiveCassandraRepositoryFactory(template);
|
||||
MyPersonRepository repository = repositoryFactory.getRepository(MyPersonRepository.class);
|
||||
|
||||
assertThat(repository).isNotNull();
|
||||
}
|
||||
|
||||
interface MyPersonRepository extends Repository<Person, Long> {}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.cassandra.repository.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
|
||||
import org.springframework.data.cassandra.domain.Person;
|
||||
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
|
||||
import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig;
|
||||
import org.springframework.data.repository.query.DefaultEvaluationContextProvider;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.TestSubscriber;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SimpleReactiveCassandraRepository}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
public class SimpleReactiveCassandraRepositoryIntegrationTests extends AbstractKeyspaceCreatingIntegrationTest
|
||||
implements BeanClassLoaderAware, BeanFactoryAware {
|
||||
|
||||
@Configuration
|
||||
public static class Config extends IntegrationTestConfig {
|
||||
|
||||
@Override
|
||||
public String[] getEntityBasePackages() {
|
||||
return new String[] { Person.class.getPackage().getName() };
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired private ReactiveCassandraOperations operations;
|
||||
|
||||
ReactiveCassandraRepositoryFactory factory;
|
||||
ClassLoader classLoader;
|
||||
BeanFactory beanFactory;
|
||||
PersonRepostitory repository;
|
||||
|
||||
Person dave, oliver, carter, boyd;
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
factory = new ReactiveCassandraRepositoryFactory(operations);
|
||||
factory.setRepositoryBaseClass(SimpleReactiveCassandraRepository.class);
|
||||
factory.setBeanClassLoader(classLoader);
|
||||
factory.setBeanFactory(beanFactory);
|
||||
factory.setEvaluationContextProvider(DefaultEvaluationContextProvider.INSTANCE);
|
||||
|
||||
repository = factory.getRepository(PersonRepostitory.class);
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
dave = new Person("42", "Dave", "Matthews");
|
||||
oliver = new Person("4", "Oliver August", "Matthews");
|
||||
carter = new Person("49", "Carter", "Beauford");
|
||||
boyd = new Person("45", "Boyd", "Tinsley");
|
||||
|
||||
repository.save(Arrays.asList(oliver, dave, carter, boyd)).last().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsByIdShouldReturnTrueForExistingObject() {
|
||||
|
||||
Boolean exists = repository.exists(dave.getId()).block();
|
||||
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsByIdShouldReturnFalseForAbsentObject() {
|
||||
|
||||
TestSubscriber<Boolean> testSubscriber = TestSubscriber.subscribe(repository.exists("unknown"));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValues(false).assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsByMonoOfIdShouldReturnTrueForExistingObject() {
|
||||
|
||||
Boolean exists = repository.exists(Mono.just(dave.getId())).block();
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void existsByEmptyMonoOfIdShouldReturnEmptyMono() {
|
||||
|
||||
TestSubscriber<Boolean> testSubscriber = TestSubscriber.subscribe(repository.exists(Mono.empty()));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues().assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findOneShouldReturnObject() {
|
||||
|
||||
Person person = repository.findOne(dave.getId()).block();
|
||||
|
||||
assertThat(person).isEqualTo(dave);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findOneShouldCompleteWithoutValueForAbsentObject() {
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.findOne("unknown"));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues().assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findOneByMonoOfIdShouldReturnTrueForExistingObject() {
|
||||
|
||||
Person person = repository.findOne(Mono.just(dave.getId())).block();
|
||||
|
||||
assertThat(person).isEqualTo(dave);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findOneByEmptyMonoOfIdShouldReturnEmptyMono() {
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.findOne(Mono.empty()));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues().assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findAllShouldReturnAllResults() {
|
||||
|
||||
List<Person> persons = repository.findAll().collectList().block();
|
||||
|
||||
assertThat(persons).hasSize(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findAllByIterableOfIdShouldReturnResults() {
|
||||
|
||||
List<Person> persons = repository.findAll(Arrays.asList(dave.getId(), boyd.getId())).collectList().block();
|
||||
|
||||
assertThat(persons).hasSize(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findAllByPublisherOfIdShouldReturnResults() {
|
||||
|
||||
List<Person> persons = repository.findAll(Flux.just(dave.getId(), boyd.getId())).collectList().block();
|
||||
|
||||
assertThat(persons).hasSize(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void findAllByEmptyPublisherOfIdShouldReturnResults() {
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.findAll(Flux.empty()));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues().assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void countShouldReturnNumberOfRecords() {
|
||||
|
||||
TestSubscriber<Long> testSubscriber = TestSubscriber.subscribe(repository.count());
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(1).assertValues(4L).assertNoError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertEntityShouldInsertEntity() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
Person person = new Person("36", "Homer", "Simpson");
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.insert(person));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(1).assertValues(person);
|
||||
repository.findAll().count().subscribeWith(TestSubscriber.create()).awaitAndAssertNextValues(1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertShouldDeferredWrite() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
Person person = new Person("36", "Homer", "Simpson");
|
||||
|
||||
repository.insert(person);
|
||||
|
||||
repository.findAll().count().subscribeWith(TestSubscriber.create()).awaitAndAssertNextValues(0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertIterableOfEntitiesShouldInsertEntity() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber
|
||||
.subscribe(repository.insert(Arrays.asList(dave, oliver, boyd)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(3);
|
||||
|
||||
repository.findAll().count().subscribeWith(TestSubscriber.create()).awaitAndAssertNextValues(3L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void insertPublisherOfEntitiesShouldInsertEntity() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.insert(Flux.just(dave, oliver, boyd)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(3);
|
||||
repository.findAll().count().subscribeWith(TestSubscriber.create()).awaitAndAssertNextValues(3L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void saveEntityShouldUpdateExistingEntity() {
|
||||
|
||||
dave.setFirstname("Hello, Dave");
|
||||
dave.setLastname("Bowman");
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.save(dave));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(1).assertValues(dave);
|
||||
|
||||
Person loaded = repository.findOne(dave.getId()).block();
|
||||
|
||||
assertThat(loaded.getFirstname()).isEqualTo(dave.getFirstname());
|
||||
assertThat(loaded.getLastname()).isEqualTo(dave.getLastname());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void saveEntityShouldInsertNewEntity() {
|
||||
|
||||
Person person = new Person("36", "Homer", "Simpson");
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.save(person));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(1).assertValues(person);
|
||||
|
||||
Person loaded = repository.findOne(person.getId()).block();
|
||||
|
||||
assertThat(loaded).isEqualTo(person);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void saveIterableOfNewEntitiesShouldInsertEntity() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber
|
||||
.subscribe(repository.save(Arrays.asList(dave, oliver, boyd)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(3);
|
||||
|
||||
repository.findAll().count().subscribeWith(TestSubscriber.create()).awaitAndAssertNextValues(3L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void saveIterableOfMixedEntitiesShouldInsertEntity() {
|
||||
|
||||
Person person = new Person("36", "Homer", "Simpson");
|
||||
|
||||
dave.setFirstname("Hello, Dave");
|
||||
dave.setLastname("Bowman");
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.save(Arrays.asList(person, dave)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(2);
|
||||
|
||||
Person persistentDave = repository.findOne(dave.getId()).block();
|
||||
assertThat(persistentDave).isEqualTo(dave);
|
||||
|
||||
Person persistentHomer = repository.findOne(person.getId()).block();
|
||||
assertThat(persistentHomer).isEqualTo(person);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void savePublisherOfEntitiesShouldInsertEntity() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.save(Flux.just(dave, oliver, boyd)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(3);
|
||||
repository.findAll().count().subscribeWith(TestSubscriber.create()).awaitAndAssertNextValues(3L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteAllShouldRemoveEntities() {
|
||||
|
||||
repository.deleteAll().block();
|
||||
|
||||
TestSubscriber<Person> testSubscriber = TestSubscriber.subscribe(repository.findAll());
|
||||
|
||||
testSubscriber.await().assertComplete().assertValueCount(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteByIdShouldRemoveEntity() {
|
||||
|
||||
TestSubscriber<Void> testSubscriber = TestSubscriber.subscribe(repository.delete(dave.getId()));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues();
|
||||
|
||||
TestSubscriber<Person> verificationSubscriber = TestSubscriber.subscribe(repository.findOne(dave.getId()));
|
||||
|
||||
verificationSubscriber.await().assertComplete().assertNoValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteShouldRemoveEntity() {
|
||||
|
||||
TestSubscriber<Void> testSubscriber = TestSubscriber.subscribe(repository.delete(dave));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues();
|
||||
|
||||
TestSubscriber<Person> verificationSubscriber = TestSubscriber.subscribe(repository.findOne(dave.getId()));
|
||||
|
||||
verificationSubscriber.await().assertComplete().assertNoValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deleteIterableOfEntitiesShouldRemoveEntities() {
|
||||
|
||||
TestSubscriber<Void> testSubscriber = TestSubscriber.subscribe(repository.delete(Arrays.asList(dave, boyd)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues();
|
||||
|
||||
TestSubscriber<Person> verificationSubscriber = TestSubscriber.subscribe(repository.findOne(boyd.getId()));
|
||||
verificationSubscriber.await().assertComplete().assertNoValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DATACASS-335
|
||||
*/
|
||||
@Test
|
||||
public void deletePublisherOfEntitiesShouldRemoveEntities() {
|
||||
|
||||
TestSubscriber<Void> testSubscriber = TestSubscriber.subscribe(repository.delete(Flux.just(dave, boyd)));
|
||||
|
||||
testSubscriber.await().assertComplete().assertNoValues();
|
||||
|
||||
TestSubscriber<Person> verificationSubscriber = TestSubscriber.subscribe(repository.findOne(boyd.getId()));
|
||||
verificationSubscriber.await().assertComplete().assertNoValues();
|
||||
}
|
||||
|
||||
static interface PersonRepostitory extends ReactiveCassandraRepository<Person, String> {}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package org.springframework.data.cassandra.test.integration.repository.querymethods.derived;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assume.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -188,7 +187,7 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC
|
||||
@Test
|
||||
public void shouldFindByNumberOfChildren() throws Exception {
|
||||
|
||||
assumeThat(SpringVersion.getVersion(), startsWith("4.3"));
|
||||
assumeTrue(Version.parse(SpringVersion.getVersion()).isGreaterThanOrEqualTo(Version.parse("4.3")));
|
||||
|
||||
template.execute("CREATE INDEX IF NOT EXISTS person_number_of_children ON person (numberofchildren);");
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.cassandra.test.integration.support.FastShutdownNettyO
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.cassandra.config.SchemaAction;
|
||||
import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration;
|
||||
import org.springframework.data.cassandra.config.java.AbstractReactiveCassandraConfiguration;
|
||||
|
||||
import com.datastax.driver.core.NettyOptions;
|
||||
import com.datastax.driver.core.QueryOptions;
|
||||
@@ -40,7 +41,7 @@ import com.datastax.driver.core.QueryOptions;
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Configuration
|
||||
public class IntegrationTestConfig extends AbstractCassandraConfiguration {
|
||||
public class IntegrationTestConfig extends AbstractReactiveCassandraConfiguration {
|
||||
|
||||
public static final CassandraConnectionProperties PROPS = new CassandraConnectionProperties();
|
||||
public static final int PORT = PROPS.getCassandraPort();
|
||||
|
||||
1180
spring-data-cassandra/src/test/java/reactor/test/TestSubscriber.java
Normal file
1180
spring-data-cassandra/src/test/java/reactor/test/TestSubscriber.java
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user