diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchOperations.java new file mode 100644 index 000000000..c2962c937 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchOperations.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core; + +/** + * Batch operations for insert/update/delete actions on a table. {@link CassandraBatchOperations} use logged Cassandra + * {@code BATCH}es for single entities and collections of entities. A {@link CassandraBatchOperations} instance cannot + * be modified/used once it was executed. + *

+ * Batches are atomic by default. In the context of a Cassandra batch operation, atomic means that if any of the batch + * succeeds, all of it will. Statement order does not matter within a batch. {@link CassandraBatchOperations} applies + * all rows using the same {@link #withTimestamp(long) timestamp} if supplied, otherwise Cassandra will generate a + * timestamp. + *

+ * Multi partition batches should only be used to achieve atomicity for a few writes on different tables. Apart from + * this they should be avoided because they’re too expensive. Single partition batches can be used to get atomicity and + * isolation, they're not much more expensive than normal writes. + * + * @author Mark Paluch + * @since 1.5 + */ +public interface CassandraBatchOperations { + + /** + * Execute the batch. The batch can be executed only once. + * + * @throws IllegalStateException if the batch is executed after it was executed once + */ + void execute(); + + /** + * Apply a given {@code timestamp} to the whole batch. + * + * @param timestamp the timestamp to apply. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations withTimestamp(long timestamp); + + /** + * Add a single insert to the batch. + * + * @param entity the entity to insert, must not be {@literal null}. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations insert(Object entity); + + /** + * Add a collection of inserts to the batch. + * + * @param entities the entities to insert, must not be {@literal null}. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations insert(Iterable entities); + + /** + * Add a single update to the batch. + * + * @param entity the entity to update, must not be {@literal null}. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations update(Object entity); + + /** + * Add a collection of updates to the batch. + * + * @param entities the entities to insert, must not be {@literal null}. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations update(Iterable entities); + + /** + * Add a single delete to the batch. + * + * @param entity the entity to delete, must not be {@literal null}. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations delete(Object entity); + + /** + * Add a collection of deletes to the batch. + * + * @param entities the entities to delete, must not be {@literal null}. + * @return {@code this} {@link CassandraBatchOperations} + * @throws IllegalStateException if the batch was already executed + */ + CassandraBatchOperations delete(Iterable entities); +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java new file mode 100644 index 000000000..a7dd68c99 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraBatchTemplate.java @@ -0,0 +1,177 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.util.Assert; + +import com.datastax.driver.core.querybuilder.Batch; +import com.datastax.driver.core.querybuilder.QueryBuilder; + +/** + * Default implementation for {@link CassandraBatchOperations}. + * + * @author Mark Paluch + * @since 1.5 + */ +class CassandraBatchTemplate implements CassandraBatchOperations { + + private final CassandraTemplate cassandraTemplate; + private final Batch batch; + private AtomicBoolean executed = new AtomicBoolean(); + + public CassandraBatchTemplate(CassandraTemplate cassandraTemplate) { + + Assert.notNull(cassandraTemplate, "CassandraTemplate must not be null"); + + this.cassandraTemplate = cassandraTemplate; + this.batch = QueryBuilder.batch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#execute() + */ + @Override + public void execute() { + + if (executed.compareAndSet(false, true)) { + cassandraTemplate.execute(batch); + return; + } + + ensureNotExecuted(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#withTimestamp(long) + */ + @Override + public CassandraBatchOperations withTimestamp(long timestamp) { + + ensureNotExecuted(); + + batch.using(QueryBuilder.timestamp(timestamp)); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#insert(java.lang.Object) + */ + @Override + public CassandraBatchOperations insert(Object entity) { + + ensureNotExecuted(); + Assert.notNull(entity, "Entity must not be null"); + + batch.add(cassandraTemplate.createInsertQuery(entity, null)); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#insert(java.lang.Iterable) + */ + @Override + public CassandraBatchOperations insert(Iterable entities) { + + ensureNotExecuted(); + Assert.notNull(entities, "Entities must not be null"); + + for (Object entity : entities) { + + Assert.notNull(entity, "Entity must not be null"); + batch.add(cassandraTemplate.createInsertQuery(entity, null)); + } + + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#update(java.lang.Object) + */ + @Override + public CassandraBatchOperations update(Object entity) { + + ensureNotExecuted(); + Assert.notNull(entity, "Entity must not be null"); + + batch.add(cassandraTemplate.createUpdateQuery(entity, null)); + + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#update(java.lang.Iterable) + */ + @Override + public CassandraBatchOperations update(Iterable entities) { + + ensureNotExecuted(); + Assert.notNull(entities, "Entities must not be null"); + + for (Object entity : entities) { + + Assert.notNull(entity, "Entity must not be null"); + batch.add(cassandraTemplate.createUpdateQuery(entity, null)); + } + + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#delete(java.lang.Object) + */ + @Override + public CassandraBatchOperations delete(Object entity) { + + ensureNotExecuted(); + Assert.notNull(entity, "Entity must not be null"); + + batch.add(cassandraTemplate.createDeleteQuery(entity, null)); + + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraBatchOperations#delete(java.lang.Iterable) + */ + @Override + public CassandraBatchOperations delete(Iterable entities) { + + ensureNotExecuted(); + Assert.notNull(entities, "Entities must not be null"); + + for (Object entity : entities) { + + Assert.notNull(entity, "Entity must not be null"); + batch.add(cassandraTemplate.createDeleteQuery(entity, null)); + } + + return this; + } + + private void ensureNotExecuted() { + Assert.state(!executed.get(), "This Cassandra Batch was already executed"); + } +} 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 da1d045a9..83346d47e 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 @@ -59,7 +59,7 @@ public interface CassandraOperations extends CqlOperations { * Returns a {@link java.util.Iterator} that wraps the Cassandra {@link com.datastax.driver.core.ResultSet}. * * @param element return type. - * @param query query to execute. Must not be empty or {@literal null}. + * @param query query to execute. Must not be empty or {@literal null}. * @param entityClass Class type of the elements in the {@link Iterator} stream. Must not be {@literal null}. * @return an {@link Iterator} (stream) over the elements in the query result set. * @since 1.5 @@ -465,6 +465,7 @@ public interface CassandraOperations extends CqlOperations { /** * Deletes all entities of a given class. + * * @param entityClass The entity type must not be {@literal null}. */ void deleteAll(Class entityClass); @@ -533,6 +534,14 @@ public interface CassandraOperations extends CqlOperations { */ Cancellable deleteAsynchronously(List entities, DeletionListener listener, QueryOptions options); + /** + * Returns a new {@link CassandraBatchOperations}. Each {@link CassandraBatchOperations} instance can be executed only + * once so you might want to obtain new {@link CassandraBatchOperations} instances for each batch. + * + * @return a new {@link CassandraBatchOperations} associated with the given entity class. + */ + CassandraBatchOperations batchOps(); + /** * Returns the underlying {@link CassandraConverter}. * 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 07669c4b7..93c02817f 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 @@ -704,16 +704,23 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation protected T doInsert(T entity, WriteOptions options) { Assert.notNull(entity, "Entity must not be null"); - Insert insert = createInsertQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter); + Insert insert = createInsertQuery(entity, options); execute(insert); return entity; } + Insert createInsertQuery(T entity, WriteOptions options) { + + Assert.notNull(entity, "Entity must not be null"); + + return createInsertQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter); + } + protected Cancellable doInsertAsync(final T entity, final WriteListener listener, WriteOptions options) { Assert.notNull(entity, "Entity must not be null"); - Insert insert = createInsertQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter); + Insert insert = createInsertQuery(entity, options); AsynchronousQueryListener queryListener = (listener == null ? null : new AsynchronousQueryListener() { @@ -840,17 +847,24 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation return executeAsynchronously(batch, queryListener); } + Delete createDeleteQuery(T entity, QueryOptions options) { + + Assert.notNull(entity, "Entity must not be null"); + + return createDeleteQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter); + } + protected void doDelete(T entity, QueryOptions options) { Assert.notNull(entity, "Entity must not be null"); - execute(createDeleteQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter)); + execute(createDeleteQuery(entity, options)); } protected Cancellable doDeleteAsync(final T entity, final DeletionListener listener, QueryOptions options) { Assert.notNull(entity, "Entity must not be null"); - Delete delete = createDeleteQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter); + Delete delete = createDeleteQuery(entity, options); AsynchronousQueryListener queryListener = (listener == null ? null : new AsynchronousQueryListener() { @Override @@ -867,10 +881,17 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation return executeAsynchronously(delete, queryListener); } + Update createUpdateQuery(T entity, WriteOptions options) { + + Assert.notNull(entity, "Entity must not be null"); + + return createUpdateQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter); + } + protected T doUpdate(T entity, WriteOptions options) { Assert.notNull(entity, "Entity must not be null"); - execute(createUpdateQuery(getTableName(entity.getClass()).toCql(), entity, options, cassandraConverter)); + execute(createUpdateQuery(entity, options)); return entity; } @@ -893,8 +914,16 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation } }); - return executeAsynchronously(createUpdateQuery(getTableName(entity.getClass()).toCql(), entity, options, - cassandraConverter), queryListener); + return executeAsynchronously(createUpdateQuery(entity, options), queryListener); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#batchOps(java.lang.Class) + */ + @Override + public CassandraBatchOperations batchOps() { + return new CassandraBatchTemplate(this); } /** diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java new file mode 100644 index 000000000..bd0768347 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraBatchTemplateIntegrationTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.domain.FlatGroup; +import org.springframework.data.cassandra.domain.Group; +import org.springframework.data.cassandra.domain.GroupKey; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; + +/** + * Integration tests for {@link CassandraBatchTemplate}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class CassandraBatchTemplateIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { Group.class.getPackage().getName() }; + } + } + + @Autowired CassandraTemplate cassandraTemplate; + + @Before + public void setUp() throws Exception { + cassandraTemplate.deleteAll(Group.class); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldInsertEntities() { + + Group walter = new Group(new GroupKey("users", "0x1", "walter")); + Group mike = new Group(new GroupKey("users", "0x1", "mike")); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.insert(walter).insert(mike).execute(); + + Group loaded = cassandraTemplate.selectOneById(Group.class, walter.getId()); + assertThat(loaded.getId().getUsername(), is(equalTo(walter.getId().getUsername()))); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldInsertCollectionOfEntities() { + + Group walter = new Group(new GroupKey("users", "0x1", "walter")); + Group mike = new Group(new GroupKey("users", "0x1", "mike")); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.insert(Arrays.asList(walter, mike)).execute(); + + Group loaded = cassandraTemplate.selectOneById(Group.class, walter.getId()); + assertThat(loaded.getId().getUsername(), is(equalTo(walter.getId().getUsername()))); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldUpdateEntities() { + + Group walter = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "walter"))); + Group mike = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "mike"))); + + walter.setEmail("walter@white.com"); + mike.setEmail("mike@sauls.com"); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.update(walter).update(mike).execute(); + + Group loaded = cassandraTemplate.selectOneById(Group.class, walter.getId()); + assertThat(loaded.getEmail(), is(equalTo(walter.getEmail()))); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldUpdateCollectionOfEntities() { + + Group walter = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "walter"))); + Group mike = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "mike"))); + + walter.setEmail("walter@white.com"); + mike.setEmail("mike@sauls.com"); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.update(Arrays.asList(walter, mike)).execute(); + + Group loaded = cassandraTemplate.selectOneById(Group.class, walter.getId()); + assertThat(loaded.getEmail(), is(equalTo(walter.getEmail()))); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldUpdatesCollectionOfEntities() { + + FlatGroup walter = cassandraTemplate.insert(new FlatGroup("users", "0x1", "walter")); + FlatGroup mike = cassandraTemplate.insert(new FlatGroup("users", "0x1", "mike")); + + walter.setEmail("walter@white.com"); + mike.setEmail("mike@sauls.com"); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.update(Arrays.asList(walter, mike)).execute(); + + FlatGroup loaded = cassandraTemplate.selectOneById(FlatGroup.class, walter); + assertThat(loaded.getEmail(), is(equalTo(walter.getEmail()))); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldDeleteEntities() { + + Group walter = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "walter"))); + Group mike = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "mike"))); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + + batchOperations.delete(walter).delete(mike).execute(); + + Group loaded = cassandraTemplate.selectOneById(Group.class, walter.getId()); + assertThat(loaded, is(nullValue())); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldDeleteCollectionOfEntities() { + + Group walter = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "walter"))); + Group mike = cassandraTemplate.insert(new Group(new GroupKey("users", "0x1", "mike"))); + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + + batchOperations.delete(Arrays.asList(walter, mike)).execute(); + + Group loaded = cassandraTemplate.selectOneById(Group.class, walter.getId()); + assertThat(loaded, is(nullValue())); + } + + /** + * @see DATACASS-288 + */ + @Test + public void shouldApplyTimestampToAllEntities() { + + Group walter = new Group(new GroupKey("users", "0x1", "walter")); + Group mike = new Group(new GroupKey("users", "0x1", "mike")); + + walter.setEmail("walter@white.com"); + mike.setEmail("mike@sauls.com"); + + long timestamp = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) * 1000; + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.insert(walter).insert(mike).withTimestamp(timestamp).execute(); + + ResultSet resultSet = cassandraTemplate.query("SELECT writetime(email) FROM group;"); + + assertThat(resultSet.getAvailableWithoutFetching(), is(2)); + for (Row row : resultSet) { + assertThat(row.getLong(0), is(timestamp)); + } + } + + /** + * @see DATACASS-288 + */ + @Test(expected = IllegalStateException.class) + public void shouldNotExecuteTwice() { + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.insert(new Group(new GroupKey("users", "0x1", "walter"))).execute(); + + batchOperations.execute(); + fail("Missing IllegalStateException"); + } + + /** + * @see DATACASS-288 + */ + @Test(expected = IllegalStateException.class) + public void shouldNotAllowModificationAfterExecution() { + + CassandraBatchOperations batchOperations = new CassandraBatchTemplate(cassandraTemplate); + batchOperations.insert(new Group(new GroupKey("users", "0x1", "walter"))).execute(); + + batchOperations.update(new Group()); + fail("Missing IllegalStateException"); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java index d2be01a58..458863e86 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java @@ -29,12 +29,15 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.test.integration.simpletons.Book; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; +import com.datastax.driver.core.querybuilder.Batch; import com.datastax.driver.core.querybuilder.Select; /** @@ -167,4 +170,15 @@ public class CassandraTemplateUnitTests { verify(mockSession, times(1)).execute(eq("SELECT * FROM Test")); verifyZeroInteractions(mockCassandraConverter); } + + /** + * @see DATACASS-288 + */ + @Test + public void batchOperationsShouldCallSession() { + + template.batchOps().insert(new Book()).execute(); + + verify(mockSession).execute(Mockito.any(Batch.class)); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/FlatGroup.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/FlatGroup.java new file mode 100644 index 000000000..ce43bf9f5 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/FlatGroup.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.domain; + +import lombok.Data; + +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; +import org.springframework.data.cassandra.mapping.Table; + +/** + * @author Mark Paluch + * @see http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling + */ +@Table +@Data +public class FlatGroup { + + @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) private String groupname; + @PrimaryKeyColumn(name = "hash_prefix", ordinal = 2, type = PrimaryKeyType.PARTITIONED) private String hashPrefix; + @PrimaryKeyColumn(ordinal = 3, type = PrimaryKeyType.CLUSTERED) private String username; + + private String email; + private int age; + + public FlatGroup(String groupname, String hashPrefix, String username) { + this.groupname = groupname; + this.hashPrefix = hashPrefix; + this.username = username; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Group.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Group.java new file mode 100644 index 000000000..0e9cb27bd --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Group.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.domain; + +import org.springframework.data.cassandra.mapping.PrimaryKey; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + * @see http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling + */ +@Table +@Data +@NoArgsConstructor +public class Group { + + @PrimaryKey private GroupKey id; + + private String email; + private int age; + + public Group(GroupKey id) { + this.id = id; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/GroupKey.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/GroupKey.java new file mode 100644 index 000000000..c368c39fa --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/GroupKey.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.domain; + +import java.io.Serializable; + +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.data.cassandra.mapping.PrimaryKeyClass; +import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + * @see http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling + */ +@PrimaryKeyClass +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GroupKey implements Serializable { + + @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) private String groupname; + @PrimaryKeyColumn(name = "hash_prefix", ordinal = 2, type = PrimaryKeyType.PARTITIONED) private String hashPrefix; + @PrimaryKeyColumn(ordinal = 3, type = PrimaryKeyType.CLUSTERED) private String username; +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/simpletons/Book.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/simpletons/Book.java index b349aa3a3..f533f1dc0 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/simpletons/Book.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/simpletons/Book.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,18 @@ import java.util.Date; import org.springframework.data.cassandra.mapping.PrimaryKey; import org.springframework.data.cassandra.mapping.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + /** * Test POJO * * @author David Webb + * @author Mark Paluch */ @Table("book") +@Data +@NoArgsConstructor public class Book { @PrimaryKey private String isbn; @@ -37,115 +43,7 @@ public class Book { private boolean isInStock; private BookCondition condition; - /** - * @return Returns the isbn. - */ - public String getIsbn() { - return isbn; - } - - /** - * @return Returns the saleDate. - */ - public Date getSaleDate() { - return saleDate; - } - - /** - * @param saleDate The saleDate to set. - */ - public void setSaleDate(Date saleDate) { - this.saleDate = saleDate; - } - - /** - * @return Returns the isInStock. - */ - public boolean isInStock() { - return isInStock; - } - - /** - * @param isInStock The isInStock to set. - */ - public void setInStock(boolean isInStock) { - this.isInStock = isInStock; - } - - /** - * @param isbn The isbn to set. - */ - public void setIsbn(String isbn) { + public Book(String isbn) { this.isbn = isbn; } - - /** - * @return Returns the title. - */ - public String getTitle() { - return title; - } - - /** - * @param title The title to set. - */ - public void setTitle(String title) { - this.title = title; - } - - /** - * @return Returns the author. - */ - public String getAuthor() { - return author; - } - - /** - * @param author The author to set. - */ - public void setAuthor(String author) { - this.author = author; - } - - /** - * @return Returns the pages. - */ - public int getPages() { - return pages; - } - - /** - * @param pages The pages to set. - */ - public void setPages(int pages) { - this.pages = pages; - } - - /** - * @param condition The condition to set. - */ - public void setCondition(BookCondition condition) { - this.condition = condition; - } - - /** - * @return Returns the condition. - */ - public BookCondition getCondition() { - return condition; - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("isbn -> " + isbn).append("\n"); - sb.append("tile -> " + title).append("\n"); - sb.append("author -> " + author).append("\n"); - sb.append("pages -> " + pages).append("\n"); - sb.append("condition -> " + condition).append("\n"); - return sb.toString(); - } }