From 1a3314a650e8bfda9049871c4382729f476262c6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Jan 2018 11:53:06 +0100 Subject: [PATCH] DATACASS-485 - Imperative Fluent Cassandra API. We now provide an alternative API for CassandraOperations that allows defining operations in a fluent way. FluentCassandraOperations reduces the number of methods and strips down the interface to a minimum while offering a more readable API. // select with filter query and projecting return type template.query(Person.class) .as(Jedi.class) .matching(query(where("firstname").is("luke"))) .all(); // insert template.insert(Person.class) .inTable(STAR_WARS) .one(luke); // update template.update(Person.class) .apply(update("firstname", "Han")) .matching(query(where("id").is("han-solo"))) .all(); // remove all matching template.delete(Jedi.class) .inTable(STAR_WARS) .matching(query(where("name").is("luke"))) .all(); --- .../core/CassandraAdminTemplate.java | 18 +- .../cassandra/core/CassandraOperations.java | 2 +- .../cassandra/core/CassandraTemplate.java | 147 ++++++-- .../core/ExecutableDeleteOperation.java | 107 ++++++ .../ExecutableDeleteOperationSupport.java | 106 ++++++ .../core/ExecutableInsertOperation.java | 111 ++++++ .../ExecutableInsertOperationSupport.java | 109 ++++++ .../core/ExecutableSelectOperation.java | 198 ++++++++++ .../ExecutableSelectOperationSupport.java | 180 ++++++++++ .../core/ExecutableUpdateOperation.java | 131 +++++++ .../ExecutableUpdateOperationSupport.java | 122 +++++++ .../core/FluentCassandraOperations.java | 26 ++ .../data/cassandra/core/StatementFactory.java | 85 ++++- ...ExecutableDeleteOperationSupportTests.java | 100 ++++++ ...ExecutableInsertOperationSupportTests.java | 112 ++++++ ...ExecutableSelectOperationSupportTests.java | 337 ++++++++++++++++++ ...ExecutableUpdateOperationSupportTests.java | 121 +++++++ 17 files changed, 1972 insertions(+), 40 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperation.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupport.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperation.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupport.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperation.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupport.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperation.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupport.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/FluentCassandraOperations.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupportTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupportTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupportTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupportTests.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java index e9619ab2e..f4c119f0d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java @@ -64,8 +64,7 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand super(sessionFactory, converter); } - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#createTable(boolean, org.springframework.data.cassandra.core.cql.CqlIdentifier, java.lang.Class, java.util.Map) */ @Override @@ -84,8 +83,7 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand dropTable(getTableName(entityClass)); } - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#dropTable(org.springframework.data.cassandra.core.cql.CqlIdentifier) */ @Override @@ -93,14 +91,16 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand dropTable(false, tableName); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraAdminOperations#dropTable(boolean, CqlIdentifier) + */ @Override public void dropTable(boolean ifExists, CqlIdentifier tableName) { getCqlOperations() .execute(DropTableCqlGenerator.toCql(DropTableSpecification.dropTable(tableName).ifExists(ifExists))); } - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#dropUserType(org.springframework.data.cassandra.core.cql.CqlIdentifier) */ @Override @@ -111,8 +111,7 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand getCqlOperations().execute(DropUserTypeCqlGenerator.toCql(DropUserTypeSpecification.dropType(typeName))); } - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#getTableMetadata(java.lang.String, org.springframework.data.cassandra.core.cql.CqlIdentifier) */ @Override @@ -125,8 +124,7 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand .getCluster().getMetadata().getKeyspace(keyspace).getTable(tableName.toCql()))); } - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraAdminOperations#getKeyspaceMetadata() */ @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java index 39d87eb18..e1f9ad3b5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraOperations.java @@ -47,7 +47,7 @@ import com.datastax.driver.core.Statement; * @see InsertOptions * @see UpdateOptions */ -public interface CassandraOperations { +public interface CassandraOperations extends FluentCassandraOperations { /** * Returns a new {@link CassandraBatchOperations}. Each {@link CassandraBatchOperations} instance can be executed only diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index 39b185639..2d49804e3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -15,13 +15,14 @@ */ package org.springframework.data.cassandra.core; -import java.util.List; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - import lombok.NonNull; import lombok.Value; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.core.convert.CassandraConverter; @@ -35,6 +36,7 @@ import org.springframework.data.cassandra.core.cql.CqlProvider; import org.springframework.data.cassandra.core.cql.CqlTemplate; import org.springframework.data.cassandra.core.cql.QueryOptions; import org.springframework.data.cassandra.core.cql.SessionCallback; +import org.springframework.data.cassandra.core.cql.WriteOptions; import org.springframework.data.cassandra.core.cql.session.DefaultSessionFactory; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; @@ -42,11 +44,13 @@ import org.springframework.data.cassandra.core.mapping.CassandraPersistentProper import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.domain.Slice; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; @@ -85,6 +89,8 @@ public class CassandraTemplate implements CassandraOperations { private final StatementFactory statementFactory; + private final SpelAwareProxyProjectionFactory projectionFactory; + /** * Creates an instance of {@link CassandraTemplate} initialized with the given {@link Session} and a default * {@link MappingCassandraConverter}. @@ -144,6 +150,7 @@ public class CassandraTemplate implements CassandraOperations { this.cqlOperations = cqlOperations; this.mappingContext = converter.getMappingContext(); this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); + this.projectionFactory = new SpelAwareProxyProjectionFactory(); } /* (non-Javadoc) @@ -172,8 +179,8 @@ public class CassandraTemplate implements CassandraOperations { } /** - * Returns the {@link CassandraMappingContext} used by this template to access mapping meta-data - * in order to store (map) object to Cassandra tables. + * Returns the {@link CassandraMappingContext} used by this template to access mapping meta-data in order to store + * (map) object to Cassandra tables. * * @return the {@link CassandraMappingContext} used by this template. * @see org.springframework.data.cassandra.core.mapping.CassandraMappingContext @@ -264,7 +271,9 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return getCqlOperations().query(statement, (row, rowNum) -> getConverter().read(entityClass, row)); + Function mapper = getMapper(entityClass, entityClass); + + return getCqlOperations().query(statement, (row, rowNum) -> mapper.apply(row)); } /* (non-Javadoc) @@ -278,10 +287,9 @@ public class CassandraTemplate implements CassandraOperations { ResultSet resultSet = getCqlOperations().queryForResultSet(statement); - CassandraConverter converter = getConverter(); + Function mapper = getMapper(entityClass, entityClass); - return QueryUtils.readSlice(resultSet, (row, rowNum) -> converter.read(entityClass, row), 0, - getEffectiveFetchSize(statement)); + return QueryUtils.readSlice(resultSet, (row, rowNum) -> mapper.apply(row), 0, getEffectiveFetchSize(statement)); } /* (non-Javadoc) @@ -294,7 +302,7 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(entityClass, "Entity type must not be null"); return StreamSupport.stream(getCqlOperations().queryForResultSet(statement).spliterator(), false) - .map(row -> getConverter().read(entityClass, row)); + .map(getMapper(entityClass, entityClass)); } /* (non-Javadoc) @@ -318,8 +326,16 @@ public class CassandraTemplate implements CassandraOperations { 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); + return doSelect(query, entityClass, getTableName(entityClass), entityClass); + } + + List doSelect(Query query, Class entityClass, CqlIdentifier tableName, Class returnType) { + + Function mapper = getMapper(entityClass, returnType); + + RegularStatement select = getStatementFactory().select(query, + getMappingContext().getRequiredPersistentEntity(entityClass), tableName); + return getCqlOperations().query(select, (row, rowNum) -> mapper.apply(row)); } /* (non-Javadoc) @@ -344,8 +360,16 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return stream(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), - entityClass); + return doStream(query, entityClass, getTableName(entityClass), entityClass); + } + + Stream doStream(Query query, Class entityClass, CqlIdentifier tableName, Class returnType) { + + RegularStatement statement = getStatementFactory().select(query, + getMappingContext().getRequiredPersistentEntity(entityClass), tableName); + + return StreamSupport.stream(getCqlOperations().queryForResultSet(statement).spliterator(), false) + .map(getMapper(entityClass, returnType)); } /* (non-Javadoc) @@ -374,6 +398,14 @@ public class CassandraTemplate implements CassandraOperations { getStatementFactory().update(query, update, getMappingContext().getRequiredPersistentEntity(entityClass))); } + WriteResult doUpdate(Query query, org.springframework.data.cassandra.core.query.Update update, Class entityClass, + CqlIdentifier tableName) { + + RegularStatement statement = getStatementFactory().update(query, update, + getMappingContext().getRequiredPersistentEntity(entityClass), tableName); + return getCqlOperations().execute(new StatementCallback(statement)); + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @@ -383,8 +415,14 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return getCqlOperations() - .execute(getStatementFactory().delete(query, getMappingContext().getRequiredPersistentEntity(entityClass))); + return doDelete(query, entityClass, getTableName(entityClass)).wasApplied(); + } + + WriteResult doDelete(Query query, Class entityClass, CqlIdentifier tableName) { + + RegularStatement delete = getStatementFactory().delete(query, getRequiredPersistentEntity(entityClass), tableName); + + return getCqlOperations().execute(new StatementCallback(delete)); } // ------------------------------------------------------------------------- @@ -415,7 +453,12 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - RegularStatement count = getStatementFactory().count(query, getRequiredPersistentEntity(entityClass)); + return doCount(query, entityClass, getTableName(entityClass)); + } + + long doCount(Query query, Class entityClass, CqlIdentifier tableName) { + + RegularStatement count = getStatementFactory().count(query, getRequiredPersistentEntity(entityClass), tableName); Long result = getCqlOperations().queryForObject(count, Long.class); @@ -433,7 +476,7 @@ public class CassandraTemplate implements CassandraOperations { CassandraPersistentEntity entity = getRequiredPersistentEntity(entityClass); - Select select = QueryBuilder.select().from(entity.getTableName().toCql()); + Select select = QueryBuilder.select().from(getTableName(entityClass).toCql()); getConverter().write(id, select.where(), entity); @@ -449,8 +492,13 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - RegularStatement select = getStatementFactory() - .select(query.limit(1), getRequiredPersistentEntity(entityClass)); + return doExists(query, entityClass, getTableName(entityClass)); + } + + boolean doExists(Query query, Class entityClass, CqlIdentifier tableName) { + + RegularStatement select = getStatementFactory().select(query.limit(1), getRequiredPersistentEntity(entityClass), + tableName); return getCqlOperations().queryForResultSet(select).iterator().hasNext(); } @@ -490,7 +538,13 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(entity, "Entity must not be null"); Assert.notNull(options, "InsertOptions must not be null"); - Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, getConverter()); + CqlIdentifier tableName = getTableName(entity); + return doInsert(entity, options, tableName); + } + + WriteResult doInsert(Object entity, WriteOptions options, CqlIdentifier tableName) { + + Insert insert = QueryUtils.createInsertQuery(tableName.toCql(), entity, options, getConverter()); // noinspection ConstantConditions return getCqlOperations().execute(new StatementCallback(insert)); @@ -573,10 +627,59 @@ public class CassandraTemplate implements CassandraOperations { getCqlOperations().execute(truncate); } + // ------------------------------------------------------------------------- + // Fluent API entry points + // ------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation#query(java.lang.Class) + */ + @Override + public ExecutableSelect query(Class domainType) { + return new ExecutableSelectOperationSupport(this).query(domainType); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation#insert(java.lang.Class) + */ + @Override + public ExecutableInsert insert(Class domainType) { + return new ExecutableInsertOperationSupport(this).insert(domainType); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation#update(java.lang.Class) + */ + @Override + public ExecutableUpdate update(Class domainType) { + return new ExecutableUpdateOperationSupport(this).update(domainType); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation#remove(java.lang.Class) + */ + @Override + public ExecutableDelete delete(Class domainType) { + return new ExecutableDeleteOperationSupport(this).delete(domainType); + } + // ------------------------------------------------------------------------- // Implementation hooks and helper methods // ------------------------------------------------------------------------- + @SuppressWarnings("unchecked") + private Function getMapper(Class entityType, Class targetType) { + + Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType : targetType; + + return row -> { + + Object source = getConverter().read(typeToRead, row); + + return (T) (targetType.isInterface() ? projectionFactory.createProjection(targetType, source) : source); + }; + } + private int getConfiguredFetchSize(Session session) { return session.getCluster().getConfiguration().getQueryOptions().getFetchSize(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperation.java new file mode 100644 index 000000000..9c130db43 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperation.java @@ -0,0 +1,107 @@ +/* + * Copyright 2018 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.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.Query; + +/** + * {@link ExecutableDeleteOperation} allows creation and execution of Cassandra {@code DELETE} operations in a fluent + * API style. + *

+ * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching} into the + * Cassandra specific representation. The table to operate on is by default derived from the initial + * {@literal domainType} and can be defined there via {@link org.springframework.data.cassandra.core.mapping.Table}. + * Using {@code inTable} allows to override the table name for the execution. + * + *

+ *     
+ *         delete(Jedi.class)
+ *             .inTable("star_wars")
+ *             .matching(query(where("firstname").is("luke")))
+ *             .all();
+ *     
+ * 
+ * + * @author Mark Paluch + * @since 2.1 + */ +public interface ExecutableDeleteOperation { + + /** + * Start creating a {@code DELETE} operation for the given {@literal domainType}. + * + * @param domainType must not be {@literal null}. + * @return new instance of {@link ExecutableDelete}. + * @throws IllegalArgumentException if domainType is {@literal null}. + */ + ExecutableDelete delete(Class domainType); + + /** + * Table override (optional). + */ + interface DeleteWithTable { + + /** + * Explicitly set the name of the table to perform the query on. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null} or empty. + * @return new instance of {@link DeleteWithTable}. + * @throws IllegalArgumentException if {@code table} is {@literal null} or empty. + */ + DeleteWithQuery inTable(String table); + + /** + * Explicitly set the name of the table to perform the query on. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null}. + * @return new instance of {@link DeleteWithTable}. + * @throws IllegalArgumentException if {@link CqlIdentifier} is {@literal null}. + */ + DeleteWithQuery inTable(CqlIdentifier table); + } + + interface TerminatingDelete { + + /** + * Remove all matching rows. + * + * @return the {@link WriteResult}. Never {@literal null}. + */ + WriteResult all(); + } + + interface DeleteWithQuery { + + /** + * Define the query filtering elements. + * + * @param query must not be {@literal null}. + * @return new instance of {@link TerminatingDelete}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + TerminatingDelete matching(Query query); + } + + /** + * {@link ExecutableDelete} provides methods for constructing {@code DELETE} operations in a fluent way. + */ + interface ExecutableDelete extends DeleteWithTable, DeleteWithQuery {} +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupport.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupport.java new file mode 100644 index 000000000..81d8936be --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupport.java @@ -0,0 +1,106 @@ +/* + * Copyright 2018 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 lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ExecutableDeleteOperation}. + * + * @author Mark Paluch + * @since 2.1 + */ +@RequiredArgsConstructor +class ExecutableDeleteOperationSupport implements ExecutableDeleteOperation { + + private final @NonNull CassandraTemplate template; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation#remove(java.lang.Class) + */ + @Override + public ExecutableDelete delete(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null!"); + + return new ExecutableDeleteSupport(template, domainType, Query.empty(), null); + } + + @RequiredArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) + static class ExecutableDeleteSupport implements ExecutableDelete, DeleteWithTable, TerminatingDelete { + + @NonNull CassandraTemplate template; + + @NonNull Class domainType; + + @NonNull Query query; + + @Nullable CqlIdentifier tableName; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation.DeleteWithTable#inTable(java.lang.String) + */ + @Override + public DeleteWithQuery inTable(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty"); + + return new ExecutableDeleteSupport(template, domainType, query, CqlIdentifier.of(tableName)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation.DeleteWithTable#inTable(org.springframework.data.cassandra.core.cql.CqlIdentifier) + */ + @Override + public DeleteWithQuery inTable(CqlIdentifier tableName) { + + Assert.notNull(tableName, "Table name must not be null"); + + return new ExecutableDeleteSupport(template, domainType, query, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation.DeleteWithQuery#matching(org.springframework.data.cassandra.core.query.Query) + */ + @Override + public TerminatingDelete matching(Query query) { + + Assert.notNull(query, "Query must not be null!"); + + return new ExecutableDeleteSupport(template, domainType, query, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation.TerminatingDelete#all() + */ + public WriteResult all() { + return template.doDelete(query, domainType, getTableName()); + } + + private CqlIdentifier getTableName() { + return tableName != null ? tableName : template.getTableName(domainType); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperation.java new file mode 100644 index 000000000..f504b648e --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperation.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018 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.springframework.data.cassandra.core.cql.CqlIdentifier; + +/** + * {@link ExecutableInsertOperation} allows creation and execution of Cassandra {@code INSERT} insert operations in a + * fluent API style. + *

+ * The table to operate on is by default derived from the initial {@literal domainType} and can be defined there via + * {@link org.springframework.data.cassandra.core.mapping.Table}. Using {@code inTable} allows to override the + * collection name for the execution. + * + *

+ *     
+ *         insert(Jedi.class)
+ *             .inTable("star_wars")
+ *             .one(luke);
+ *     
+ * 
+ * + * @author Mark Paluch + * @since 2.1 + */ +public interface ExecutableInsertOperation { + + /** + * Start creating an {@code INSERT} operation for given {@literal domainType}. + * + * @param domainType must not be {@literal null}. + * @return new instance of {@link ExecutableInsert}. + * @throws IllegalArgumentException if domainType is {@literal null}. + */ + ExecutableInsert insert(Class domainType); + + /** + * Trigger insert execution by calling one of the terminating methods. + */ + interface TerminatingInsert { + + /** + * Insert exactly one object. + * + * @param object must not be {@literal null}. + * @throws IllegalArgumentException if object is {@literal null}. + */ + WriteResult one(T object); + } + + /** + * Collection override (optional). + */ + interface InsertWithTable extends InsertWithOptions { + + /** + * Explicitly set the name of the table. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null} or empty. + * @return new instance of {@link TerminatingInsert}. + * @throws IllegalArgumentException if {@code table} is {@literal null} or empty. + */ + InsertWithOptions inTable(String table); + + /** + * Explicitly set the name of the table. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null}. + * @return new instance of {@link TerminatingInsert}. + * @throws IllegalArgumentException if {@link CqlIdentifier} is {@literal null}. + */ + InsertWithOptions inTable(CqlIdentifier table); + } + + /** + * Apply {@link InsertOptions} (optional). + */ + interface InsertWithOptions extends TerminatingInsert { + + /** + * Set insert options. + * + * @param insertOptions insertOptions not be {@literal null}. + * @return new instance of {@link TerminatingInsert}. + * @throws IllegalArgumentException if {@link InsertOptions} is {@literal null}. + */ + TerminatingInsert withOptions(InsertOptions insertOptions); + } + + /** + * {@link ExecutableInsert} provides methods for constructing {@code INSERT} operations in a fluent way. + */ + interface ExecutableInsert extends TerminatingInsert, InsertWithTable, InsertWithOptions {} +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupport.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupport.java new file mode 100644 index 000000000..3426dce05 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupport.java @@ -0,0 +1,109 @@ +/* + * Copyright 2018 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 lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ExecutableInsertOperation}. + * + * @author Mark Paluch + * @since 2.1 + */ +@RequiredArgsConstructor +class ExecutableInsertOperationSupport implements ExecutableInsertOperation { + + private final @NonNull CassandraTemplate template; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation#insert(java.lang.Class) + */ + @Override + public ExecutableInsert insert(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null!"); + + return new ExecutableInsertSupport<>(template, domainType, null, InsertOptions.empty()); + } + + @RequiredArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) + static class ExecutableInsertSupport implements ExecutableInsert { + + @NonNull CassandraTemplate template; + + @NonNull Class domainType; + + @Nullable CqlIdentifier tableName; + + @NonNull InsertOptions insertOptions; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation.InsertWithTable#inTable(java.lang.String) + */ + @Override + public InsertWithOptions inTable(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty"); + + return new ExecutableInsertSupport<>(template, domainType, CqlIdentifier.of(tableName), insertOptions); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation.InsertWithTable#inTable(org.springframework.data.cassandra.core.cql.CqlIdentifier) + */ + @Override + public InsertWithOptions inTable(CqlIdentifier tableName) { + + Assert.notNull(tableName, "Table name must not be null"); + + return new ExecutableInsertSupport<>(template, domainType, tableName, insertOptions); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation.InsertWithOptions#withOptions(org.springframework.data.cassandra.core.InsertOptions) + */ + @Override + public TerminatingInsert withOptions(InsertOptions insertOptions) { + + Assert.notNull(insertOptions, "InsertOptions must not be null"); + + return new ExecutableInsertSupport<>(template, domainType, tableName, insertOptions); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation.TerminatingInsert#one(java.lang.Object) + */ + @Override + public WriteResult one(T object) { + + Assert.notNull(object, "Object must not be null!"); + + return template.doInsert(object, insertOptions, getTableName()); + } + + private CqlIdentifier getTableName() { + return tableName != null ? tableName : template.getTableName(domainType); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperation.java new file mode 100644 index 000000000..d57a7fa70 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperation.java @@ -0,0 +1,198 @@ +/* + * Copyright 2018 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 java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.lang.Nullable; + +/** + * {@link ExecutableSelectOperation} allows creation and execution of Cassandra {@code SELECT} operations in a fluent + * API style. + *

+ * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching} into the + * Cassandra specific representation. By default, the originating {@literal domainType} is also used for mapping back + * the result from the {@link com.datastax.driver.core.Row}. However, it is possible to define an different + * {@literal returnType} via {@code as} to mapping the result. + *

+ * The table to operate on is by default derived from the initial {@literal domainType} and can be defined there via + * {@link org.springframework.data.cassandra.core.mapping.Table}. Using {@code inTable} allows to override the table + * name for the execution. + * + *

+ *     
+ *         query(Human.class)
+ *             .inTable("star_wars")
+ *             .as(Jedi.class)
+ *             .matching(query(where("firstname").is("luke")))
+ *             .all();
+ *     
+ * 
+ * + * @author Mark Paluch + * @since 2.1 + */ +public interface ExecutableSelectOperation { + + /** + * Start creating a {@code SELECT} operation for the given {@literal domainType}. + * + * @param domainType must not be {@literal null}. + * @return new instance of {@link ExecutableSelect}. + * @throws IllegalArgumentException if domainType is {@literal null}. + */ + ExecutableSelect query(Class domainType); + + /** + * Trigger {@code SELECT} execution by calling one of the terminating methods. + */ + interface TerminatingSelect { + + /** + * Get exactly zero or one result. + * + * @return {@link Optional#empty()} if no match found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + default Optional one() { + return Optional.ofNullable(oneValue()); + } + + /** + * Get exactly zero or one result. + * + * @return {@literal null} if no match found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + @Nullable + T oneValue(); + + /** + * Get the first or no result. + * + * @return {@link Optional#empty()} if no match found. + */ + default Optional first() { + return Optional.ofNullable(firstValue()); + } + + /** + * Get the first or no result. + * + * @return {@literal null} if no match found. + */ + @Nullable + T firstValue(); + + /** + * Get all matching elements. + * + * @return never {@literal null}. + */ + List all(); + + /** + * Stream all matching elements. + * + * @return a {@link Stream} that wraps the a Cassandra {@link com.datastax.driver.core.ResultSet} that needs to be + * closed. Never {@literal null}. + */ + Stream stream(); + + /** + * Get the number of matching elements. + * + * @return total number of matching elements. + */ + long count(); + + /** + * Check for the presence of matching elements. + * + * @return {@literal true} if at least one matching element exists. + */ + boolean exists(); + } + + /** + * Terminating operations invoking the actual query execution. + */ + interface SelectWithQuery extends TerminatingSelect { + + /** + * Set the filter query to be used. + * + * @param query must not be {@literal null}. + * @return new instance of {@link TerminatingSelect}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + TerminatingSelect matching(Query query); + } + + /** + * Table override (Optional). + */ + interface SelectWithTable extends SelectWithQuery { + + /** + * Explicitly set the name of the table to perform the query on. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null} or empty. + * @return new instance of {@link SelectWithProjection}. + * @throws IllegalArgumentException if {@code table} is {@literal null} or empty. + */ + SelectWithProjection inTable(String table); + + /** + * Explicitly set the name of the table to perform the query on. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null}. + * @return new instance of {@link SelectWithProjection}. + * @throws IllegalArgumentException if {@link CqlIdentifier} is {@literal null}. + */ + SelectWithProjection inTable(CqlIdentifier table); + } + + /** + * Result type override (Optional). + */ + interface SelectWithProjection extends SelectWithQuery { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link SelectWithProjection}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + SelectWithQuery as(Class resultType); + } + + /** + * {@link ExecutableSelect} provides methods for constructing {@code SELECT} operations in a fluent way. + */ + interface ExecutableSelect extends SelectWithTable, SelectWithProjection {} +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupport.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupport.java new file mode 100644 index 000000000..506f6f062 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupport.java @@ -0,0 +1,180 @@ +/* + * Copyright 2018 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 lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of {@link ExecutableSelectOperation}. + * + * @author Mark Paluch + * @since 2.1 + */ +@RequiredArgsConstructor +class ExecutableSelectOperationSupport implements ExecutableSelectOperation { + + private final @NonNull CassandraTemplate template; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation#query(java.lang.Class) + */ + @Override + public ExecutableSelect query(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null!"); + + return new ExecutableSelectSupport<>(template, domainType, domainType, Query.empty(), null); + } + + @RequiredArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) + static class ExecutableSelectSupport + implements ExecutableSelect, SelectWithTable, SelectWithProjection, SelectWithQuery { + + @NonNull CassandraTemplate template; + + @NonNull Class domainType; + + @NonNull Class returnType; + + @NonNull Query query; + + @Nullable CqlIdentifier tableName; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.SelectWithTable#inTable(java.lang.String) + */ + @Override + public SelectWithProjection inTable(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty!"); + + return new ExecutableSelectSupport<>(template, domainType, returnType, query, CqlIdentifier.of(tableName)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.SelectWithTable#inTable(org.springframework.data.cassandra.core.cql.CqlIdentifier) + */ + @Override + public SelectWithProjection inTable(CqlIdentifier tableName) { + + Assert.notNull(tableName, "Table name must not be null!"); + + return new ExecutableSelectSupport<>(template, domainType, returnType, query, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.SelectWithProjection#as(java.lang.Class) + */ + @Override + public SelectWithQuery as(Class returnType) { + + Assert.notNull(returnType, "ReturnType must not be null!"); + + return new ExecutableSelectSupport<>(template, domainType, returnType, query, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.SelectWithQuery#matching(org.springframework.data.cassandra.core.query.Query) + */ + @Override + public TerminatingSelect matching(Query query) { + + Assert.notNull(query, "Query must not be null!"); + + return new ExecutableSelectSupport<>(template, domainType, returnType, query, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#oneValue() + */ + @Override + public T oneValue() { + + List result = template.doSelect(query.limit(2), domainType, getTableName(), returnType); + + if (ObjectUtils.isEmpty(result)) { + return null; + } + + if (result.size() > 1) { + throw new IncorrectResultSizeDataAccessException("Query " + query + " returned non unique result.", 1); + } + + return result.iterator().next(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#firstValue() + */ + @Override + public T firstValue() { + + List result = template.doSelect(query.limit(1), domainType, getTableName(), returnType); + + return ObjectUtils.isEmpty(result) ? null : result.iterator().next(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#all() + */ + @Override + public List all() { + return template.doSelect(query, domainType, getTableName(), returnType); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#stream() + */ + @Override + public Stream stream() { + return template.doStream(query, domainType, getTableName(), returnType); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#count() + */ + @Override + public long count() { + return template.doCount(query, domainType, getTableName()); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#exists() + */ + @Override + public boolean exists() { + return template.doExists(query, domainType, getTableName()); + } + + private CqlIdentifier getTableName() { + return tableName != null ? tableName : template.getTableName(domainType); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperation.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperation.java new file mode 100644 index 000000000..213c35de2 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperation.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018 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.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; + +/** + * {@link ExecutableUpdateOperation} allows creation and execution of Cassandra {@code UPDATE} operation in a fluent API + * style. + *

+ * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as + * the {@link Update} via {@code apply} into the Cassandra specific representations. The table to operate on is by + * default derived from the initial {@literal domainType} and can be defined there via + * {@link org.springframework.data.cassandra.core.mapping.Table}. Using {@code inTable} allows to override the table + * name for the execution. + * + *

+ *     
+ *         update(Jedi.class)
+ *             .inTable("star_wars")
+ *             .matching(query(where("firstname").is("luke")))
+ *             .apply(update("lastname", "skywalker"))
+ *             .all();
+ *     
+ * 
+ * + * @author Mark Paluch + * @since 2.1 + */ +public interface ExecutableUpdateOperation { + + /** + * Start creating an {@code UPDATE} operation for the given {@literal domainType}. + * + * @param domainType must not be {@literal null}. + * @return new instance of {@link ExecutableUpdate}. + * @throws IllegalArgumentException if domainType is {@literal null}. + */ + ExecutableUpdate update(Class domainType); + + /** + * Declare the {@link Update} to apply. + */ + interface UpdateWithUpdate { + + /** + * Set the {@link Update} to be applied. + * + * @param update must not be {@literal null}. + * @return new instance of {@link TerminatingUpdate}. + * @throws IllegalArgumentException if update is {@literal null}. + */ + TerminatingUpdate apply(Update update); + } + + /** + * Explicitly define the name of the table to perform operation in. + */ + interface UpdateWithTable { + + /** + * Explicitly set the name of the table to perform the query on. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null} or empty. + * @return new instance of {@link UpdateWithTable}. + * @throws IllegalArgumentException if {@code table} is {@literal null} or empty. + */ + UpdateWithQuery inTable(String table); + + /** + * Explicitly set the name of the table to perform the query on. + *

+ * Skip this step to use the default table derived from the domain type. + * + * @param table must not be {@literal null}. + * @return new instance of {@link UpdateWithTable}. + * @throws IllegalArgumentException if {@link CqlIdentifier} is {@literal null}. + */ + UpdateWithQuery inTable(CqlIdentifier table); + } + + /** + * Define a filter query for the {@link Update}. + */ + interface UpdateWithQuery { + + /** + * Filter documents by given {@literal query}. + * + * @param query must not be {@literal null}. + * @return new instance of {@link UpdateWithQuery}. + * @throws IllegalArgumentException if query is {@literal null}. + */ + UpdateWithUpdate matching(Query query); + } + + /** + * Trigger update execution by calling one of the terminating methods. + */ + interface TerminatingUpdate { + + /** + * Update all matching rows in the table. + * + * @return never {@literal null}. + */ + WriteResult all(); + } + + /** + * {@link ExecutableUpdate} provides methods for constructing {@code UPDATE} operations in a fluent way. + */ + interface ExecutableUpdate extends UpdateWithTable, UpdateWithQuery {} +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupport.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupport.java new file mode 100644 index 000000000..791bf4f88 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupport.java @@ -0,0 +1,122 @@ +/* + * Copyright 2018 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 lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; + +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ExecutableUpdateOperation}. + * + * @author Mark Paluch + * @since 2.1 + */ +@RequiredArgsConstructor +class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { + + private final @NonNull CassandraTemplate template; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation#update(java.lang.Class) + */ + @Override + public ExecutableUpdate update(Class domainType) { + + Assert.notNull(domainType, "DomainType must not be null!"); + + return new ExecutableUpdateSupport<>(template, domainType, Query.empty(), null, null); + } + + @RequiredArgsConstructor + @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) + static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithTable, UpdateWithQuery, + UpdateWithUpdate, TerminatingUpdate { + + @NonNull CassandraTemplate template; + + @NonNull Class domainType; + + @NonNull Query query; + + @Nullable Update update; + + @Nullable CqlIdentifier tableName; + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.UpdateWithUpdate#apply(org.springframework.data.cassandra.core.query.Update) + */ + @Override + public TerminatingUpdate apply(Update update) { + + Assert.notNull(update, "Update must not be null!"); + + return new ExecutableUpdateSupport<>(template, domainType, query, update, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.UpdateWithTable#inTable(java.lang.String) + */ + @Override + public UpdateWithQuery inTable(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty!"); + + return new ExecutableUpdateSupport<>(template, domainType, query, update, CqlIdentifier.of(tableName)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.UpdateWithTable#inTable(org.springframework.data.cassandra.core.cql.CqlIdentifier) + */ + @Override + public UpdateWithQuery inTable(CqlIdentifier tableName) { + + Assert.notNull(tableName, "Table name must not be null!"); + + return new ExecutableUpdateSupport<>(template, domainType, query, update, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.UpdateWithQuery#matching(org.springframework.data.cassandra.core.query.Query) + */ + @Override + public UpdateWithUpdate matching(Query query) { + + Assert.notNull(query, "Query must not be null!"); + + return new ExecutableUpdateSupport<>(template, domainType, query, update, tableName); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.TerminatingUpdate#all() + */ + @Override + public WriteResult all() { + return template.doUpdate(query, update, domainType, getTableName()); + } + + private CqlIdentifier getTableName() { + return tableName != null ? tableName : template.getTableName(domainType); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/FluentCassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/FluentCassandraOperations.java new file mode 100644 index 000000000..473305df4 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/FluentCassandraOperations.java @@ -0,0 +1,26 @@ +/* + * Copyright 2018 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; + +/** + * Stripped down interface providing access to a fluent API that specifies a basic set of Cassandra operations. + * + * @author Mark Paluch + * @since 2.1 + * @see CassandraOperations + */ +public interface FluentCassandraOperations extends ExecutableSelectOperation, ExecutableInsertOperation, + ExecutableUpdateOperation, ExecutableDeleteOperation {} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java index 84b62c700..765d137f3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java @@ -138,11 +138,25 @@ public class StatementFactory { Assert.notNull(query, "Query must not be null"); Assert.notNull(entity, "Entity must not be null"); + return count(query, entity, entity.getTableName()); + } + + /** + * Create a {@literal COUNT} statement by mapping {@link Query} to {@link Select}. + * + * @param query user-defined count {@link Query} to execute; must not be {@literal null}. + * @param entity {@link CassandraPersistentEntity entity} to count; must not be {@literal null}. + * @param tableName must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + * @since 2.1 + */ + public RegularStatement count(Query query, CassandraPersistentEntity entity, CqlIdentifier tableName) { + Filter filter = getQueryMapper().getMappedObject(query, entity); List selectors = Collections.singletonList(FunctionCall.from("COUNT", 1L)); - return createSelect(query, entity, filter, selectors); + return createSelect(query, entity, filter, selectors, tableName); } /** @@ -157,20 +171,38 @@ public class StatementFactory { Assert.notNull(query, "Query must not be null"); Assert.notNull(entity, "Entity must not be null"); + return select(query, entity, entity.getTableName()); + } + + /** + * Create a {@literal SELECT} statement by mapping {@link Query} to {@link Select}. + * + * @param query must not be {@literal null}. + * @param entity must not be {@literal null}. + * @param tableName must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + * @since 2.1 + */ + public RegularStatement select(Query query, CassandraPersistentEntity entity, CqlIdentifier tableName) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(entity, "Table name must not be null"); + Filter filter = getQueryMapper().getMappedObject(query, entity); List selectors = getQueryMapper().getMappedSelectors(query.getColumns(), entity); - return createSelect(query, entity, filter, selectors); + return createSelect(query, entity, filter, selectors, tableName); } private Select createSelect(Query query, CassandraPersistentEntity entity, Filter filter, - List selectors) { + List selectors, CqlIdentifier tableName) { Sort sort = Optional.of(query.getSort()).map(querySort -> getQueryMapper().getMappedSort(querySort, entity)) .orElse(Sort.unsorted()); - Select select = createSelectAndOrder(selectors, entity.getTableName(), filter, sort); + Select select = createSelectAndOrder(selectors, tableName, filter, sort); query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(select, queryOptions)); @@ -253,16 +285,37 @@ public class StatementFactory { * @param entity must not be {@literal null}. * @return the rendered {@link RegularStatement}. */ - public RegularStatement update(Query query, Update updateObj, CassandraPersistentEntity entity) { + public RegularStatement update(Query query, Update update, CassandraPersistentEntity entity) { Assert.notNull(query, "Query must not be null"); + Assert.notNull(update, "Update must not be null"); Assert.notNull(entity, "Entity must not be null"); + return update(query, update, entity, entity.getTableName()); + } + + /** + * Create an {@literal UPDATE} statement by mapping {@link Query} to {@link Update}. + * + * @param query must not be {@literal null}. + * @param updateObj must not be {@literal null}. + * @param entity must not be {@literal null}. + * @param tableName must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + * @since 2.1 + */ + RegularStatement update(Query query, Update updateObj, CassandraPersistentEntity entity, CqlIdentifier tableName) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(updateObj, "Update must not be null"); + Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(tableName, "Table name must not be null"); + Filter filter = getQueryMapper().getMappedObject(query, entity); Update mappedUpdate = getUpdateMapper().getMappedObject(updateObj, entity); - com.datastax.driver.core.querybuilder.Update update = update(entity.getTableName(), mappedUpdate, filter); + com.datastax.driver.core.querybuilder.Update update = update(tableName, mappedUpdate, filter); query.getQueryOptions().ifPresent(queryOptions -> { if (queryOptions instanceof WriteOptions) { @@ -381,11 +434,29 @@ public class StatementFactory { Assert.notNull(query, "Query must not be null"); Assert.notNull(entity, "Entity must not be null"); + return delete(query, entity, entity.getTableName()); + } + + /** + * Create a {@literal DELETE} statement by mapping {@link Query} to {@link Delete}. + * + * @param query must not be {@literal null}. + * @param entity must not be {@literal null}. + * @param tableName must not be {@literal null}. + * @return the rendered {@link RegularStatement}. + * @see 2.1 + */ + public RegularStatement delete(Query query, CassandraPersistentEntity entity, CqlIdentifier tableName) { + + Assert.notNull(query, "Query must not be null"); + Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(tableName, "Table name must not be null"); + Filter filter = getQueryMapper().getMappedObject(query, entity); List columnNames = getQueryMapper().getMappedColumnNames(query.getColumns(), entity); - Delete delete = delete(columnNames, entity.getTableName(), filter); + Delete delete = delete(columnNames, tableName, filter); query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(delete, queryOptions)); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupportTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupportTests.java new file mode 100644 index 000000000..87175b94c --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableDeleteOperationSupportTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 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.springframework.data.cassandra.core.query.Criteria.*; +import static org.springframework.data.cassandra.core.query.Query.*; + +import lombok.Data; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.Column; +import org.springframework.data.cassandra.core.mapping.Indexed; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest; + +/** + * Integration tests for {@link ExecutableDeleteOperationSupport}. + * + * @author Mark Paluch + */ +public class ExecutableDeleteOperationSupportTests extends AbstractKeyspaceCreatingIntegrationTest { + + CassandraAdminTemplate template; + + Person han; + Person luke; + + @Before + public void setUp() { + + template = new CassandraAdminTemplate(session, new MappingCassandraConverter()); + template.dropTable(true, CqlIdentifier.of("person")); + template.createTable(true, CqlIdentifier.of("person"), ExecutableInsertOperationSupportTests.Person.class, + Collections.emptyMap()); + + han = new Person(); + han.firstname = "han"; + han.id = "id-1"; + + luke = new Person(); + luke.firstname = "luke"; + luke.id = "id-2"; + + template.insert(han); + template.insert(luke); + } + + @Test // DATACASS-485 + public void removeAllMatching() { + + WriteResult writeResult = template.delete(Person.class).matching(query(where("id").is(han.id))).all(); + + assertThat(writeResult.wasApplied()).isTrue(); + } + + @Test // DATACASS-485 + public void removeAllMatchingWithAlternateDomainTypeAndCollection() { + + WriteResult writeResult = template.delete(Jedi.class).inTable("person") + .matching(query(where("id").in(han.id, luke.id))) + .all(); + + assertThat(writeResult.wasApplied()).isTrue(); + assertThat(template.select(Query.empty(), Person.class)).isEmpty(); + } + + @Data + @Table + static class Person { + @Id String id; + @Indexed String firstname; + } + + @Data + static class Jedi { + + @Column("firstname") String name; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupportTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupportTests.java new file mode 100644 index 000000000..1cb8652bd --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableInsertOperationSupportTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2018 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 lombok.Data; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.Indexed; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest; + +/** + * Integration tests for {@link ExecutableInsertOperationSupport}. + * + * @author Mark Paluch + */ +public class ExecutableInsertOperationSupportTests extends AbstractKeyspaceCreatingIntegrationTest { + + CassandraAdminTemplate template; + + Person han; + Person luke; + + @Before + public void setUp() { + + template = new CassandraAdminTemplate(session, new MappingCassandraConverter()); + template.dropTable(true, CqlIdentifier.of("person")); + template.createTable(true, CqlIdentifier.of("person"), Person.class, Collections.emptyMap()); + + initPersons(); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void domainTypeIsRequired() { + template.insert((Class) null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void tableIsRequiredOnSet() { + template.insert(Person.class).inTable((String) null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void optionsIsRequiredOnSet() { + template.insert(Person.class).withOptions(null); + } + + @Test // DATACASS-485 + public void insertOne() { + + WriteResult writeResult = template.insert(Person.class).inTable("person").one(han); + + assertThat(writeResult.wasApplied()).isTrue(); + assertThat(template.selectOneById(han.id, Person.class)).isEqualTo(han); + } + + @Test // DATACASS-485 + public void insertOneWithOptions() { + + template.insert(Person.class).inTable("person").one(han); + + WriteResult writeResult = template.insert(Person.class).inTable("person") + .withOptions(InsertOptions.builder().withIfNotExists().build()).one(han); + + assertThat(writeResult.wasApplied()).isFalse(); + assertThat(template.selectOneById(han.id, Person.class)).isEqualTo(han); + } + + @Data + @Table + static class Person { + + @Id String id; + @Indexed String firstname; + @Indexed String lastname; + } + + private void initPersons() { + + han = new Person(); + han.firstname = "han"; + han.lastname = "solo"; + han.id = "id-1"; + + luke = new Person(); + luke.firstname = "luke"; + luke.lastname = "skywalker"; + luke.id = "id-2"; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupportTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupportTests.java new file mode 100644 index 000000000..bca80ad6e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableSelectOperationSupportTests.java @@ -0,0 +1,337 @@ +/* + * Copyright 2018 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.springframework.data.cassandra.core.query.Criteria.*; +import static org.springframework.data.cassandra.core.query.Query.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Collections; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.Column; +import org.springframework.data.cassandra.core.mapping.Indexed; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest; + +/** + * Integration tests for {@link ExecutableSelectOperationSupport}. + * + * @author Mark Paluch + */ +public class ExecutableSelectOperationSupportTests extends AbstractKeyspaceCreatingIntegrationTest { + + CassandraAdminTemplate template; + + Person han; + Person luke; + + @Before + public void setUp() { + + template = new CassandraAdminTemplate(session, new MappingCassandraConverter()); + template.dropTable(true, CqlIdentifier.of("person")); + template.createTable(true, CqlIdentifier.of("person"), Person.class, Collections.emptyMap()); + + initPersons(); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void domainTypeIsRequired() { + template.query(null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void returnTypeIsRequiredOnSet() { + template.query(Person.class).as(null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void tableIsRequiredOnSet() { + template.query(Person.class).inTable((String) null); + } + + @Test // DATACASS-485 + public void findAll() { + assertThat(template.query(Person.class).all()).containsExactlyInAnyOrder(han, luke); + } + + @Test // DATACASS-485 + public void findAllWithCollection() { + assertThat(template.query(Human.class).inTable("person").all()).hasSize(2); + } + + @Test // DATACASS-485 + public void findAllWithProjection() { + assertThat(template.query(Person.class).as(Jedi.class).all()).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + + @Test // DATACASS-485 + public void findByReturningAllValuesAsClosedInterfaceProjection() { + + assertThat(template.query(Person.class).as(PersonProjection.class).all()) + .hasOnlyElementsOfTypes(PersonProjection.class); + } + + @Test // DATACASS-485 + public void findAllBy() { + assertThat(template.query(Person.class).matching(queryLuke()).all()).containsExactlyInAnyOrder(luke); + } + + @Test // DATACASS-485 + public void findAllByWithCollectionUsingMappingInformation() { + assertThat(template.query(Jedi.class).inTable("person").all()).isNotEmpty().hasOnlyElementsOfType(Jedi.class); + } + + @Test // DATACASS-485 + public void findAllByWithCollection() { + assertThat(template.query(Human.class).inTable("person").matching(queryLuke()).all()).hasSize(1); + } + + @Test // DATACASS-485 + public void findAllByWithProjection() { + assertThat(template.query(Person.class).as(Jedi.class).all()).hasOnlyElementsOfType(Jedi.class).isNotEmpty(); + } + + @Test // DATACASS-485 + public void findBy() { + assertThat(template.query(Person.class).matching(queryLuke()).one()).contains(luke); + } + + @Test // DATACASS-485 + public void findByNoMatch() { + assertThat(template.query(Person.class).matching(querySpock()).one()).isEmpty(); + } + + @Test(expected = IncorrectResultSizeDataAccessException.class) // DATACASS-485 + public void findByTooManyResults() { + template.query(Person.class).one(); + } + + @Test // DATACASS-485 + public void findByReturningOneValue() { + assertThat(template.query(Person.class).matching(queryLuke()).oneValue()).isEqualTo(luke); + } + + @Test(expected = IncorrectResultSizeDataAccessException.class) // DATACASS-485 + public void findByReturningOneValueButTooManyResults() { + template.query(Person.class).oneValue(); + } + + @Test // DATACASS-485 + public void findByReturningFirstValue() { + + assertThat(template.query(Person.class).matching(queryLuke()).firstValue()).isEqualTo(luke); + } + + @Test // DATACASS-485 + public void findByReturningFirstValueForManyResults() { + assertThat(template.query(Person.class).firstValue()).isIn(han, luke); + } + + @Test // DATACASS-485 + public void findByReturningFirstValueAsClosedInterfaceProjection() { + + PersonProjection result = template.query(Person.class).as(PersonProjection.class) + .matching(query(where("firstname").is("han")).withAllowFiltering()).firstValue(); + + assertThat(result).isInstanceOf(PersonProjection.class); + assertThat(result.getFirstname()).isEqualTo("han"); + } + + @Test // DATACASS-485 + public void findByReturningFirstValueAsOpenInterfaceProjection() { + + PersonSpELProjection result = template.query(Person.class).as(PersonSpELProjection.class) + .matching(query(where("firstname").is("han")).withAllowFiltering()).firstValue(); + + assertThat(result).isInstanceOf(PersonSpELProjection.class); + assertThat(result.getName()).isEqualTo("han"); + } + + @Test // DATACASS-485 + public void streamAll() { + + try (Stream stream = template.query(Person.class).stream()) { + assertThat(stream).containsExactlyInAnyOrder(han, luke); + } + } + + @Test // DATACASS-485 + public void streamAllWithCollection() { + + Stream stream = template.query(Human.class).inTable("person").stream(); + assertThat(stream).hasSize(2); + } + + @Test // DATACASS-485 + public void streamAllWithProjection() { + + try (Stream stream = template.query(Person.class).as(Jedi.class).stream()) { + assertThat(stream).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + } + + @Test // DATACASS-485 + public void streamAllReturningResultsAsClosedInterfaceProjection() { + + TerminatingSelect operation = template.query(Person.class).as(PersonProjection.class); + + assertThat(operation.stream()) // + .hasSize(2) // + .allSatisfy(it -> { + assertThat(it).isInstanceOf(PersonProjection.class); + assertThat(it.getFirstname()).isNotBlank(); + }); + } + + @Test // DATACASS-485 + public void streamAllReturningResultsAsOpenInterfaceProjection() { + + TerminatingSelect operation = template.query(Person.class).as(PersonSpELProjection.class); + + assertThat(operation.stream()) // + .hasSize(2) // + .allSatisfy(it -> { + assertThat(it).isInstanceOf(PersonSpELProjection.class); + assertThat(it.getName()).isNotBlank(); + }); + } + + @Test // DATACASS-485 + public void streamAllBy() { + + Stream stream = template.query(Person.class).matching(queryLuke()).stream(); + assertThat(stream).containsExactlyInAnyOrder(luke); + } + + @Test // DATACASS-485 + public void firstShouldReturnFirstEntryInCollection() { + assertThat(template.query(Person.class).first()).isNotEmpty(); + } + + @Test // DATACASS-485 + public void countShouldReturnNrOfElementsInCollectionWhenNoQueryPresent() { + assertThat(template.query(Person.class).count()).isEqualTo(2); + } + + @Test // DATACASS-485 + public void countShouldReturnNrOfElementsMatchingQuery() { + + assertThat(template.query(Person.class) + .matching(query(where("firstname").is(luke.getFirstname())).withAllowFiltering()).count()).isEqualTo(1); + } + + @Test // DATACASS-485 + public void existsShouldReturnTrueIfAtLeastOneElementExistsInCollection() { + assertThat(template.query(Person.class).exists()).isTrue(); + } + + @Test // DATACASS-485 + public void existsShouldReturnFalseIfNoElementExistsInCollection() { + + template.truncate(Person.class); + + assertThat(template.query(Person.class).exists()).isFalse(); + } + + @Test // DATACASS-485 + public void existsShouldReturnTrueIfAtLeastOneElementMatchesQuery() { + + assertThat(template.query(Person.class).matching(queryLuke()).exists()).isTrue(); + } + + @Test // DATACASS-485 + public void existsShouldReturnFalseWhenNoElementMatchesQuery() { + assertThat(template.query(Person.class).matching(querySpock()).exists()).isFalse(); + } + + @Test // DATACASS-485 + public void returnsTargetObjectDirectlyIfProjectionInterfaceIsImplemented() { + assertThat(template.query(Person.class).as(Contact.class).all()).allMatch(it -> it instanceof Person); + } + + private static Query queryLuke() { + return query(where("firstname").is("luke")).withAllowFiltering(); + } + + private static Query querySpock() { + return query(where("firstname").is("spock")).withAllowFiltering(); + } + + interface Contact {} + + @Data + @Table + static class Person implements Contact { + + @Id String id; + @Indexed String firstname; + @Indexed String lastname; + } + + interface PersonProjection { + String getFirstname(); + } + + public interface PersonSpELProjection { + + @Value("#{target.firstname}") + String getName(); + } + + @Data + static class Human { + @Id String id; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Jedi { + + @Column("firstname") String name; + } + + private void initPersons() { + + han = new Person(); + han.firstname = "han"; + han.lastname = "solo"; + han.id = "id-1"; + + luke = new Person(); + luke.firstname = "luke"; + luke.lastname = "skywalker"; + luke.id = "id-2"; + + template.insert(han); + template.insert(luke); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupportTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupportTests.java new file mode 100644 index 000000000..ddc3e16a1 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ExecutableUpdateOperationSupportTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2018 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.springframework.data.cassandra.core.query.Criteria.*; +import static org.springframework.data.cassandra.core.query.Query.*; +import static org.springframework.data.cassandra.core.query.Update.*; + +import lombok.Data; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.core.mapping.Column; +import org.springframework.data.cassandra.core.mapping.Indexed; +import org.springframework.data.cassandra.core.mapping.Table; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest; + +/** + * Integration tests for {@link ExecutableUpdateOperationSupport}. + * + * @author Mark Paluch + */ +public class ExecutableUpdateOperationSupportTests extends AbstractKeyspaceCreatingIntegrationTest { + + CassandraAdminTemplate template; + + Person han; + Person luke; + + @Before + public void setUp() { + + template = new CassandraAdminTemplate(session, new MappingCassandraConverter()); + template.dropTable(true, CqlIdentifier.of("person")); + template.createTable(false, CqlIdentifier.of("person"), Person.class, Collections.emptyMap()); + + han = new Person(); + han.firstname = "han"; + han.id = "id-1"; + + luke = new Person(); + luke.firstname = "luke"; + luke.id = "id-2"; + + template.insert(han); + template.insert(luke); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void domainTypeIsRequired() { + template.update(null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void queryIsRequired() { + template.update(Person.class).matching(null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void tableIsRequiredOnSet() { + template.update(Person.class).inTable((CqlIdentifier) null); + } + + @Test // DATACASS-485 + public void updateAllMatching() { + + WriteResult writeResult = template.update(Person.class).matching(queryHan()).apply(update("firstname", "Han")) + .all(); + + assertThat(writeResult.wasApplied()).isTrue(); + } + + @Test // DATACASS-485 + public void updateWithDifferentDomainClassAndCollection() { + + WriteResult writeResult = template.update(Jedi.class).inTable("person").matching(query(where("id").is(han.getId()))) + .apply(update("name", "Han")).all(); + + assertThat(writeResult.wasApplied()).isTrue(); + assertThat(template.selectOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", + "Han"); + } + + private Query queryHan() { + return query(where("id").is(han.getId())); + } + + @Data + @Table + static class Person { + + @Id String id; + @Indexed String firstname; + } + + @Data + static class Jedi { + + @Column("firstname") String name; + } +}