org.springframework.data.cassandra.core.AsyncCassandraTemplate Maven / Gradle / Ivy
Show all versions of spring-data-cassandra Show documentation
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.cassandra.core;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.DriverException;
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.cql.AsyncResultSet;
import com.datastax.oss.driver.api.core.cql.BoundStatement;
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.datastax.oss.driver.api.querybuilder.delete.Delete;
import com.datastax.oss.driver.api.querybuilder.insert.Insert;
import com.datastax.oss.driver.api.querybuilder.insert.RegularInsert;
import com.datastax.oss.driver.api.querybuilder.select.Select;
import com.datastax.oss.driver.api.querybuilder.truncate.Truncate;
import com.datastax.oss.driver.api.querybuilder.update.Update;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.cassandra.SessionFactory;
import org.springframework.data.cassandra.core.EntityOperations.AdaptibleEntity;
import org.springframework.data.cassandra.core.convert.CassandraConverter;
import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
import org.springframework.data.cassandra.core.cql.*;
import org.springframework.data.cassandra.core.cql.session.DefaultSessionFactory;
import org.springframework.data.cassandra.core.cql.util.CassandraFutureAdapter;
import org.springframework.data.cassandra.core.cql.util.StatementBuilder;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver;
import org.springframework.data.cassandra.core.mapping.event.AfterConvertEvent;
import org.springframework.data.cassandra.core.mapping.event.AfterDeleteEvent;
import org.springframework.data.cassandra.core.mapping.event.AfterLoadEvent;
import org.springframework.data.cassandra.core.mapping.event.AfterSaveEvent;
import org.springframework.data.cassandra.core.mapping.event.BeforeConvertCallback;
import org.springframework.data.cassandra.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.cassandra.core.mapping.event.BeforeSaveCallback;
import org.springframework.data.cassandra.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.cassandra.core.mapping.event.CassandraMappingEvent;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.domain.Slice;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
/**
* Primary implementation of {@link AsyncCassandraOperations}. It simplifies the use of asynchronous Cassandra usage and
* helps to avoid common errors. It executes core Cassandra workflow. This class executes CQL queries or updates,
* initiating iteration over {@link ResultSet} and catching Cassandra exceptions and translating them to the generic,
* more informative exception hierarchy defined in the {@code org.springframework.dao} package.
*
* Can be used within a service implementation via direct instantiation with a {@link CqlSession} reference, or get
* prepared in an application context and given to services as bean reference.
*
* This class supports the use of prepared statements when enabling {@link #setUsePreparedStatements(boolean)}. All
* statements created by methods of this class (such as {@link #select(Query, Class)} or
* {@link #update(Query, org.springframework.data.cassandra.core.query.Update, Class)} will be executed as prepared
* statements. Also, statements accepted by methods (such as {@link #select(String, Class)} or
* {@link #select(Statement, Class) and others}) will be prepared prior to execution. Note that {@link Statement}
* objects passed to methods must be {@link SimpleStatement} so that these can be prepared.
*
* Note: The {@link CqlSession} 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
* @author John Blum
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations
* @since 2.0
*/
public class AsyncCassandraTemplate
implements AsyncCassandraOperations, ApplicationEventPublisherAware, ApplicationContextAware {
private final Log log = LogFactory.getLog(getClass());
private final AsyncCqlOperations cqlOperations;
private final CassandraConverter converter;
private final CqlExceptionTranslator exceptionTranslator;
private final EntityOperations entityOperations;
private final StatementFactory statementFactory;
private @Nullable ApplicationEventPublisher eventPublisher;
private @Nullable EntityCallbacks entityCallbacks;
private boolean usePreparedStatements = true;
/**
* Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link CqlSession} and a default
* {@link MappingCassandraConverter}.
*
* @param session {@link CqlSession} used to interact with Cassandra; must not be {@literal null}.
* @see CassandraConverter
* @see CqlSession
*/
public AsyncCassandraTemplate(CqlSession session) {
this(session, newConverter(session));
}
/**
* Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link CqlSession} and
* {@link CassandraConverter}.
*
* @param session {@link CqlSession} 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 CassandraConverter
* @see CqlSession
*/
public AsyncCassandraTemplate(CqlSession session, CassandraConverter converter) {
this(new DefaultSessionFactory(session), converter);
}
/**
* Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link SessionFactory} and
* {@link CassandraConverter}.
*
* @param sessionFactory {@link SessionFactory} 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 CassandraConverter
* @see CqlSession
*/
public AsyncCassandraTemplate(SessionFactory sessionFactory, CassandraConverter converter) {
this(new AsyncCqlTemplate(sessionFactory), converter);
}
/**
* Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link AsyncCqlTemplate} and
* {@link CassandraConverter}.
*
* @param asyncCqlTemplate {@link AsyncCqlTemplate} 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 CassandraConverter
* @see CqlSession
*/
public AsyncCassandraTemplate(AsyncCqlTemplate asyncCqlTemplate, CassandraConverter converter) {
Assert.notNull(asyncCqlTemplate, "AsyncCqlTemplate must not be null");
Assert.notNull(converter, "CassandraConverter must not be null");
this.converter = converter;
this.cqlOperations = asyncCqlTemplate;
this.entityOperations = new EntityOperations(converter);
this.exceptionTranslator = asyncCqlTemplate.getExceptionTranslator();
this.statementFactory = new StatementFactory(converter);
}
/* (non-Javadoc)
* @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher)
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}
/* (non-Javadoc)
* @see org.springframework.context.ApplicationContextAware(org.springframework.context.ApplicationContext)
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (entityCallbacks == null) {
setEntityCallbacks(EntityCallbacks.create(applicationContext));
}
}
/**
* Configure {@link EntityCallbacks} to pre-/post-process entities during persistence operations.
*
* @param entityCallbacks
*/
public void setEntityCallbacks(@Nullable EntityCallbacks entityCallbacks) {
this.entityCallbacks = entityCallbacks;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#getAsyncCqlOperations()
*/
@Override
public AsyncCqlOperations getAsyncCqlOperations() {
return this.cqlOperations;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#getConverter()
*/
@Override
public CassandraConverter getConverter() {
return this.converter;
}
/**
* Returns whether this instance is configured to use {@link PreparedStatement prepared statements}. If enabled
* (default), then all persistence methods (such as {@link #select}, {@link #update}, and others) will make use of
* prepared statements. Note that methods accepting a {@link Statement} must be called with {@link SimpleStatement}
* instances to participate in statement preparation.
*
* @return {@literal true} if prepared statements usage is enabled; {@literal false} otherwise.
* @since 3.2
*/
public boolean isUsePreparedStatements() {
return usePreparedStatements;
}
/**
* Enable/disable {@link PreparedStatement prepared statements} usage. If enabled (default), then all persistence
* methods (such as {@link #select}, {@link #update}, and others) will make use of prepared statements. Note that
* methods accepting a {@link Statement} must be called with {@link SimpleStatement} instances to participate in
* statement preparation.
*
* @param usePreparedStatements whether to use prepared statements.
* @since 3.2
*/
public void setUsePreparedStatements(boolean usePreparedStatements) {
this.usePreparedStatements = usePreparedStatements;
}
/**
* Returns the {@link EntityOperations} used to perform data access operations on an entity inside a Cassandra data
* source.
*
* @return the configured {@link EntityOperations} for this template.
* @see org.springframework.data.cassandra.core.EntityOperations
*/
protected EntityOperations getEntityOperations() {
return this.entityOperations;
}
/**
* Returns a reference to the configured {@link ProjectionFactory} used by this template to process CQL query
* projections.
*
* @return a reference to the configured {@link ProjectionFactory} used by this template to process CQL query
* projections.
* @see org.springframework.data.projection.SpelAwareProxyProjectionFactory
* @since 2.1
* @deprecated since 3.4, use {@link CassandraConverter#getProjectionFactory()} instead.
*/
@Deprecated
protected SpelAwareProxyProjectionFactory getProjectionFactory() {
return (SpelAwareProxyProjectionFactory) getConverter().getProjectionFactory();
}
private CassandraPersistentEntity getRequiredPersistentEntity(Class entityType) {
return getEntityOperations().getRequiredPersistentEntity(entityType);
}
/**
* Returns the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements.
*
* @return the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements.
* @see StatementFactory
* @since 2.1
*/
protected StatementFactory getStatementFactory() {
return this.statementFactory;
}
private CqlIdentifier getTableName(Class entityClass) {
return getEntityOperations().getTableName(entityClass);
}
// -------------------------------------------------------------------------
// Methods dealing with static CQL
// -------------------------------------------------------------------------
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(java.lang.String, java.lang.Class)
*/
@Override
public ListenableFuture> select(String cql, Class entityClass) {
Assert.hasText(cql, "CQL must not be empty");
return select(SimpleStatement.newInstance(cql), entityClass);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(java.lang.String, java.util.function.Consumer, java.lang.Class)
*/
@Override
public ListenableFuture select(String cql, Consumer entityConsumer, Class entityClass)
throws DataAccessException {
Assert.hasText(cql, "CQL must not be empty");
Assert.notNull(entityConsumer, "Entity Consumer must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return select(SimpleStatement.newInstance(cql), entityConsumer, entityClass);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(java.lang.String, java.lang.Class)
*/
@Override
public ListenableFuture selectOne(String cql, Class entityClass) {
Assert.hasText(cql, "CQL must not be empty");
Assert.notNull(entityClass, "Entity type must not be null");
return selectOne(SimpleStatement.newInstance(cql), entityClass);
}
// -------------------------------------------------------------------------
// Methods dealing with com.datastax.oss.driver.api.core.cql.Statement
// -------------------------------------------------------------------------
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(com.datastax.oss.driver.api.core.cql.Statement, java.lang.Class)
*/
@Override
public ListenableFuture execute(Statement statement) throws DataAccessException {
Assert.notNull(statement, "Statement must not be null");
return doQueryForResultSet(statement);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(com.datastax.oss.driver.api.core.cql.Statement, java.lang.Class)
*/
@Override
public ListenableFuture> select(Statement statement, Class entityClass) {
Assert.notNull(statement, "Statement must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
Function mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
return doQuery(statement, (row, rowNum) -> mapper.apply(row));
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(com.datastax.oss.driver.api.core.cql.Statement, java.util.function.Consumer, java.lang.Class)
*/
@Override
public ListenableFuture select(Statement statement, Consumer entityConsumer, Class entityClass)
throws DataAccessException {
Assert.notNull(statement, "Statement must not be null");
Assert.notNull(entityConsumer, "Entity Consumer must not be empty");
Assert.notNull(entityClass, "Entity type must not be null");
Function mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
return doQuery(statement, row -> {
entityConsumer.accept(mapper.apply(row));
});
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(com.datastax.oss.driver.api.core.cql.Statement, java.lang.Class)
*/
@Override
public ListenableFuture selectOne(Statement statement, Class entityClass) {
return new MappingListenableFutureAdapter<>(select(statement, entityClass),
list -> list.isEmpty() ? null : list.get(0));
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#slice(com.datastax.oss.driver.api.core.cql.Statement, java.lang.Class)
*/
@Override
public ListenableFuture> slice(Statement statement, Class entityClass) {
Assert.notNull(statement, "Statement must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
ListenableFuture resultSet = doQueryForResultSet(statement);
Function mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement));
return new MappingListenableFutureAdapter<>(resultSet,
rs -> EntityQueryUtils.readSlice(rs, (row, rowNum) -> mapper.apply(row), 0, getEffectivePageSize(statement)));
}
// -------------------------------------------------------------------------
// Methods dealing with org.springframework.data.cassandra.core.query.Query
// -------------------------------------------------------------------------
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.lang.Class)
*/
@Override
public ListenableFuture> select(Query query, Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return select(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass))
.build(), entityClass);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.util.function.Consumer, java.lang.Class)
*/
@Override
public ListenableFuture select(Query query, Consumer entityConsumer, Class entityClass)
throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityConsumer, "Entity Consumer must not be empty");
Assert.notNull(entityClass, "Entity type must not be null");
return select(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass))
.build(), entityConsumer,
entityClass);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(org.springframework.data.cassandra.core.query.Query, java.lang.Class)
*/
@Override
public ListenableFuture selectOne(Query query, Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return selectOne(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass))
.build(),
entityClass);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#slice(org.springframework.data.cassandra.core.query.Query, java.lang.Class)
*/
@Override
public ListenableFuture> slice(Query query, Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return slice(getStatementFactory().select(query, getRequiredPersistentEntity(entityClass))
.build(), entityClass);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(org.springframework.data.cassandra.core.query.Query, org.springframework.data.cassandra.core.query.Update, java.lang.Class)
*/
@Override
public ListenableFuture update(Query query, org.springframework.data.cassandra.core.query.Update update,
Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(update, "Update must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return doExecute(getStatementFactory().update(query, update, getRequiredPersistentEntity(entityClass))
.build(),
AsyncResultSet::wasApplied);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class)
*/
@Override
public ListenableFuture delete(Query query, Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return doDelete(query, entityClass, getTableName(entityClass));
}
private ListenableFuture doDelete(Query query, Class entityClass, CqlIdentifier tableName) {
StatementBuilder builder = getStatementFactory().delete(query, getRequiredPersistentEntity(entityClass),
tableName);
SimpleStatement delete = builder.build();
maybeEmitEvent(new BeforeDeleteEvent<>(delete, entityClass, tableName));
ListenableFuture future = doExecute(delete, AsyncResultSet::wasApplied);
future.addCallback(success -> maybeEmitEvent(new AfterDeleteEvent<>(delete, entityClass, tableName)), e -> {
});
return future;
}
// -------------------------------------------------------------------------
// Methods dealing with entities
// -------------------------------------------------------------------------
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#count(java.lang.Class)
*/
@Override
public ListenableFuture count(Class entityClass) {
Assert.notNull(entityClass, "Entity type must not be null");
return doCount(Query.empty(), entityClass, getTableName(entityClass));
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#count(org.springframework.data.cassandra.core.query.Query, java.lang.Class)
*/
@Override
public ListenableFuture count(Query query, Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
return doCount(query, entityClass, getTableName(entityClass));
}
ListenableFuture doCount(Query query, Class entityClass, CqlIdentifier tableName) {
StatementBuilder countStatement = getStatementFactory()
.count(query, getRequiredPersistentEntity(entityClass), tableName);
SimpleStatement statement = countStatement.build();
ListenableFuture result = doExecute(statement, it -> {
SingleColumnRowMapper mapper = SingleColumnRowMapper.newInstance(Long.class);
Row row = DataAccessUtils.requiredSingleResult(Streamable.of(it.currentPage())
.toList());
return mapper.mapRow(row, 0);
});
return new MappingListenableFutureAdapter<>(result, it -> it != null ? it : 0L);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#exists(java.lang.Object, java.lang.Class)
*/
@Override
public ListenableFuture exists(Object id, Class entityClass) {
Assert.notNull(id, "Id must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
CassandraPersistentEntity entity = getRequiredPersistentEntity(entityClass);
StatementBuilder select = getStatementFactory()
.selectOneById(id, entity, entity.getTableName());
return doExecute(select.build(), resultSet -> resultSet.one() != null);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#exists(org.springframework.data.cassandra.core.query.Query, java.lang.Class)
*/
@Override
public ListenableFuture exists(Query query, Class entityClass) throws DataAccessException {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
StatementBuilder select = getStatementFactory()
.select(query.limit(1), getRequiredPersistentEntity(entityClass), getTableName(entityClass));
return doExecute(select.build(), resultSet -> resultSet.one() != null);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOneById(java.lang.Object, java.lang.Class)
*/
@Override
public ListenableFuture selectOneById(Object id, Class entityClass) {
Assert.notNull(id, "Id must not be null");
Assert.notNull(entityClass, "Entity type must not be null");
CassandraPersistentEntity entity = getRequiredPersistentEntity(entityClass);
CqlIdentifier tableName = entity.getTableName();
StatementBuilder