diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/BasicConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/BasicConfiguration.java new file mode 100644 index 00000000..fe971aca --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/BasicConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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 example.springdata.cassandra.optimisticlocking; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; + +/** + * Basic {@link Configuration} to create the necessary schema for the {@link OptimisticPerson} table. + * + * @author Mark Paluch + */ +@SpringBootApplication +@EntityScan(basePackageClasses = OptimisticPerson.class) +class BasicConfiguration { + +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/OptimisticPerson.java b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/OptimisticPerson.java new file mode 100644 index 00000000..322bb0cf --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/OptimisticPerson.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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 example.springdata.cassandra.optimisticlocking; + +import lombok.Data; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.cassandra.core.mapping.Table; + +/** + * Simple domain object that declares properties annotated with Spring Data's {@code @Version} annotation to enable the + * object for optimistic locking. + * + * @author Mark Paluch + */ +@Data +@Table +public class OptimisticPerson { + + @Id Long id; + + @Version long version; + + String name; + +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepository.java new file mode 100644 index 00000000..c106e593 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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 example.springdata.cassandra.optimisticlocking; + +import org.springframework.data.repository.CrudRepository; + +/** + * Simple repository interface for {@link OptimisticPerson} instances. + * + * @author Mark Paluch + */ +public interface OptimisticPersonRepository extends CrudRepository {} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/SimplePerson.java b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/SimplePerson.java new file mode 100644 index 00000000..82ed760f --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/optimisticlocking/SimplePerson.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 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 example.springdata.cassandra.optimisticlocking; + +import lombok.Data; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.core.mapping.Table; + +/** + * Simple domain object. + * + * @author Mark Paluch + */ +@Data +@Table +public class SimplePerson { + + @Id Long id; + + String name; + +} diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java index 7de35346..99b54fc4 100644 --- a/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java +++ b/cassandra/example/src/test/java/example/springdata/cassandra/auditing/AuditedPersonRepositoryTests.java @@ -32,7 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; /** - * Integration test showing the basic usage of {@link AuditedPersonRepository}. + * Integration test showing the basic usage of Auditing through {@link AuditedPersonRepository}. * * @author Mark Paluch */ diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepositoryTests.java new file mode 100644 index 00000000..e081c641 --- /dev/null +++ b/cassandra/example/src/test/java/example/springdata/cassandra/optimisticlocking/OptimisticPersonRepositoryTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020 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 example.springdata.cassandra.optimisticlocking; + +import static org.assertj.core.api.Assertions.*; + +import example.springdata.cassandra.util.CassandraKeyspace; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.EntityWriteResult; +import org.springframework.data.cassandra.core.UpdateOptions; +import org.springframework.data.cassandra.core.query.Criteria; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration test showing the basic usage of Optimistic Locking through {@link OptimisticPersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BasicConfiguration.class) +public class OptimisticPersonRepositoryTests { + + @ClassRule public final static CassandraKeyspace CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost(); + + @Autowired OptimisticPersonRepository repository; + @Autowired CassandraOperations operations; + + @Before + public void setUp() { + repository.deleteAll(); + } + + /** + * Saving an object using the Cassandra Repository will create a persistent representation of the object in Cassandra + * and increment the version property. + */ + @Test + public void insertShouldIncrementVersion() { + + OptimisticPerson person = new OptimisticPerson(); + person.setId(42L); + person.setName("Walter White"); + + OptimisticPerson saved = repository.save(person); + + assertThat(saved.getVersion()).isGreaterThan(0); + } + + /** + * Modifying an existing object will update the last modified fields. + */ + @Test + public void updateShouldDetectChangedEntity() { + + OptimisticPerson person = new OptimisticPerson(); + person.setId(42L); + person.setName("Walter White"); + + // Load the person because we intend to change it. + OptimisticPerson saved = repository.save(person); + + // Another process has changed the person object in the meantime. + OptimisticPerson anotherProcess = repository.findById(person.getId()).get(); + anotherProcess.setName("Heisenberg"); + repository.save(anotherProcess); + + // Now it's our turn to modify the object... + saved.setName("Walter"); + + // ...which fails with a OptimisticLockingFailureException, using LWT under the hood. + assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> repository.save(saved)); + } + + /** + * This tests uses lightweight transactions by leveraging mapped {@code IF} conditions with the {@code UPDATE} + * statement through {@link CassandraOperations#update(Object, UpdateOptions)}. + */ + @Test + public void updateUsingLightWeightTransactions() { + + SimplePerson person = new SimplePerson(); + person.setId(42L); + person.setName("Walter White"); + + operations.insert(person); + + EntityWriteResult success = operations.update(person, + UpdateOptions.builder().ifCondition(Criteria.where("name").is("Walter White")).build()); + + assertThat(success.wasApplied()).isTrue(); + + EntityWriteResult failed = operations.update(person, + UpdateOptions.builder().ifCondition(Criteria.where("name").is("Heisenberg")).build()); + + assertThat(failed.wasApplied()).isFalse(); + } + +}