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 2d49804e3..e5ef7d4d2 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,14 +15,14 @@ */ package org.springframework.data.cassandra.core; -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 lombok.NonNull; +import lombok.Value; + import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.core.convert.CassandraConverter; @@ -44,7 +44,9 @@ 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.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -87,10 +89,10 @@ public class CassandraTemplate implements CassandraOperations { private final MappingContext, CassandraPersistentProperty> mappingContext; - private final StatementFactory statementFactory; - private final SpelAwareProxyProjectionFactory projectionFactory; + private final StatementFactory statementFactory; + /** * Creates an instance of {@link CassandraTemplate} initialized with the given {@link Session} and a default * {@link MappingCassandraConverter}. @@ -149,8 +151,8 @@ public class CassandraTemplate implements CassandraOperations { this.converter = converter; this.cqlOperations = cqlOperations; this.mappingContext = converter.getMappingContext(); - this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); this.projectionFactory = new SpelAwareProxyProjectionFactory(); + this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } /* (non-Javadoc) @@ -174,6 +176,7 @@ public class CassandraTemplate implements CassandraOperations { * @see org.springframework.data.cassandra.core.CassandraOperations#CqlOperations() */ @Override + @org.springframework.lang.NonNull public CqlOperations getCqlOperations() { return this.cqlOperations; } @@ -189,14 +192,28 @@ public class CassandraTemplate implements CassandraOperations { return this.mappingContext; } + @org.springframework.lang.NonNull private CassandraPersistentEntity getRequiredPersistentEntity(Object entity) { return getRequiredPersistentEntity(entity.getClass()); } + @org.springframework.lang.NonNull private CassandraPersistentEntity getRequiredPersistentEntity(Class entityType) { return getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entityType)); } + /** + * Returns a reference to the configured {@link ProjectionFactory} used by this template + * to process CQL query projections. + * + * @return a reference to the configured {@link ProjectionFactory} used by this template + * to process CQL query projections. + * @see org.springframework.data.projection.SpelAwareProxyProjectionFactory + */ + protected SpelAwareProxyProjectionFactory getProjectionFactory() { + return this.projectionFactory; + } + /** * Returns the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. * @@ -207,6 +224,7 @@ public class CassandraTemplate implements CassandraOperations { return this.statementFactory; } + @org.springframework.lang.NonNull private CqlIdentifier getTableName(Object entity) { return getRequiredPersistentEntity(entity).getTableName(); } @@ -215,6 +233,7 @@ public class CassandraTemplate implements CassandraOperations { * @see org.springframework.data.cassandra.core.CassandraOperations#getTableName(java.lang.Class) */ @Override + @org.springframework.lang.NonNull public CqlIdentifier getTableName(Class entityClass) { return getRequiredPersistentEntity(entityClass).getTableName(); } @@ -289,7 +308,8 @@ public class CassandraTemplate implements CassandraOperations { Function mapper = getMapper(entityClass, entityClass); - return QueryUtils.readSlice(resultSet, (row, rowNum) -> mapper.apply(row), 0, getEffectiveFetchSize(statement)); + return QueryUtils.readSlice(resultSet, (row, rowNum) -> mapper.apply(row), + 0, getEffectiveFetchSize(statement)); } /* (non-Javadoc) @@ -301,8 +321,9 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return StreamSupport.stream(getCqlOperations().queryForResultSet(statement).spliterator(), false) - .map(getMapper(entityClass, entityClass)); + ResultSet resultSet = getCqlOperations().queryForResultSet(statement); + + return StreamSupport.stream(resultSet.spliterator(), false).map(getMapper(entityClass, entityClass)); } /* (non-Javadoc) @@ -333,8 +354,9 @@ public class CassandraTemplate implements CassandraOperations { Function mapper = getMapper(entityClass, returnType); - RegularStatement select = getStatementFactory().select(query, - getMappingContext().getRequiredPersistentEntity(entityClass), tableName); + RegularStatement select = getStatementFactory() + .select(query, getRequiredPersistentEntity(entityClass), tableName); + return getCqlOperations().query(select, (row, rowNum) -> mapper.apply(row)); } @@ -347,8 +369,9 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return slice(this.statementFactory.select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), - entityClass); + RegularStatement select = getStatementFactory().select(query, getRequiredPersistentEntity(entityClass)); + + return slice(select, entityClass); } /* (non-Javadoc) @@ -365,11 +388,12 @@ public class CassandraTemplate implements CassandraOperations { Stream doStream(Query query, Class entityClass, CqlIdentifier tableName, Class returnType) { - RegularStatement statement = getStatementFactory().select(query, - getMappingContext().getRequiredPersistentEntity(entityClass), tableName); + RegularStatement statement = getStatementFactory() + .select(query, getRequiredPersistentEntity(entityClass), tableName); - return StreamSupport.stream(getCqlOperations().queryForResultSet(statement).spliterator(), false) - .map(getMapper(entityClass, returnType)); + ResultSet resultSet = getCqlOperations().queryForResultSet(statement); + + return StreamSupport.stream(resultSet.spliterator(), false).map(getMapper(entityClass, returnType)); } /* (non-Javadoc) @@ -380,7 +404,7 @@ public class CassandraTemplate implements CassandraOperations { List result = select(query, entityClass); - return (result.isEmpty() ? null : result.get(0)); + return result.isEmpty() ? null : result.get(0); } /* (non-Javadoc) @@ -394,16 +418,20 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(update, "Update must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return getCqlOperations().execute( - getStatementFactory().update(query, update, getMappingContext().getRequiredPersistentEntity(entityClass))); + Statement updateStatement = getStatementFactory() + .update(query, update, getRequiredPersistentEntity(entityClass)); + + return getCqlOperations().execute(updateStatement); } + @Nullable 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)); + RegularStatement updateStatement = getStatementFactory() + .update(query, update, getRequiredPersistentEntity(entityClass), tableName); + + return getCqlOperations().execute(new StatementCallback(updateStatement)); } /* (non-Javadoc) @@ -415,12 +443,16 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - return doDelete(query, entityClass, getTableName(entityClass)).wasApplied(); + WriteResult result = doDelete(query, entityClass, getTableName(entityClass)); + + return result != null && result.wasApplied(); } + @Nullable WriteResult doDelete(Query query, Class entityClass, CqlIdentifier tableName) { - RegularStatement delete = getStatementFactory().delete(query, getRequiredPersistentEntity(entityClass), tableName); + RegularStatement delete = getStatementFactory() + .delete(query, getRequiredPersistentEntity(entityClass), tableName); return getCqlOperations().execute(new StatementCallback(delete)); } @@ -458,11 +490,12 @@ public class CassandraTemplate implements CassandraOperations { long doCount(Query query, Class entityClass, CqlIdentifier tableName) { - RegularStatement count = getStatementFactory().count(query, getRequiredPersistentEntity(entityClass), tableName); + RegularStatement countStatement = getStatementFactory() + .count(query, getRequiredPersistentEntity(entityClass), tableName); - Long result = getCqlOperations().queryForObject(count, Long.class); + Long count = getCqlOperations().queryForObject(countStatement, Long.class); - return result != null ? result : 0L; + return count != null ? count : 0L; } /* (non-Javadoc) @@ -497,8 +530,8 @@ public class CassandraTemplate implements CassandraOperations { boolean doExists(Query query, Class entityClass, CqlIdentifier tableName) { - RegularStatement select = getStatementFactory().select(query.limit(1), getRequiredPersistentEntity(entityClass), - tableName); + RegularStatement select = getStatementFactory() + .select(query.limit(1), getRequiredPersistentEntity(entityClass), tableName); return getCqlOperations().queryForResultSet(select).iterator().hasNext(); } @@ -538,8 +571,7 @@ public class CassandraTemplate implements CassandraOperations { Assert.notNull(entity, "Entity must not be null"); Assert.notNull(options, "InsertOptions must not be null"); - CqlIdentifier tableName = getTableName(entity); - return doInsert(entity, options, tableName); + return doInsert(entity, options, getTableName(entity)); } WriteResult doInsert(Object entity, WriteOptions options, CqlIdentifier tableName) { @@ -651,7 +683,7 @@ public class CassandraTemplate implements CassandraOperations { * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation#update(java.lang.Class) */ @Override - public ExecutableUpdate update(Class domainType) { + public ExecutableUpdate update(Class domainType) { return new ExecutableUpdateOperationSupport(this).update(domainType); } @@ -667,19 +699,6 @@ public class CassandraTemplate implements CassandraOperations { // 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(); } @@ -703,6 +722,24 @@ public class CassandraTemplate implements CassandraOperations { return getCqlOperations().execute(this::getConfiguredFetchSize); } + @SuppressWarnings("unchecked") + private Function getMapper(Class entityType, Class targetType) { + + Class typeToRead = resolveTypeToRead(entityType, targetType); + + return row -> { + + Object source = getConverter().read(typeToRead, row); + + return (T) (targetType.isInterface() + ? getProjectionFactory().createProjection(targetType, source) : source); + }; + } + + private Class resolveTypeToRead(Class entityType, Class targetType) { + return targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType : targetType; + } + /* (non-Javadoc) * @see org.springframework.data.cassandra.core.CassandraOperations#batchOps() */ 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 index 9c130db43..e920796e0 100644 --- 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 @@ -17,6 +17,7 @@ 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.util.Assert; /** * {@link ExecutableDeleteOperation} allows creation and execution of Cassandra {@code DELETE} operations in a fluent @@ -42,11 +43,12 @@ import org.springframework.data.cassandra.core.query.Query; public interface ExecutableDeleteOperation { /** - * Start creating a {@code DELETE} operation for the given {@literal domainType}. + * Begin creating a {@code DELETE} operation for the given {@link Class domainType}. * - * @param domainType must not be {@literal null}. + * @param domainType {@link Class type} of domain object to delete; must not be {@literal null}. * @return new instance of {@link ExecutableDelete}. - * @throws IllegalArgumentException if domainType is {@literal null}. + * @throws IllegalArgumentException if {@link Class domainType} is {@literal null}. + * @see ExecutableDelete */ ExecutableDelete delete(Class domainType); @@ -56,52 +58,73 @@ public interface ExecutableDeleteOperation { interface DeleteWithTable { /** - * Explicitly set the name of the table to perform the query on. + * Explicitly set the {@link String name} of the table on which to execute the delete. *

- * Skip this step to use the default table derived from the domain type. + * Skip this step to use the default table derived from the {@link Class 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. + * @param table {@link String name} of the table; must not be {@literal null} or empty. + * @return new instance of {@link DeleteWithQuery}. + * @throws IllegalArgumentException if {@link String table} is {@literal null} or empty. + * @see #inTable(CqlIdentifier) + * @see DeleteWithQuery */ - DeleteWithQuery inTable(String table); + default DeleteWithQuery inTable(String table) { + + Assert.hasText(table, "Table name must not be null or empty"); + + return inTable(CqlIdentifier.of(table)); + } /** - * Explicitly set the name of the table to perform the query on. + * Explicitly set the {@link CqlIdentifier name} of the table on which to execute the delete. *

- * Skip this step to use the default table derived from the domain type. + * Skip this step to use the default table derived from the {@link Class domain type}. * - * @param table must not be {@literal null}. - * @return new instance of {@link DeleteWithTable}. - * @throws IllegalArgumentException if {@link CqlIdentifier} is {@literal null}. + * @param table {@link CqlIdentifier name} of the table; must not be {@literal null}. + * @return new instance of {@link DeleteWithQuery}. + * @throws IllegalArgumentException if {@link CqlIdentifier table} is {@literal null}. + * @see org.springframework.data.cassandra.core.cql.CqlIdentifier + * @see DeleteWithQuery */ DeleteWithQuery inTable(CqlIdentifier table); + } + /** + * Filtering (optional). + */ + interface DeleteWithQuery { + + /** + * Define the {@link Query} filtering elements to delete. + * + * @param query {@link Query} used to filter the elements to delete; must not be {@literal null}. + * @return new instance of {@link TerminatingDelete}. + * @throws IllegalArgumentException if {@link Query} is {@literal null}. + * @see TerminatingDelete + */ + TerminatingDelete matching(Query query); + + } + + /** + * Trigger {@code DELETE} execution by calling one of the terminating methods. + */ interface TerminatingDelete { /** * Remove all matching rows. * - * @return the {@link WriteResult}. Never {@literal null}. + * @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. + * the {@link ExecutableDelete} interface 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 index 81d8936be..6177c8a42 100644 --- 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 @@ -29,6 +29,8 @@ import org.springframework.util.Assert; * Implementation of {@link ExecutableDeleteOperation}. * * @author Mark Paluch + * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation + * @see org.springframework.data.cassandra.core.query.Query * @since 2.1 */ @RequiredArgsConstructor @@ -42,14 +44,21 @@ class ExecutableDeleteOperationSupport implements ExecutableDeleteOperation { @Override public ExecutableDelete delete(Class domainType) { - Assert.notNull(domainType, "DomainType must not be null!"); + Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableDeleteSupport(template, domainType, Query.empty(), null); + return new ExecutableDeleteSupport(this.template, domainType, Query.empty(), null); } + // TODO: rethink the implementation + // While the use of final fields and construction on mutation effectively makes this class Thread-safe, + // it is possible this implementation could generate a high-level of young-gen garbage on the JVM heap, + // particularly if the template delete(..) (and this class) are used inside of a loop for a large number + // of domain types. Of course, this assumption is highly contingent on the user's `Query` + // in addition to his/her application design. + @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) - static class ExecutableDeleteSupport implements ExecutableDelete, DeleteWithTable, TerminatingDelete { + static class ExecutableDeleteSupport implements ExecutableDelete, TerminatingDelete { @NonNull CassandraTemplate template; @@ -59,17 +68,6 @@ class ExecutableDeleteOperationSupport implements ExecutableDeleteOperation { @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) */ @@ -78,7 +76,7 @@ class ExecutableDeleteOperationSupport implements ExecutableDeleteOperation { Assert.notNull(tableName, "Table name must not be null"); - return new ExecutableDeleteSupport(template, domainType, query, tableName); + return new ExecutableDeleteSupport(this.template, this.domainType, this.query, tableName); } /* (non-Javadoc) @@ -87,20 +85,20 @@ class ExecutableDeleteOperationSupport implements ExecutableDeleteOperation { @Override public TerminatingDelete matching(Query query) { - Assert.notNull(query, "Query must not be null!"); + Assert.notNull(query, "Query must not be null"); - return new ExecutableDeleteSupport(template, domainType, query, tableName); + return new ExecutableDeleteSupport(this.template, this.domainType, query, this.tableName); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ExecutableDeleteOperation.TerminatingDelete#all() */ public WriteResult all() { - return template.doDelete(query, domainType, getTableName()); + return this.template.doDelete(this.query, this.domainType, getTableName()); } private CqlIdentifier getTableName() { - return tableName != null ? tableName : template.getTableName(domainType); + return this.tableName != null ? this.tableName : this.template.getTableName(this.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 index f504b648e..c55755671 100644 --- 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 @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.core; import org.springframework.data.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; /** * {@link ExecutableInsertOperation} allows creation and execution of Cassandra {@code INSERT} insert operations in a @@ -34,59 +35,56 @@ import org.springframework.data.cassandra.core.cql.CqlIdentifier; * * * @author Mark Paluch + * @author John Blum * @since 2.1 */ public interface ExecutableInsertOperation { /** - * Start creating an {@code INSERT} operation for given {@literal domainType}. + * Begin creating an {@code INSERT} operation for given {@link Class domainType}. * - * @param domainType must not be {@literal null}. + * @param domainType {@link Class type} of domain object to insert; must not be {@literal null}. * @return new instance of {@link ExecutableInsert}. - * @throws IllegalArgumentException if domainType is {@literal null}. + * @throws IllegalArgumentException if {@link Class domainType} is {@literal null}. + * @see ExecutableInsert */ 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). + * Table override (optional). */ interface InsertWithTable extends InsertWithOptions { /** - * Explicitly set the name of the table. + * Explicitly set the {@link String name} of the table. *

- * Skip this step to use the default table derived from the domain type. + * Skip this step to use the default table derived from the {@link Class 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. + * @param table {@link String name} of the table; must not be {@literal null} or empty. + * @return new instance of {@link InsertWithOptions}. + * @throws IllegalArgumentException if {@link String table} is {@literal null} or empty. + * @see InsertWithOptions */ - InsertWithOptions inTable(String table); + default InsertWithOptions inTable(String table) { + + Assert.hasText(table, "Table name must not be null or empty"); + + return inTable(CqlIdentifier.of(table)); + } /** - * Explicitly set the name of the table. + * Explicitly set the {@link CqlIdentifier name} of the table. *

- * Skip this step to use the default table derived from the domain type. + * Skip this step to use the default table derived from the {@link Class domain type}. * - * @param table must not be {@literal null}. - * @return new instance of {@link TerminatingInsert}. - * @throws IllegalArgumentException if {@link CqlIdentifier} is {@literal null}. + * @param table {@link CqlIdentifier name} of the table; must not be {@literal null}. + * @return new instance of {@link InsertWithOptions}. + * @throws IllegalArgumentException if {@link CqlIdentifier table} is {@literal null}. + * @see org.springframework.data.cassandra.core.cql.CqlIdentifier + * @see InsertWithOptions */ InsertWithOptions inTable(CqlIdentifier table); + } /** @@ -95,17 +93,38 @@ public interface ExecutableInsertOperation { interface InsertWithOptions extends TerminatingInsert { /** - * Set insert options. + * Set {@link InsertOptions}. * - * @param insertOptions insertOptions not be {@literal null}. + * @param insertOptions {@link InsertOptions} to set; must not be {@literal null}. * @return new instance of {@link TerminatingInsert}. * @throws IllegalArgumentException if {@link InsertOptions} is {@literal null}. + * @see org.springframework.data.cassandra.core.InsertOptions + * @see TerminatingInsert */ TerminatingInsert withOptions(InsertOptions insertOptions); + } /** - * {@link ExecutableInsert} provides methods for constructing {@code INSERT} operations in a fluent way. + * Trigger {@code INSERT} execution by calling one of the terminating methods. */ - interface ExecutableInsert extends TerminatingInsert, InsertWithTable, InsertWithOptions {} + interface TerminatingInsert { + + /** + * Insert exactly one {@link Object}. + * + * @param object {@link Object} to insert; must not be {@literal null}. + * @throws IllegalArgumentException if {@link Object} is {@literal null}. + * @see org.springframework.data.cassandra.core.WriteResult + */ + WriteResult one(T object); + + } + + /** + * The {@link ExecutableInsert} interface provides methods for constructing {@code INSERT} operations + * in a fluent way. + */ + interface ExecutableInsert extends InsertWithTable {} + } 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 index 3426dce05..32de1fb9a 100644 --- 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 @@ -28,6 +28,7 @@ import org.springframework.util.Assert; * Implementation of {@link ExecutableInsertOperation}. * * @author Mark Paluch + * @see org.springframework.data.cassandra.core.ExecutableInsertOperation * @since 2.1 */ @RequiredArgsConstructor @@ -41,11 +42,17 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { @Override public ExecutableInsert insert(Class domainType) { - Assert.notNull(domainType, "DomainType must not be null!"); + Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableInsertSupport<>(template, domainType, null, InsertOptions.empty()); + return new ExecutableInsertSupport<>(this.template, domainType, InsertOptions.empty(), null); } + // TODO: rethink the implementation + // While the use of final fields and construction on mutation effectively makes this class Thread-safe, + // it is possible this implementation could generate a high-level of young-gen garbage on the JVM heap, + // particularly if the template insert(..) (and this class) are used inside of a loop for a large number + // of domain types. Of course, this assumption is highly contingent on the user's application design. + @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) static class ExecutableInsertSupport implements ExecutableInsert { @@ -54,20 +61,9 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { @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); - } + @Nullable CqlIdentifier tableName; /* (non-Javadoc) * @see org.springframework.data.cassandra.core.ExecutableInsertOperation.InsertWithTable#inTable(org.springframework.data.cassandra.core.cql.CqlIdentifier) @@ -77,7 +73,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { Assert.notNull(tableName, "Table name must not be null"); - return new ExecutableInsertSupport<>(template, domainType, tableName, insertOptions); + return new ExecutableInsertSupport<>(this.template, this.domainType, this.insertOptions, tableName); } /* (non-Javadoc) @@ -88,7 +84,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { Assert.notNull(insertOptions, "InsertOptions must not be null"); - return new ExecutableInsertSupport<>(template, domainType, tableName, insertOptions); + return new ExecutableInsertSupport<>(this.template, this.domainType, insertOptions, this.tableName); } /* (non-Javadoc) @@ -97,13 +93,13 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { @Override public WriteResult one(T object) { - Assert.notNull(object, "Object must not be null!"); + Assert.notNull(object, "Object must not be null"); - return template.doInsert(object, insertOptions, getTableName()); + return this.template.doInsert(object, this.insertOptions, getTableName()); } private CqlIdentifier getTableName() { - return tableName != null ? tableName : template.getTableName(domainType); + return this.tableName != null ? this.tableName : this.template.getTableName(this.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 index d57a7fa70..62c600b66 100644 --- 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 @@ -22,19 +22,20 @@ 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; +import org.springframework.util.Assert; /** - * {@link ExecutableSelectOperation} allows creation and execution of Cassandra {@code SELECT} operations in a fluent - * API style. + * The {@link ExecutableSelectOperation} interface 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 + * 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. + * {@literal returnType} via {@code as} for 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. + * By default, the table to operate on is derived from the initial {@literal domainType} and can be defined there + * with the {@link org.springframework.data.cassandra.core.mapping.Table} annotation as well. Using {@code inTable} + * allows a user to override the table name for the execution. * *

  *     
@@ -47,75 +48,103 @@ import org.springframework.lang.Nullable;
  * 
* * @author Mark Paluch + * @author John Blum + * @see org.springframework.data.cassandra.core.query.Query * @since 2.1 */ public interface ExecutableSelectOperation { /** - * Start creating a {@code SELECT} operation for the given {@literal domainType}. + * Begin creating a Cassandra {@code SELECT} query operation for the given {@link Class domainType}. * - * @param domainType must not be {@literal null}. + * @param {@link Class type} of the application domain object. + * @param domainType {@link Class type} to domain object to query; must not be {@literal null}. * @return new instance of {@link ExecutableSelect}. - * @throws IllegalArgumentException if domainType is {@literal null}. + * @throws IllegalArgumentException if {@link Class domainType} is {@literal null}. + * @see ExecutableSelect */ ExecutableSelect query(Class domainType); /** - * Trigger {@code SELECT} execution by calling one of the terminating methods. + * Table override (optional). + */ + interface SelectWithTable extends SelectWithQuery { + + /** + * Explicitly set the {@link String name} of the table on which to execute the query. + *

+ * Skip this step to use the default table derived from the {@link Class domain type}. + * + * @param table {@link String name} of the table; must not be {@literal null} or empty. + * @return new instance of {@link SelectWithProjection}. + * @throws IllegalArgumentException if {@link String table} is {@literal null} or empty. + * @see #inTable(CqlIdentifier) + * @see SelectWithProjection + */ + default SelectWithProjection inTable(String table) { + + Assert.hasText(table, "Table name must not be null or empty"); + + return inTable(CqlIdentifier.of(table)); + } + + /** + * Explicitly set the {@link CqlIdentifier name} of the table on which to execute the query. + *

+ * Skip this step to use the default table derived from the {@link Class domain type}. + * + * @param table {@link CqlIdentifier name} of the table; must not be {@literal null}. + * @return new instance of {@link SelectWithProjection}. + * @throws IllegalArgumentException if {@link CqlIdentifier table} is {@literal null}. + * @see org.springframework.data.cassandra.core.cql.CqlIdentifier + * @see SelectWithProjection + */ + SelectWithProjection inTable(CqlIdentifier table); + + } + + /** + * Result type override (optional). + */ + interface SelectWithProjection extends SelectWithQuery { + + /** + * Define the {@link Class result target type} that the Cassandra Row fields should be mapped to. + *

+ * Skip this step if you are anyway only interested in the original {@link Class domain type}. + * + * @param {@link Class type} of the result. + * @param resultType desired {@link Class target type} of the result; must not be {@literal null}. + * @return new instance of {@link SelectWithQuery}. + * @throws IllegalArgumentException if resultType is {@literal null}. + * @see SelectWithQuery + */ + SelectWithQuery as(Class resultType); + + } + + /** + * Filtering (optional). + */ + interface SelectWithQuery extends TerminatingSelect { + + /** + * Set the {@link Query} to use as a filter. + * + * @param query {@link Query} used as a filter; must not be {@literal null}. + * @return new instance of {@link TerminatingSelect}. + * @throws IllegalArgumentException if {@link Query} is {@literal null}. + * @see TerminatingSelect + */ + TerminatingSelect matching(Query query); + + } + + /** + * Trigger {@code SELECT} query 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. * @@ -127,72 +156,75 @@ public interface ExecutableSelectOperation { * Check for the presence of matching elements. * * @return {@literal true} if at least one matching element exists. + * @see #count() */ - boolean exists(); + default boolean exists() { + return count() > 0; + } + + /** + * Get the first result, or no result. + * + * @return the first result or {@link Optional#empty()} if no match found. + * @see #firstValue() + */ + default Optional first() { + return Optional.ofNullable(firstValue()); + } + + /** + * Get the first result, or no result. + * + * @return the first result or {@literal null} if no match found. + */ + @Nullable + T firstValue(); + + /** + * Get exactly zero or one result. + * + * @return a single result or {@link Optional#empty()} if no match found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + * @see #oneValue() + */ + default Optional one() { + return Optional.ofNullable(oneValue()); + } + + /** + * Get exactly zero or one result. + * + * @return the single result or {@literal null} if no match found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + @Nullable + T oneValue(); + + /** + * Get all matching elements. + * + * @return a {@link List} of all the matching elements; never {@literal null}. + * @see java.util.List + */ + List all(); + + /** + * Stream all matching elements. + * + * @return a {@link Stream} wrapping the Cassandra {@link com.datastax.driver.core.ResultSet}, + * which needs to be closed; never {@literal null}. + * @see java.util.stream.Stream + * @see #all() + */ + default Stream stream() { + return all().stream(); + } } /** - * 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. + * The {@link ExecutableSelect} interface provides methods for constructing {@code SELECT} query 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 index 506f6f062..945c9ee5e 100644 --- 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 @@ -15,14 +15,14 @@ */ package org.springframework.data.cassandra.core; +import java.util.List; +import java.util.stream.Stream; + 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; @@ -34,6 +34,8 @@ import org.springframework.util.ObjectUtils; * Implementation of {@link ExecutableSelectOperation}. * * @author Mark Paluch + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation + * @see org.springframework.data.cassandra.core.query.Query * @since 2.1 */ @RequiredArgsConstructor @@ -47,15 +49,21 @@ class ExecutableSelectOperationSupport implements ExecutableSelectOperation { @Override public ExecutableSelect query(Class domainType) { - Assert.notNull(domainType, "DomainType must not be null!"); + Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableSelectSupport<>(template, domainType, domainType, Query.empty(), null); + return new ExecutableSelectSupport<>(this.template, domainType, domainType, Query.empty(), null); } + // TODO: rethink the implementation + // While the use of final fields and construction on mutation effectively makes this class Thread-safe, + // it is possible this implementation could generate a high-level of young-gen garbage on the JVM heap, + // particularly if the template query(..) (and this class) are used inside of a loop for a large number + // of domain types. Of course, this assumption is highly contingent on the user's `Query` + // in addition to his/her application design. + @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) - static class ExecutableSelectSupport - implements ExecutableSelect, SelectWithTable, SelectWithProjection, SelectWithQuery { + static class ExecutableSelectSupport implements ExecutableSelect { @NonNull CassandraTemplate template; @@ -67,26 +75,16 @@ class ExecutableSelectOperationSupport implements ExecutableSelectOperation { @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!"); + Assert.notNull(tableName, "Table name must not be null"); - return new ExecutableSelectSupport<>(template, domainType, returnType, query, tableName); + return new ExecutableSelectSupport<>(this.template, this.domainType, this.returnType, + this.query, tableName); } /* (non-Javadoc) @@ -95,9 +93,10 @@ class ExecutableSelectOperationSupport implements ExecutableSelectOperation { @Override public SelectWithQuery as(Class returnType) { - Assert.notNull(returnType, "ReturnType must not be null!"); + Assert.notNull(returnType, "ReturnType must not be null"); - return new ExecutableSelectSupport<>(template, domainType, returnType, query, tableName); + return new ExecutableSelectSupport<>(this.template, this.domainType, returnType, + this.query, this.tableName); } /* (non-Javadoc) @@ -106,28 +105,26 @@ class ExecutableSelectOperationSupport implements ExecutableSelectOperation { @Override public TerminatingSelect matching(Query query) { - Assert.notNull(query, "Query must not be null!"); + Assert.notNull(query, "Query must not be null"); - return new ExecutableSelectSupport<>(template, domainType, returnType, query, tableName); + return new ExecutableSelectSupport<>(this.template, this.domainType, this.returnType, + query, this.tableName); } /* (non-Javadoc) - * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#oneValue() + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#count() */ @Override - public T oneValue() { + public long count() { + return this.template.doCount(this.query, this.domainType, getTableName()); + } - 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#exists() + */ + @Override + public boolean exists() { + return this.template.doExists(this.query, this.domainType, getTableName()); } /* (non-Javadoc) @@ -136,17 +133,39 @@ class ExecutableSelectOperationSupport implements ExecutableSelectOperation { @Override public T firstValue() { - List result = template.doSelect(query.limit(1), domainType, getTableName(), returnType); + List result = + this.template.doSelect(this.query.limit(1), this.domainType, getTableName(), this.returnType); return ObjectUtils.isEmpty(result) ? null : result.iterator().next(); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.ExecutableSelectOperation.TerminatingSelect#oneValue() + */ + @Override + public T oneValue() { + + List result = + this.template.doSelect(this.query.limit(2), this.domainType, getTableName(), this.returnType); + + if (ObjectUtils.isEmpty(result)) { + return null; + } + + if (result.size() > 1) { + throw new IncorrectResultSizeDataAccessException( + String.format("Query [%s] returned non unique result.", this.query), 1); + } + + return 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); + return this.template.doSelect(this.query, this.domainType, getTableName(), this.returnType); } /* (non-Javadoc) @@ -154,27 +173,11 @@ class ExecutableSelectOperationSupport implements ExecutableSelectOperation { */ @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()); + return this.template.doStream(this.query, this.domainType, getTableName(), this.returnType); } private CqlIdentifier getTableName() { - return tableName != null ? tableName : template.getTableName(domainType); + return this.tableName != null ? this.tableName : this.template.getTableName(this.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 index 213c35de2..ad44e657c 100644 --- 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 @@ -18,16 +18,17 @@ 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; +import org.springframework.util.Assert; /** - * {@link ExecutableUpdateOperation} allows creation and execution of Cassandra {@code UPDATE} operation in a fluent API - * style. + * {@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. + * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, + * as well as the {@link Update} provided 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 + * the developer to override the table name for the execution. * *

  *     
@@ -40,92 +41,99 @@ import org.springframework.data.cassandra.core.query.Update;
  * 
* * @author Mark Paluch + * @author John Blum + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation + * @see org.springframework.data.cassandra.core.query.Query + * @see org.springframework.data.cassandra.core.query.Update * @since 2.1 */ public interface ExecutableUpdateOperation { /** - * Start creating an {@code UPDATE} operation for the given {@literal domainType}. + * Begin creating an {@code UPDATE} operation for the given {@link Class domainType}. * - * @param domainType must not be {@literal null}. + * @param domainType {@link Class type} of domain object to update; must not be {@literal null}. * @return new instance of {@link ExecutableUpdate}. - * @throws IllegalArgumentException if domainType is {@literal null}. + * @throws IllegalArgumentException if {@link Class domainType} is {@literal null}. */ - ExecutableUpdate update(Class domainType); + ExecutableUpdate update(Class domainType); /** - * Declare the {@link Update} to apply. + * Table override (optional). */ - interface UpdateWithUpdate { + interface UpdateWithTable { /** - * 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. + * Explicitly set the {@link String name} of the table on which to execute the update. *

- * Skip this step to use the default table derived from the domain type. + * Skip this step to use the default table derived from the {@link Class 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}. + * @param table {@link String name} of the table; must not be {@literal null} or empty. * @return new instance of {@link UpdateWithQuery}. - * @throws IllegalArgumentException if query is {@literal null}. + * @throws IllegalArgumentException if {@link String table} is {@literal null} or empty. + * @see #inTable(CqlIdentifier) + * @see UpdateWithQuery */ - UpdateWithUpdate matching(Query query); - } + default UpdateWithQuery inTable(String table) { - /** - * Trigger update execution by calling one of the terminating methods. - */ - interface TerminatingUpdate { + Assert.hasText(table, "Table name must not be null or empty"); + + return inTable(CqlIdentifier.of(table)); + } /** - * Update all matching rows in the table. + * Explicitly set the {@link CqlIdentifier name} of the table on which to execute the update. + *

+ * Skip this step to use the default table derived from the {@link Class domain type}. * - * @return never {@literal null}. + * @param table {@link CqlIdentifier name} of the table; must not be {@literal null}. + * @return new instance of {@link UpdateWithQuery}. + * @throws IllegalArgumentException if {@link CqlIdentifier table} is {@literal null}. + * @see org.springframework.data.cassandra.core.cql.CqlIdentifier + * @see UpdateWithQuery */ - WriteResult all(); + UpdateWithQuery inTable(CqlIdentifier table); + } /** - * {@link ExecutableUpdate} provides methods for constructing {@code UPDATE} operations in a fluent way. + * Filtering (optional). */ - interface ExecutableUpdate extends UpdateWithTable, UpdateWithQuery {} + interface UpdateWithQuery { + + /** + * Filter rows with the given {@link Query}. + * + * @param query {@link Query} used to filter rows; must not be {@literal null}. + * @return new instance of {@link TerminatingUpdate}. + * @throws IllegalArgumentException if {@link Query} is {@literal null}. + * @see TerminatingUpdate + */ + TerminatingUpdate matching(Query query); + + } + + /** + * Set the {@link Update} to apply and execute the complete Cassandra {@link Update} statement. + */ + interface TerminatingUpdate { + + /** + * Apply the given {@link Update} and execute the complete Cassandra {@link Update} statement. + * + * @param update {@link Update} to apply; must not be {@literal null}. + * @return the {@link WriteResult result} of the update; never {@literal null}. + * @throws IllegalArgumentException if {@link Update} is {@literal null}. + * @see org.springframework.data.cassandra.core.WriteResult + */ + WriteResult apply(Update update); + + } + + /** + * The {@link ExecutableUpdate} interface 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 index 791bf4f88..30c10c500 100644 --- 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 @@ -30,6 +30,9 @@ import org.springframework.util.Assert; * Implementation of {@link ExecutableUpdateOperation}. * * @author Mark Paluch + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation + * @see org.springframework.data.cassandra.core.query.Query + * @see org.springframework.data.cassandra.core.query.Update * @since 2.1 */ @RequiredArgsConstructor @@ -41,82 +44,67 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation#update(java.lang.Class) */ @Override - public ExecutableUpdate update(Class domainType) { + public ExecutableUpdate update(Class domainType) { - Assert.notNull(domainType, "DomainType must not be null!"); + Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableUpdateSupport<>(template, domainType, Query.empty(), null, null); + return new ExecutableUpdateSupport(this.template, domainType, Query.empty(), null); } + // TODO: rethink the implementation + // While the use of final fields and construction on mutation effectively makes this class Thread-safe, + // it is possible this implementation could generate a high-level of young-gen garbage on the JVM heap, + // particularly if the template update(..) (and this class) are used inside of a loop for a large number + // of domain types. Of course, this assumption is highly contingent on the user's `Query` + // in addition to his/her application design. + @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) - static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithTable, UpdateWithQuery, - UpdateWithUpdate, TerminatingUpdate { + static class ExecutableUpdateSupport implements ExecutableUpdate, TerminatingUpdate { @NonNull CassandraTemplate template; - @NonNull Class domainType; + @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) { + public UpdateWithQuery inTable(CqlIdentifier tableName) { - Assert.notNull(tableName, "Table name must not be null!"); + Assert.notNull(tableName, "Table name must not be null"); - return new ExecutableUpdateSupport<>(template, domainType, query, update, tableName); + return new ExecutableUpdateSupport(this.template, this.domainType, this.query, 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) { + public TerminatingUpdate matching(Query query) { - Assert.notNull(query, "Query must not be null!"); + Assert.notNull(query, "Query must not be null"); - return new ExecutableUpdateSupport<>(template, domainType, query, update, tableName); + return new ExecutableUpdateSupport(this.template, this.domainType, query, this.tableName); } /* (non-Javadoc) - * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.TerminatingUpdate#all() + * @see org.springframework.data.cassandra.core.ExecutableUpdateOperation.TerminatingUpdate#apply(org.springframework.data.cassandra.core.query.Update) */ @Override - public WriteResult all() { - return template.doUpdate(query, update, domainType, getTableName()); + public WriteResult apply(Update update) { + + Assert.notNull(update, "Update must not be null"); + + return this.template.doUpdate(this.query, update, this.domainType, getTableName()); } private CqlIdentifier getTableName() { - return tableName != null ? tableName : template.getTableName(domainType); + return this.tableName != null ? this.tableName : this.template.getTableName(this.domainType); } } } 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 index 87175b94c..100ce13f3 100644 --- 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 @@ -15,16 +15,17 @@ */ 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.cassandra.core.query.Criteria.where; +import static org.springframework.data.cassandra.core.query.Query.query; import java.util.Collections; +import lombok.Data; + 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; @@ -69,20 +70,27 @@ public class ExecutableDeleteOperationSupportTests extends AbstractKeyspaceCreat @Test // DATACASS-485 public void removeAllMatching() { - WriteResult writeResult = template.delete(Person.class).matching(query(where("id").is(han.id))).all(); + WriteResult deleteResult = this.template + .delete(Person.class) + .matching(query(where("id").is(han.id))) + .all(); - assertThat(writeResult.wasApplied()).isTrue(); + assertThat(deleteResult).isNotNull(); + assertThat(deleteResult.wasApplied()).isTrue(); } @Test // DATACASS-485 public void removeAllMatchingWithAlternateDomainTypeAndCollection() { - WriteResult writeResult = template.delete(Jedi.class).inTable("person") + WriteResult deleteResult = this.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(); + assertThat(deleteResult).isNotNull(); + assertThat(deleteResult.wasApplied()).isTrue(); + assertThat(this.template.select(Query.empty(), Person.class)).isEmpty(); } @Data @@ -94,7 +102,6 @@ public class ExecutableDeleteOperationSupportTests extends AbstractKeyspaceCreat @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 index 1cb8652bd..bda3abf4e 100644 --- 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 @@ -15,14 +15,15 @@ */ package org.springframework.data.cassandra.core; -import static org.assertj.core.api.Assertions.*; - -import lombok.Data; +import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections; +import lombok.Data; + 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; @@ -52,51 +53,6 @@ public class ExecutableInsertOperationSupportTests extends AbstractKeyspaceCreat 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(); @@ -109,4 +65,53 @@ public class ExecutableInsertOperationSupportTests extends AbstractKeyspaceCreat luke.lastname = "skywalker"; luke.id = "id-2"; } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void domainTypeIsRequired() { + this.template.insert((Class) null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void tableIsRequiredOnSet() { + this.template.insert(Person.class).inTable((String) null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void optionsIsRequiredOnSet() { + this.template.insert(Person.class).withOptions(null); + } + + @Test // DATACASS-485 + public void insertOne() { + + WriteResult insertResult = this.template + .insert(Person.class) + .inTable("person") + .one(han); + + assertThat(insertResult.wasApplied()).isTrue(); + assertThat(this.template.selectOneById(han.id, Person.class)).isEqualTo(han); + } + + @Test // DATACASS-485 + public void insertOneWithOptions() { + + this.template.insert(Person.class).inTable("person").one(han); + + WriteResult insertResult = this.template + .insert(Person.class).inTable("person") + .withOptions(InsertOptions.builder().withIfNotExists().build()) + .one(han); + + assertThat(insertResult.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; + } } 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 index bca80ad6e..6a7660d9b 100644 --- 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 @@ -15,19 +15,20 @@ */ 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.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.cassandra.core.query.Criteria.where; +import static org.springframework.data.cassandra.core.query.Query.query; + +import java.util.Collections; +import java.util.stream.Stream; 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; @@ -55,269 +56,13 @@ public class ExecutableSelectOperationSupportTests extends AbstractKeyspaceCreat @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()); + this.template = new CassandraAdminTemplate(session, new MappingCassandraConverter()); + this.template.dropTable(true, CqlIdentifier.of("person")); + this.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() { @@ -334,4 +79,265 @@ public class ExecutableSelectOperationSupportTests extends AbstractKeyspaceCreat template.insert(han); template.insert(luke); } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void domainTypeIsRequired() { + this.template.query(null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void returnTypeIsRequiredOnSet() { + this.template.query(Person.class).as(null); + } + + @Test(expected = IllegalArgumentException.class) // DATACASS-485 + public void tableIsRequiredOnSet() { + this.template.query(Person.class).inTable((String) null); + } + + @Test // DATACASS-485 + public void findAll() { + assertThat(this.template.query(Person.class).all()).containsExactlyInAnyOrder(han, luke); + } + + @Test // DATACASS-485 + public void findAllWithCollection() { + assertThat(this.template.query(Human.class).inTable("person").all()).hasSize(2); + } + + @Test // DATACASS-485 + public void findAllWithProjection() { + assertThat(this.template.query(Person.class).as(Jedi.class).all()).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + + @Test // DATACASS-485 + public void findByReturningAllValuesAsClosedInterfaceProjection() { + assertThat(this.template.query(Person.class).as(PersonProjection.class).all()) + .hasOnlyElementsOfTypes(PersonProjection.class); + } + + @Test // DATACASS-485 + public void findAllBy() { + assertThat(this.template.query(Person.class).matching(queryLuke()).all()).containsExactlyInAnyOrder(luke); + } + + @Test // DATACASS-485 + public void findAllByWithCollectionUsingMappingInformation() { + assertThat(this.template.query(Jedi.class).inTable("person").all()) + .isNotEmpty().hasOnlyElementsOfType(Jedi.class); + } + + @Test // DATACASS-485 + public void findAllByWithCollection() { + assertThat(this.template.query(Human.class).inTable("person").matching(queryLuke()).all()).hasSize(1); + } + + @Test // DATACASS-485 + public void findAllByWithProjection() { + assertThat(this.template.query(Person.class).as(Jedi.class).all()) + .hasOnlyElementsOfType(Jedi.class).isNotEmpty(); + } + + @Test // DATACASS-485 + public void findBy() { + assertThat(this.template.query(Person.class).matching(queryLuke()).one()).contains(luke); + } + + @Test // DATACASS-485 + public void findByNoMatch() { + assertThat(this.template.query(Person.class).matching(querySpock()).one()).isEmpty(); + } + + @Test(expected = IncorrectResultSizeDataAccessException.class) // DATACASS-485 + public void findByTooManyResults() { + this.template.query(Person.class).one(); + } + + @Test // DATACASS-485 + public void findByReturningOneValue() { + assertThat(this.template.query(Person.class).matching(queryLuke()).oneValue()).isEqualTo(luke); + } + + @Test(expected = IncorrectResultSizeDataAccessException.class) // DATACASS-485 + public void findByReturningOneValueButTooManyResults() { + this.template.query(Person.class).oneValue(); + } + + @Test // DATACASS-485 + public void findByReturningFirstValue() { + assertThat(this.template.query(Person.class).matching(queryLuke()).firstValue()).isEqualTo(luke); + } + + @Test // DATACASS-485 + public void findByReturningFirstValueForManyResults() { + assertThat(this.template.query(Person.class).firstValue()).isIn(han, luke); + } + + @Test // DATACASS-485 + public void findByReturningFirstValueAsClosedInterfaceProjection() { + + PersonProjection result = this.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 = this.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 = this.template.query(Person.class).stream()) { + assertThat(stream).containsExactlyInAnyOrder(han, luke); + } + } + + @Test // DATACASS-485 + public void streamAllWithCollection() { + + Stream stream = this.template.query(Human.class).inTable("person").stream(); + + assertThat(stream).hasSize(2); + } + + @Test // DATACASS-485 + public void streamAllWithProjection() { + + try (Stream stream = this.template.query(Person.class).as(Jedi.class).stream()) { + assertThat(stream).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + } + + @Test // DATACASS-485 + public void streamAllReturningResultsAsClosedInterfaceProjection() { + + TerminatingSelect operation = + this.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 = + this.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 = this.template.query(Person.class).matching(queryLuke()).stream(); + + assertThat(stream).containsExactlyInAnyOrder(luke); + } + + @Test // DATACASS-485 + public void firstShouldReturnFirstEntryInCollection() { + assertThat(this.template.query(Person.class).first()).isNotEmpty(); + } + + @Test // DATACASS-485 + public void countShouldReturnNrOfElementsInCollectionWhenNoQueryPresent() { + assertThat(this.template.query(Person.class).count()).isEqualTo(2); + } + + @Test // DATACASS-485 + public void countShouldReturnNrOfElementsMatchingQuery() { + assertThat(this.template.query(Person.class).matching(query(where("firstname").is(luke.getFirstname())) + .withAllowFiltering()).count()).isEqualTo(1); + } + + @Test // DATACASS-485 + public void existsShouldReturnTrueIfAtLeastOneElementExistsInCollection() { + assertThat(this.template.query(Person.class).exists()).isTrue(); + } + + @Test // DATACASS-485 + public void existsShouldReturnFalseIfNoElementExistsInCollection() { + + this.template.truncate(Person.class); + + assertThat(this.template.query(Person.class).exists()).isFalse(); + } + + @Test // DATACASS-485 + public void existsShouldReturnTrueIfAtLeastOneElementMatchesQuery() { + assertThat(this.template.query(Person.class).matching(queryLuke()).exists()).isTrue(); + } + + @Test // DATACASS-485 + public void existsShouldReturnFalseWhenNoElementMatchesQuery() { + assertThat(this.template.query(Person.class).matching(querySpock()).exists()).isFalse(); + } + + @Test // DATACASS-485 + public void returnsTargetObjectDirectlyIfProjectionInterfaceIsImplemented() { + assertThat(this.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; + } } 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 index ddc3e16a1..7681c73c2 100644 --- 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 @@ -15,17 +15,18 @@ */ 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 static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.cassandra.core.query.Criteria.where; +import static org.springframework.data.cassandra.core.query.Query.query; +import static org.springframework.data.cassandra.core.query.Update.update; import java.util.Collections; +import lombok.Data; + 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; @@ -68,54 +69,68 @@ public class ExecutableUpdateOperationSupportTests extends AbstractKeyspaceCreat @Test(expected = IllegalArgumentException.class) // DATACASS-485 public void domainTypeIsRequired() { - template.update(null); + this.template.update(null); } @Test(expected = IllegalArgumentException.class) // DATACASS-485 public void queryIsRequired() { - template.update(Person.class).matching(null); + this.template.update(Person.class).matching(null); } @Test(expected = IllegalArgumentException.class) // DATACASS-485 public void tableIsRequiredOnSet() { - template.update(Person.class).inTable((CqlIdentifier) null); + this.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(); + WriteResult updateResult = this.template + .update(Person.class) + .matching(queryHan()) + .apply(update("firstname", "Han")); - assertThat(writeResult.wasApplied()).isTrue(); + assertThat(updateResult).isNotNull(); + assertThat(updateResult.wasApplied()).isTrue(); + assertThat(this.template.selectOne(queryLuke(), Person.class)).isEqualTo(luke); } @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(); + WriteResult updateResult = this.template + .update(Jedi.class) + .inTable("person") + .matching(query(where("id").is(han.getId()))) + .apply(update("name", "Han")); - assertThat(writeResult.wasApplied()).isTrue(); - assertThat(template.selectOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", - "Han"); + assertThat(updateResult).isNotNull(); + assertThat(updateResult.wasApplied()).isTrue(); + assertThat(this.template.selectOne(queryHan(), Person.class)) + .isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } private Query queryHan() { - return query(where("id").is(han.getId())); + return queryPerson(han); + } + + private Query queryLuke() { + return queryPerson(luke); + } + + private Query queryPerson(Person person) { + return query(where("id").is(person.getId())); } @Data @Table static class Person { - @Id String id; @Indexed String firstname; } @Data static class Jedi { - @Column("firstname") String name; } }