/* * Copyright 2016-2017 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 reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cassandra.core.CqlProvider; import org.springframework.cassandra.core.QueryOptions; import org.springframework.cassandra.core.ReactiveCqlOperations; import org.springframework.cassandra.core.ReactiveCqlTemplate; import org.springframework.cassandra.core.ReactiveSessionCallback; import org.springframework.cassandra.core.WriteOptions; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.session.DefaultReactiveSessionFactory; import org.springframework.cassandra.core.session.ReactiveResultSet; import org.springframework.cassandra.core.session.ReactiveSession; import org.springframework.cassandra.core.session.ReactiveSessionFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.convert.QueryMapper; import org.springframework.data.cassandra.convert.UpdateMapper; import org.springframework.data.cassandra.core.query.Query; 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 org.reactivestreams.Publisher; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; import com.datastax.driver.core.exceptions.DriverException; 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; /** * 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 * @author John Blum * @see org.springframework.cassandra.core.ReactiveCqlOperations * @see org.springframework.data.cassandra.convert.CassandraConverter * @see org.springframework.data.cassandra.convert.QueryMapper * @see org.springframework.data.cassandra.convert.UpdateMapper * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations * @see com.datastax.driver.core.querybuilder.Delete * @see com.datastax.driver.core.querybuilder.Insert * @see com.datastax.driver.core.querybuilder.QueryBuilder * @see com.datastax.driver.core.querybuilder.Select * @see com.datastax.driver.core.querybuilder.Truncate * @see com.datastax.driver.core.querybuilder.Update * @since 2.0 */ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations { private final CassandraConverter converter; private final CassandraMappingContext mappingContext; private final ReactiveCqlOperations cqlOperations; private final StatementFactory statementFactory; /** * 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.cqlOperations = new ReactiveCqlTemplate(sessionFactory); this.mappingContext = this.converter.getMappingContext(); this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } /** * 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.cqlOperations = reactiveCqlOperations; this.mappingContext = this.converter.getMappingContext(); this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getConverter() */ @Override public CassandraConverter getConverter() { return this.converter; } /* (non-Javadoc) */ private static MappingCassandraConverter newConverter() { MappingCassandraConverter converter = new MappingCassandraConverter(); converter.afterPropertiesSet(); return converter; } /** * Returns the {@link CassandraMappingContext} used by this template to access mapping meta-data used to * store (map) objects to Cassandra tables. * * @return the {@link CassandraMappingContext} used by this template. * @see org.springframework.data.cassandra.mapping.CassandraMappingContext */ protected CassandraMappingContext getMappingContext() { return this.mappingContext; } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#getReactiveCqlOperations() */ @Override public ReactiveCqlOperations getReactiveCqlOperations() { return this.cqlOperations; } /** * 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 org.springframework.data.cassandra.core.StatementFactory */ protected StatementFactory getStatementFactory() { return this.statementFactory; } /* (non-Javadoc) */ private CqlIdentifier getTableName(Object entity) { return getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entity)).getTableName(); } // ------------------------------------------------------------------------- // 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 getReactiveCqlOperations().query(cql, (row, rowNum) -> getConverter().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 org.springframework.data.cassandra.core.query.Query // ------------------------------------------------------------------------- /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @Override public <T> Flux<T> select(Query query, Class<T> 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, getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#selectOne(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @Override public <T> Mono<T> selectOne(Query query, Class<T> 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, getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#update(org.springframework.data.cassandra.core.query.Query, org.springframework.data.cassandra.core.query.Update, java.lang.Class) */ @Override public Mono<Boolean> 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 getReactiveCqlOperations().execute(getStatementFactory().update(query, update, getMappingContext().getRequiredPersistentEntity(entityClass))); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ReactiveCassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @Override public Mono<Boolean> 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 getReactiveCqlOperations().execute(getStatementFactory().delete(query, getMappingContext().getRequiredPersistentEntity(entityClass))); } // ------------------------------------------------------------------------- // 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 = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().all().from(entity.getTableName().toCql()); getConverter().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 = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().from(entity.getTableName().toCql()); getConverter().write(id, select.where(), entity); return getReactiveCqlOperations().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(getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); return getReactiveCqlOperations().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"); Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, getConverter()); class InsertCallback implements ReactiveSessionCallback<T>, CqlProvider { @Override public Publisher<T> doInSession(ReactiveSession session) throws DriverException, DataAccessException { return session.execute(insert).flatMap( reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); } @Override public String getCql() { return insert.toString(); } } return getReactiveCqlOperations().execute(new InsertCallback()).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"); Update update = QueryUtils.createUpdateQuery(getTableName(entity).toCql(), entity, options, converter); class UpdateCallback implements ReactiveSessionCallback<T>, CqlProvider { @Override public Publisher<T> doInSession(ReactiveSession session) throws DriverException, DataAccessException { return session.execute(update) .flatMap(reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); } @Override public String getCql() { return update.toString(); } } return getReactiveCqlOperations().execute(new UpdateCallback()).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 = getMappingContext().getRequiredPersistentEntity(entityClass); Delete delete = QueryBuilder.delete().from(entity.getTableName().toCql()); getConverter().write(id, delete.where(), entity); return getReactiveCqlOperations().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"); Delete delete = QueryUtils.createDeleteQuery(getTableName(entity).toCql(), entity, options, getConverter()); class DeleteCallback implements ReactiveSessionCallback<T>, CqlProvider { @Override public Publisher<T> doInSession(ReactiveSession session) throws DriverException, DataAccessException { return session.execute(delete).flatMap( reactiveResultSet -> reactiveResultSet.wasApplied() ? Mono.just(entity) : Mono.empty()); } @Override public String getCql() { return delete.toString(); } } return getReactiveCqlOperations().execute(new DeleteCallback()).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( getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); return getReactiveCqlOperations().execute(truncate).then(); } }