diff --git a/.travis.yml b/.travis.yml index 9f82cf44..41503a71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ jdk: services: - redis-server + - cassandra cache: directories: @@ -14,4 +15,4 @@ sudo: false install: true -script: "mvn clean dependency:list test -Dsort" \ No newline at end of file +script: "mvn clean dependency:list test -Dsort" diff --git a/cassandra/example/Readme.MD b/cassandra/example/Readme.MD index ed96c1d5..be5d0a2b 100644 --- a/cassandra/example/Readme.MD +++ b/cassandra/example/Readme.MD @@ -6,8 +6,6 @@ Before we can start we have to install Cassandra, e.g. via brew on Max OS. ``` brew install cassandra -pip install cassandra-driver -brew install cassandra-driver More details can be found here: https://wiki.apache.org/cassandra/GettingStarted ``` @@ -17,26 +15,5 @@ More details can be found here: https://wiki.apache.org/cassandra/GettingStarted /usr/local/bin/cassandra -f ``` -### Start a Cassandra shell cqlsh -``` -/usr/local/bin/cqlsh -``` - -### Setup Keyspace and Tables - -``` -CREATE KEYSPACE example -WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }; - -USE example; - -CREATE TABLE users ( - user_id int PRIMARY KEY, - uname text, - fname text, - lname text -); -``` - That should be enough to get you started. -Now you can simply type ```mvn clean install``` to run the example. \ No newline at end of file +Now you can simply type ```mvn clean install``` to run the example. diff --git a/cassandra/example/pom.xml b/cassandra/example/pom.xml index 5caab14a..d3624fcc 100644 --- a/cassandra/example/pom.xml +++ b/cassandra/example/pom.xml @@ -12,6 +12,22 @@ Spring Data Cassandra - Example - Small sample project showing the usage of Spring Data Cassandra. - + Basic sample project showing the usage of Spring Data Cassandra. + + + + + ${project.groupId} + spring-data-cassandra-example-utils + ${project.version} + test + + + + com.fasterxml.jackson.core + jackson-databind + + + + diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/SimpleConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java similarity index 65% rename from cassandra/example/src/main/java/example/springdata/cassandra/SimpleConfiguration.java rename to cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java index 259934a2..38a27f8a 100644 --- a/cassandra/example/src/main/java/example/springdata/cassandra/SimpleConfiguration.java +++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 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. @@ -13,37 +13,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package example.springdata.cassandra; +package example.springdata.cassandra.basic; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cassandra.config.java.AbstractCqlTemplateConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration; import org.springframework.data.cassandra.core.CassandraTemplate; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; import com.datastax.driver.core.Session; /** + * Basic {@link Configuration} to create the necessary schema for the {@link User} table. + * * @author Oliver Gierke * @author Thomas Darimont + * @author Mark Paluch */ @Configuration @EnableAutoConfiguration -class SimpleConfiguration { +class BasicConfiguration { @Configuration @EnableCassandraRepositories - static class CassandraConfig extends AbstractCqlTemplateConfiguration { + static class CassandraConfig extends AbstractCassandraConfiguration { @Override public String getKeyspaceName() { return "example"; } - + @Bean public CassandraTemplate cassandraTemplate(Session session) { return new CassandraTemplate(session); } + + @Override + public String[] getEntityBasePackages() { + return new String[] { User.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE; + } } } diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java new file mode 100644 index 00000000..a02b580a --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/BasicUserRepository.java @@ -0,0 +1,58 @@ +/* + * 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. + * 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.basic; + +import java.util.List; + +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.repository.CrudRepository; + +/** + * Simple repository interface for {@link User} instances. The interface is used to declare so called query methods, + * methods to retrieve single entities or collections of them. + * + * @author Thomas Darimont + */ +public interface BasicUserRepository extends CrudRepository { + + /** + * Sample method annotated with {@link Query}. This method executes the CQL from the {@link Query} value. + * + * @param id + * @return + */ + @Query("SELECT * from users where user_id in(?0)") + User findUserByIdIn(long id); + + /** + * Derived query method. This query corresponds with {@code SELECT * FROM users WHERE uname = ?0}. + * {@link User#username} is not part of the primary so it requires a secondary index. + * + * @param username + * @return + */ + User findUserByUsername(String username); + + /** + * Derived query method using SASI (SSTable Attached Secondary Index) features through the {@code LIKE} keyword. This + * query corresponds with {@code SELECT * FROM users WHERE uname LIKE '?0%'}. {@link User#username} is not part of the + * primary so it requires a secondary index. + * + * @param lastnamePrefix + * @return + */ + List findUsersByLastnameStartsWith(String lastnamePrefix); +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/User.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/User.java similarity index 92% rename from cassandra/example/src/main/java/example/springdata/cassandra/User.java rename to cassandra/example/src/main/java/example/springdata/cassandra/basic/User.java index e4662e1f..7203c6b3 100644 --- a/cassandra/example/src/main/java/example/springdata/cassandra/User.java +++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/User.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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package example.springdata.cassandra; +package example.springdata.cassandra.basic; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/package-info.java b/cassandra/example/src/main/java/example/springdata/cassandra/basic/package-info.java similarity index 71% rename from cassandra/example/src/main/java/example/springdata/cassandra/package-info.java rename to cassandra/example/src/main/java/example/springdata/cassandra/basic/package-info.java index 6e1df637..177b26e6 100644 --- a/cassandra/example/src/main/java/example/springdata/cassandra/package-info.java +++ b/cassandra/example/src/main/java/example/springdata/cassandra/basic/package-info.java @@ -1,5 +1,5 @@ /** * Package showing a simple repository interface to use basic query method execution functionality. */ -package example.springdata.cassandra; +package example.springdata.cassandra.basic; diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/SimpleUserRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java similarity index 57% rename from cassandra/example/src/main/java/example/springdata/cassandra/SimpleUserRepository.java rename to cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java index 3989172f..0caff593 100644 --- a/cassandra/example/src/main/java/example/springdata/cassandra/SimpleUserRepository.java +++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Addressbook.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * 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. @@ -13,14 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package example.springdata.cassandra; +package example.springdata.cassandra.convert; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Data; + +import java.util.List; /** - * Simple repository interface for {@link User} instances. The interface is used to declare so called query methods, - * methods to retrieve single entities or collections of them. + * Sample Addressbook class. * - * @author Thomas Darimont + * @author Mark Paluch */ -public interface SimpleUserRepository extends CrudRepository {} +@Data +@Table +public class Addressbook { + + @Id String id; + + Contact me; + List friends; +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java new file mode 100644 index 00000000..1a4ebf58 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/Contact.java @@ -0,0 +1,34 @@ +/* + * 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 example.springdata.cassandra.convert; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Sample Contact class. + * + * @author Mark Paluch + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Contact { + + String firstname; + String lastname; +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java new file mode 100644 index 00000000..b813798b --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/ConverterConfiguration.java @@ -0,0 +1,116 @@ +/* + * 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 example.springdata.cassandra.convert; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.util.StringUtils; + +import com.datastax.driver.core.Row; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link Configuration} class to register custom converters. + * + * @author Mark Paluch + */ +@Configuration +@EnableCassandraRepositories +class ConverterConfiguration extends AbstractCassandraConfiguration { + + @Override + public String getKeyspaceName() { + return "example"; + } + + @Override + public String[] getEntityBasePackages() { + return new String[] { Addressbook.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE; + } + + @Override + public CustomConversions customConversions() { + + List> converters = new ArrayList<>(); + converters.add(new PersonWriteConverter()); + converters.add(new PersonReadConverter()); + converters.add(new CustomAddressbookReadConverter()); + + return new CustomConversions(converters); + } + + /** + * Write a {@link Contact} into its {@link String} representation. + */ + static class PersonWriteConverter implements Converter { + + public String convert(Contact source) { + + try { + return new ObjectMapper().writeValueAsString(source); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + + /** + * Read a {@link Contact} from its {@link String} representation. + */ + static class PersonReadConverter implements Converter { + + public Contact convert(String source) { + + if (StringUtils.hasText(source)) { + try { + return new ObjectMapper().readValue(source, Contact.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + return null; + } + } + + /** + * Perform custom mapping by reading a {@link Row} into a custom class. + */ + static class CustomAddressbookReadConverter implements Converter { + + public CustomAddressbook convert(Row source) { + + CustomAddressbook result = new CustomAddressbook(); + + result.setTheId(source.getString("id")); + result.setMyDetailsAsJson(source.getString("me")); + + return result; + } + } +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/CustomAddressbook.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/CustomAddressbook.java new file mode 100644 index 00000000..3a0e15ea --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/CustomAddressbook.java @@ -0,0 +1,28 @@ +/* + * 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 example.springdata.cassandra.convert; + +import lombok.Data; + +/** + * @author Mark Paluch + */ +@Data +public class CustomAddressbook { + + String theId; + String myDetailsAsJson; +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/convert/package-info.java b/cassandra/example/src/main/java/example/springdata/cassandra/convert/package-info.java new file mode 100644 index 00000000..414f660e --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/convert/package-info.java @@ -0,0 +1,4 @@ +/** + * Package showing conversion features. + */ +package example.springdata.cassandra.convert; diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java new file mode 100644 index 00000000..8255dfb3 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/Customer.java @@ -0,0 +1,32 @@ +/* + * 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 example.springdata.cassandra.projection; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Value; + +/** + * @author Mark Paluch + */ +@Value +@Table +class Customer { + + @Id String id; + String firstname, lastname; +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java new file mode 100644 index 00000000..4a2f9d36 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerProjection.java @@ -0,0 +1,26 @@ +/* + * 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 example.springdata.cassandra.projection; + +/** + * An example projection interface containing only the firstname. + * + * @author Mark Paluch + */ +interface CustomerProjection { + + String getFirstname(); +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java new file mode 100644 index 00000000..fee61443 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerRepository.java @@ -0,0 +1,72 @@ +/* + * 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 example.springdata.cassandra.projection; + +import java.util.Collection; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.repository.CrudRepository; + +/** + * Sample repository managing customers to show projecting functionality of Spring Data Cassandra. + * + * @author Mark Paluch + */ +interface CustomerRepository extends CrudRepository { + + /** + * Uses a projection interface to indicate the fields to be returned. As the projection doesn't use any dynamic + * fields, the query execution will be restricted to only the fields needed by the projection. + * + * @return + */ + Collection findAllProjectedBy(); + + /** + * When a projection is used that contains dynamic properties (i.e. SpEL expressions in an {@link Value} annotation), + * the normal target entity will be loaded but dynamically projected so that the target can be referred to in the + * expression. + * + * @return + */ + Collection findAllSummarizedBy(); + + /** + * Passes in the projection type dynamically. + * + * @param id + * @param projection + * @return + */ + Collection findById(String id, Class projection); + + /** + * Projection for a single entity. + * + * @param id + * @return + */ + CustomerProjection findProjectedById(String id); + + /** + * Dynamic projection for a single entity. + * + * @param id + * @param projection + * @return + */ + T findProjectedById(String id, Class projection); +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java new file mode 100644 index 00000000..f0a5c816 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/CustomerSummary.java @@ -0,0 +1,29 @@ +/* + * 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 example.springdata.cassandra.projection; + +import org.springframework.beans.factory.annotation.Value; + +/** + * An example of using SpEL with projections. + * + * @author Mark Paluch + */ +interface CustomerSummary { + + @Value("#{target.firstname + ' ' + target.lastname}") + String getFullName(); +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java new file mode 100644 index 00000000..0758a458 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/ProjectionConfiguration.java @@ -0,0 +1,58 @@ +/* + * 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 example.springdata.cassandra.projection; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; + +import com.datastax.driver.core.Session; + +import example.springdata.cassandra.basic.User; + +/** + * Basic {@link Configuration} to create the necessary schema for the {@link Customer} table. + * + * @author Mark Paluch + */ +@Configuration +@EnableAutoConfiguration +class ProjectionConfiguration { + + @Configuration + @EnableCassandraRepositories + static class CassandraConfig extends AbstractCassandraConfiguration { + + @Override + public String getKeyspaceName() { + return "example"; + } + + @Override + public String[] getEntityBasePackages() { + return new String[] { Customer.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE; + } + } +} diff --git a/cassandra/example/src/main/java/example/springdata/cassandra/projection/package-info.java b/cassandra/example/src/main/java/example/springdata/cassandra/projection/package-info.java new file mode 100644 index 00000000..e81c8c64 --- /dev/null +++ b/cassandra/example/src/main/java/example/springdata/cassandra/projection/package-info.java @@ -0,0 +1,4 @@ +/** + * Package showing projection features. + */ +package example.springdata.cassandra.projection; diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/SimpleUserRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/SimpleUserRepositoryTests.java deleted file mode 100644 index 8fad8bac..00000000 --- a/cassandra/example/src/test/java/example/springdata/cassandra/SimpleUserRepositoryTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2013-2014 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; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; - -import example.springdata.cassandra.SimpleConfiguration; -import example.springdata.cassandra.SimpleUserRepository; -import example.springdata.cassandra.User; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * Integration test showing the basic usage of {@link SimpleUserRepository}. - * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Christoph Strobl - */ -@Ignore -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = SimpleConfiguration.class) -public class SimpleUserRepositoryTests { - - @Autowired SimpleUserRepository repository; - User user; - - @Before - public void setUp() { - - user = new User(); - user.setId(42L); - user.setUsername("foobar"); - user.setFirstname("firstname"); - user.setLastname("lastname"); - } - - @Test - public void findSavedUserById() { - - user = repository.save(user); - - assertThat(repository.findOne(user.getId()), is(user)); - } -} diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java new file mode 100644 index 00000000..bcb98018 --- /dev/null +++ b/cassandra/example/src/test/java/example/springdata/cassandra/basic/BasicUserRepositoryTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2013-2014 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.basic; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +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.data.util.Version; +import org.springframework.test.context.junit4.SpringRunner; + +import com.datastax.driver.core.Session; + +import example.springdata.cassandra.util.CassandraVersion; +import example.springdata.cassandra.util.RequiresCassandraKeyspace; + +/** + * Integration test showing the basic usage of {@link BasicUserRepository}. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Christoph Strobl + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BasicConfiguration.class) +public class BasicUserRepositoryTests { + + public final static Version CASSANDRA_3_4 = Version.parse("3.4"); + + @ClassRule public final static RequiresCassandraKeyspace CASSANDRA_KEYSPACE = RequiresCassandraKeyspace.onLocalhost(); + + @Autowired BasicUserRepository repository; + @Autowired Session session; + User user; + + @Before + public void setUp() { + + user = new User(); + user.setId(42L); + user.setUsername("foobar"); + user.setFirstname("firstname"); + user.setLastname("lastname"); + } + + /** + * Saving an object using the Cassandra Repository will create a persistent representation of the object in Cassandra. + */ + @Test + public void findSavedUserById() { + + user = repository.save(user); + + assertThat(repository.findOne(user.getId()), is(user)); + } + + /** + * Cassandra can be queries by using query methods annotated with {@link @Query}. + */ + @Test + public void findByAnnotatedQueryMethod() { + + repository.save(user); + + assertThat(repository.findUserByIdIn(1000), is(nullValue())); + assertThat(repository.findUserByIdIn(42), is(equalTo(user))); + } + + /** + * Spring Data Cassandra supports query derivation so annotating query methods with + * {@link org.springframework.data.cassandra.repository.Query} is optional. Querying columns other than the primary + * key requires a secondary index. + */ + @Test + public void findByDerivedQueryMethod() throws InterruptedException { + + session.execute("CREATE INDEX IF NOT EXISTS user_username ON users (uname);"); + /* + Cassandra secondary indexes are created in the background without the possibility to check + whether they are available or not. So we are forced to just wait. *sigh* + */ + Thread.sleep(1000); + + repository.save(user); + + assertThat(repository.findUserByUsername(user.getUsername()), is(user)); + } + + /** + * Spring Data Cassandra supports {@code LIKE} and {@code CONTAINS} query keywords to for SASI indexes. + */ + @Test + public void findByDerivedQueryMethodWithSASI() throws InterruptedException { + + assumeTrue(CassandraVersion.getReleaseVersion(session).isGreaterThanOrEqualTo(CASSANDRA_3_4)); + + session.execute("CREATE CUSTOM INDEX ON users (lname) USING 'org.apache.cassandra.index.sasi.SASIIndex';"); + /* + Cassandra secondary indexes are created in the background without the possibility to check + whether they are available or not. So we are forced to just wait. *sigh* + */ + Thread.sleep(1000); + + repository.save(user); + + assertThat(repository.findUsersByLastnameStartsWith("last"), hasItem(user)); + } +} diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java new file mode 100644 index 00000000..ee007a7a --- /dev/null +++ b/cassandra/example/src/test/java/example/springdata/cassandra/basic/CassandraOperationsIntegrationTests.java @@ -0,0 +1,166 @@ +/* + * 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 example.springdata.cassandra.basic; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +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.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.WriteListener; +import org.springframework.test.context.junit4.SpringRunner; + +import com.datastax.driver.core.Row; +import com.datastax.driver.core.querybuilder.Insert; +import com.datastax.driver.core.querybuilder.QueryBuilder; + +import example.springdata.cassandra.util.RequiresCassandraKeyspace; + +/** + * Integration test showing the basic usage of {@link CassandraTemplate}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BasicConfiguration.CassandraConfig.class) +public class CassandraOperationsIntegrationTests { + + @ClassRule public final static RequiresCassandraKeyspace CASSANDRA_KEYSPACE = RequiresCassandraKeyspace.onLocalhost(); + + @Autowired CassandraOperations template; + + @Before + public void setUp() throws Exception { + template.truncate("users"); + } + + /** + * Cassandra {@link com.datastax.driver.core.Statement}s can be used together with {@link CassandraTemplate} and the + * mapping layer. + */ + @Test + public void insertAndSelect() { + + Insert insert = QueryBuilder.insertInto("users").value("user_id", 42L) // + .value("uname", "heisenberg") // + .value("fname", "Walter") // + .value("lname", "White") // + .ifNotExists(); // + + template.execute(insert); + + User user = template.selectOneById(User.class, 42L); + assertThat(user.getUsername(), is(equalTo("heisenberg"))); + + List users = template.select(QueryBuilder.select().from("users"), User.class); + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(equalTo(user))); + } + + /** + * Objects can be inserted and updated using {@link CassandraTemplate}. What you {@code update} is what you + * {@code select}. + */ + @Test + public void insertAndUpdate() { + + User user = new User(); + user.setId(42L); + user.setUsername("heisenberg"); + user.setFirstname("Walter"); + user.setLastname("White"); + + template.insert(user); + + user.setFirstname(null); + template.update(user); + + User loaded = template.selectOneById(User.class, 42L); + assertThat(loaded.getUsername(), is(equalTo("heisenberg"))); + assertThat(loaded.getFirstname(), is(nullValue())); + } + + /** + * Asynchronous query execution using callbacks. + */ + @Test + public void insertAsynchronously() throws InterruptedException { + + User user = new User(); + user.setId(42L); + user.setUsername("heisenberg"); + user.setFirstname("Walter"); + user.setLastname("White"); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + + template.insertAsynchronously(user, new WriteListener() { + + @Override + public void onWriteComplete(Collection entities) { + countDownLatch.countDown(); + } + + @Override + public void onException(Exception x) {} + }); + + countDownLatch.await(5, TimeUnit.SECONDS); + + User loaded = template.selectOneById(User.class, user.getId()); + assertThat(loaded, is(equalTo(user))); + } + + /** + * {@link CassandraTemplate} allows selection of projections on template-level. All basic data types including + * {@link Row} can be selected. + */ + @Test + @SuppressWarnings("unchecked") + public void selectProjections() { + + User user = new User(); + user.setId(42L); + user.setUsername("heisenberg"); + user.setFirstname("Walter"); + user.setLastname("White"); + + template.insert(user); + + Long id = template.selectOne(QueryBuilder.select("user_id").from("users"), Long.class); + assertThat(id, is(user.getId())); + + Row row = template.selectOne(QueryBuilder.select("user_id").from("users"), Row.class); + assertThat(row.getLong(0), is(user.getId())); + + Map map = template.selectOne(QueryBuilder.select().from("users"), Map.class); + assertThat(map, hasEntry("user_id", user.getId())); + assertThat(map, hasEntry("fname", "Walter")); + } +} diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java b/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java new file mode 100644 index 00000000..7ea4777d --- /dev/null +++ b/cassandra/example/src/test/java/example/springdata/cassandra/convert/ConversionIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * 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 example.springdata.cassandra.convert; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.util.Arrays; + +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.data.cassandra.core.CassandraOperations; +import org.springframework.test.context.junit4.SpringRunner; + +import com.datastax.driver.core.Row; +import com.datastax.driver.core.querybuilder.QueryBuilder; + +import example.springdata.cassandra.util.RequiresCassandraKeyspace; + +/** + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ConverterConfiguration.class) +public class ConversionIntegrationTests { + + @ClassRule public final static RequiresCassandraKeyspace CASSANDRA_KEYSPACE = RequiresCassandraKeyspace.onLocalhost(); + + @Autowired CassandraOperations operations; + + @Before + public void setUp() throws Exception { + operations.truncate("addressbook"); + } + + /** + * Creates and stores a new {@link Addressbook} inside of Cassandra. {@link Contact} classes are converted using the + * custom {@link example.springdata.cassandra.convert.ConverterConfiguration.PersonWriteConverter}. + */ + @Test + public void shouldCreateAddressbook() { + + Addressbook addressbook = new Addressbook(); + addressbook.setId("private"); + + addressbook.setMe(new Contact("Walter", "White")); + addressbook.setFriends(Arrays.asList(new Contact("Jesse", "Pinkman"), new Contact("Saul", "Goodman"))); + + operations.insert(addressbook); + + Row row = operations.selectOne(QueryBuilder.select().from("addressbook"), Row.class); + + assertThat(row, is(notNullValue())); + + assertThat(row.getString("id"), is(equalTo("private"))); + assertThat(row.getString("me"), containsString("\"firstname\":\"Walter\"")); + assertThat(row.getList("friends", String.class), hasSize(2)); + } + + /** + * Creates and loads a new {@link Addressbook} inside of Cassandra. {@link Contact} classes are converted using the + * custom {@link example.springdata.cassandra.convert.ConverterConfiguration.PersonReadConverter}. + */ + @Test + public void shouldReadAddressbook() { + + Addressbook addressbook = new Addressbook(); + addressbook.setId("private"); + + addressbook.setMe(new Contact("Walter", "White")); + addressbook.setFriends(Arrays.asList(new Contact("Jesse", "Pinkman"), new Contact("Saul", "Goodman"))); + + operations.insert(addressbook); + + Addressbook loaded = operations.selectOne(QueryBuilder.select().from("addressbook"), Addressbook.class); + + assertThat(loaded.getMe(), is(equalTo(addressbook.getMe()))); + assertThat(loaded.getFriends(), is(equalTo(addressbook.getFriends()))); + } + + /** + * Creates and stores a new {@link Addressbook} inside of Cassandra. The {@link Addressbook} is read back to a + * {@link CustomAddressbook} class using the + * {@link example.springdata.cassandra.convert.ConverterConfiguration.CustomAddressbookReadConverter}. + */ + @Test + public void shouldReadCustomAddressbook() { + + Addressbook addressbook = new Addressbook(); + addressbook.setId("private"); + + addressbook.setMe(new Contact("Walter", "White")); + + operations.insert(addressbook); + + CustomAddressbook loaded = operations.selectOne(QueryBuilder.select().from("addressbook"), CustomAddressbook.class); + + assertThat(loaded.getTheId(), is(equalTo(addressbook.getId()))); + assertThat(loaded.getMyDetailsAsJson(), containsString("\"firstname\":\"Walter\"")); + } +} diff --git a/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java b/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java new file mode 100644 index 00000000..cf197fc9 --- /dev/null +++ b/cassandra/example/src/test/java/example/springdata/cassandra/projection/CustomerRepositoryIntegrationTest.java @@ -0,0 +1,99 @@ +/* + * 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 example.springdata.cassandra.projection; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collection; + +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.data.projection.TargetAware; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import example.springdata.cassandra.util.RequiresCassandraKeyspace; + +/** + * Integration tests for {@link CustomerRepository} to show projection capabilities. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ProjectionConfiguration.class) +public class CustomerRepositoryIntegrationTest { + + @ClassRule public final static RequiresCassandraKeyspace CASSANDRA_KEYSPACE = RequiresCassandraKeyspace.onLocalhost(); + + @Autowired CustomerRepository customers; + + Customer dave, carter; + + @Before + public void setUp() { + + customers.deleteAll(); + + this.dave = customers.save(new Customer("d", "Dave", "Matthews")); + this.carter = customers.save(new Customer("c", "Carter", "Beauford")); + } + + @Test + public void projectsEntityIntoInterface() { + + Collection result = customers.findAllProjectedBy(); + + assertThat(result, hasSize(2)); + assertThat(result.iterator().next().getFirstname(), is("Carter")); + } + + @Test + public void projectsDynamically() { + + Collection result = customers.findById("d", CustomerProjection.class); + + assertThat(result, hasSize(1)); + assertThat(result.iterator().next().getFirstname(), is("Dave")); + } + + @Test + public void projectsIndividualDynamically() { + + CustomerSummary result = customers.findProjectedById(dave.getId(), CustomerSummary.class); + + assertThat(result, is(notNullValue())); + assertThat(result.getFullName(), is("Dave Matthews")); + + // Proxy backed by original instance as the projection uses dynamic elements + assertThat(((TargetAware) result).getTarget(), is(instanceOf(Customer.class))); + } + + @Test + public void projectIndividualInstance() { + + CustomerProjection result = customers.findProjectedById(dave.getId()); + + assertThat(result, is(notNullValue())); + assertThat(result.getFirstname(), is("Dave")); + assertThat(((TargetAware) result).getTarget(), is(instanceOf(Customer.class))); + } + +} diff --git a/cassandra/java8/README.md b/cassandra/java8/README.md new file mode 100644 index 00000000..26043af9 --- /dev/null +++ b/cassandra/java8/README.md @@ -0,0 +1,36 @@ +# Spring Data Cassandra - Java 8 examples + +This project contains samples of Java 8 specific features of Spring Data (Cassandra). + +## Support for JDK 8's `Stream` for repository methods + +Repository methods can use a Java 8 `Stream` as a return type which will cause the reading of the results and the to-object-conversion of rows to happen while iterating over the stream. + +```java +public interface PersonRepository extends CrudRepository { + + @Override + List findAll(); + + //Custom Query method returning a Java 8 Stream + @Query("SELECT * FROM person") + Stream findAllByCustomQueryWithStream(); +} +``` + +The test cases in `PersonRepositoryIntegrationTest` oppose a plain `List` based query method with one that uses a `Stream` and shows how the former pulls all data into memory first and the iteration is done over the pre-populated list. The execution of the `Stream`-based method in contrast shows that the individual elements are read and converted while iterating the stream. + +## Preparation + +### Install Cassandra +Before we can start we have to install Cassandra, e.g. via brew on Max OS. + +More details can be found here: https://wiki.apache.org/cassandra/GettingStarted + +### Start Cassandra +``` +/usr/local/bin/cassandra -f +``` + +That should be enough to get you started. +Now you can simply type ```mvn clean install``` to run the example. diff --git a/cassandra/java8/pom.xml b/cassandra/java8/pom.xml new file mode 100644 index 00000000..6bbc36bf --- /dev/null +++ b/cassandra/java8/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-cassandra-examples + 1.0.0.BUILD-SNAPSHOT + ../pom.xml + + + spring-data-cassandra-java8 + Spring Data Cassandra - Java 8 specific features + + + + + ${project.groupId} + spring-data-cassandra-example-utils + ${project.version} + test + + + + + diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/CassandraConfiguration.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/CassandraConfiguration.java new file mode 100644 index 00000000..c008c8a8 --- /dev/null +++ b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/CassandraConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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 example.springdata.cassandra.java8; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; + +/** + * @author Mark Paluch + */ +@Configuration +@EnableAutoConfiguration +class CassandraConfiguration { + + @Configuration + @EnableCassandraRepositories + static class CassandraConfig extends AbstractCassandraConfiguration { + + @Override + public String getKeyspaceName() { + return "example"; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE; + } + } +} diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Order.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Order.java new file mode 100644 index 00000000..eda71bd9 --- /dev/null +++ b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Order.java @@ -0,0 +1,40 @@ +/* + * 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 example.springdata.cassandra.java8; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.ZoneId; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +/** + * @author Mark Paluch + */ +@Table("pizza_orders") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Order { + + @Id String id; + LocalDate orderDate; + ZoneId zoneId; +} diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/OrderRepository.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/OrderRepository.java new file mode 100644 index 00000000..365db253 --- /dev/null +++ b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/OrderRepository.java @@ -0,0 +1,47 @@ +/* + * 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 example.springdata.cassandra.java8; + +import java.time.LocalDate; +import java.time.ZoneId; + +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.repository.Repository; + +/** + * Repository to manage {@link Order} instances. + * + * @author Mark Paluch + */ +public interface OrderRepository extends Repository { + + /** + * Method parameters are converted according the registered Converters into Cassandra types. + */ + @Query("SELECT * from pizza_orders WHERE orderdate = ?0 and zoneid = ?1 ALLOW FILTERING") + Order findOrderByOrderDateAndZoneId(LocalDate orderDate, ZoneId zoneId); + + /** + * Parameter conversion can be overridden by using the {@link CassandraType} annotation. + */ + @Query("SELECT * from pizza_orders WHERE orderdate = ?0 and zoneid = ?1 ALLOW FILTERING") + Order findOrderByDate(com.datastax.driver.core.LocalDate orderDate, String zoneId); + + void deleteAll(); + + Order save(Order order); +} diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Person.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Person.java new file mode 100644 index 00000000..a313f24d --- /dev/null +++ b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/Person.java @@ -0,0 +1,37 @@ +/* + * 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 example.springdata.cassandra.java8; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + */ +@Table +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Person { + + @Id String id; + String firstname, lastname; +} diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/PersonRepository.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/PersonRepository.java new file mode 100644 index 00000000..1bf32c95 --- /dev/null +++ b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/PersonRepository.java @@ -0,0 +1,65 @@ +/* + * 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 example.springdata.cassandra.java8; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.Repository; + +/** + * Repository to manage {@link Person} instances. + * + * @author Mark Paluch + */ +public interface PersonRepository extends Repository { + + /** + * Special customization of {@link CrudRepository#findOne(java.io.Serializable)} to return a JDK 8 {@link Optional}. + * + * @param id + * @return + */ + Optional findOne(String id); + + @Query("select * from person") + Stream streamAllPeople(); + + /** + * Sample method to derive a query from using JDK 8's {@link Optional} as return type. + * + * @param id + * @return + */ + @Query("select * from person where id = ?0") + Optional findById(String id); + + /** + * Sample default method to show JDK 8 feature support. + * + * @param person + * @return + */ + default Optional findByPerson(Person person) { + return findById(person == null ? null : person.id); + } + + void deleteAll(); + + Person save(Person person); +} diff --git a/cassandra/java8/src/main/java/example/springdata/cassandra/java8/package-info.java b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/package-info.java new file mode 100644 index 00000000..476e1287 --- /dev/null +++ b/cassandra/java8/src/main/java/example/springdata/cassandra/java8/package-info.java @@ -0,0 +1,4 @@ +/** + * Package showing Java 8 features. + */ +package example.springdata.cassandra.java8; diff --git a/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Java8IntegrationTests.java b/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Java8IntegrationTests.java new file mode 100644 index 00000000..9d4d693e --- /dev/null +++ b/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Java8IntegrationTests.java @@ -0,0 +1,87 @@ +/* + * 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 example.springdata.cassandra.java8; + +import example.springdata.cassandra.util.RequiresCassandraKeyspace; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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.data.util.Version; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration test to show the usage of Java 8 features with Spring Data Cassandra. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CassandraConfiguration.class) +public class Java8IntegrationTests { + + @ClassRule + public final static RequiresCassandraKeyspace CASSANDRA_KEYSPACE = RequiresCassandraKeyspace.onLocalhost().atLeast(Version.parse("3.0")); + + @Autowired PersonRepository repository; + + @Before + public void setUp() throws Exception { + repository.deleteAll(); + } + + @Test + public void providesFindOneWithOptional() { + + Person homer = repository.save(new Person("1", "Homer", "Simpson")); + + assertThat(repository.findOne(homer.id).isPresent(), is(true)); + assertThat(repository.findOne(homer.id + 1), is(Optional. empty())); + } + + @Test + public void invokesDefaultMethod() { + + Person homer = repository.save(new Person("1", "Homer", "Simpson")); + Optional result = repository.findByPerson(homer); + + assertThat(result.isPresent(), is(true)); + assertThat(result.get(), is(homer)); + } + + /** + * Streaming data from the store by using a repository method that returns a {@link Stream}. Note, that since the + * resulting {@link Stream} contains state it needs to be closed explicitly after use! + */ + @Test + public void useJava8StreamsWithCustomQuery() { + + Person homer = repository.save(new Person("1", "Homer", "Simpson")); + Person bart = repository.save(new Person("2", "Bart", "Simpson")); + + try (Stream stream = repository.streamAllPeople()) { + assertThat(stream.collect(Collectors.toList()), hasItems(homer, bart)); + } + } +} diff --git a/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Jsr310IntegrationTests.java b/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Jsr310IntegrationTests.java new file mode 100644 index 00000000..76221d08 --- /dev/null +++ b/cassandra/java8/src/test/java/example/springdata/cassandra/java8/Jsr310IntegrationTests.java @@ -0,0 +1,76 @@ +/* + * 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 example.springdata.cassandra.java8; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.time.LocalDate; +import java.time.ZoneId; + +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.data.util.Version; +import org.springframework.test.context.junit4.SpringRunner; + +import example.springdata.cassandra.util.RequiresCassandraKeyspace; + +/** + * Integration test to show the usage of JSR-310 date/time types with Spring Data Cassandra. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = CassandraConfiguration.class) +public class Jsr310IntegrationTests { + + @ClassRule public final static RequiresCassandraKeyspace CASSANDRA_KEYSPACE = RequiresCassandraKeyspace.onLocalhost() + .atLeast(Version.parse("3.0")); + + @Autowired OrderRepository repository; + + @Before + public void setUp() throws Exception { + repository.deleteAll(); + } + + @Test + public void findOneByJsr310Types() { + + Order order = new Order("42", LocalDate.now(), ZoneId.systemDefault()); + + repository.save(order); + + assertThat(repository.findOrderByOrderDateAndZoneId(order.getOrderDate(), order.getZoneId()), is(equalTo(order))); + } + + @Test + public void findOneByConvertedTypes() { + + Order order = new Order("42", LocalDate.of(2010, 1, 2), ZoneId.systemDefault()); + + repository.save(order); + + com.datastax.driver.core.LocalDate date = com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 1, 2); + String zoneId = order.getZoneId().getId(); + + assertThat(repository.findOrderByDate(date, zoneId), is(equalTo(order))); + } +} diff --git a/cassandra/pom.xml b/cassandra/pom.xml index a861abc0..f30cff29 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-data-cassandra-examples @@ -17,7 +17,9 @@ 2014 + util example + java8 @@ -27,9 +29,14 @@ spring-boot-starter-aop - - org.springframework.data - spring-data-cassandra + + org.springframework.boot + spring-boot-starter-data-cassandra + + + + com.datastax.cassandra + cassandra-driver-core diff --git a/cassandra/util/pom.xml b/cassandra/util/pom.xml new file mode 100644 index 00000000..38bd8544 --- /dev/null +++ b/cassandra/util/pom.xml @@ -0,0 +1,22 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-cassandra-examples + 1.0.0.BUILD-SNAPSHOT + ../pom.xml + + + spring-data-cassandra-example-utils + Spring Data Cassandra - Example Utilities + + + + junit + junit + + + + diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java new file mode 100644 index 00000000..c39503cd --- /dev/null +++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/CassandraVersion.java @@ -0,0 +1,50 @@ +/* + * 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 example.springdata.cassandra.util; + +import org.springframework.data.util.Version; +import org.springframework.util.Assert; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; + +import lombok.experimental.UtilityClass; + +/** + * Utility to retrieve the Cassandra release version. + * + * @author Mark Paluch + */ +@UtilityClass +public class CassandraVersion { + + /** + * Retrieve the Cassandra release version. + * + * @param session must not be {@literal null}. + * @return the release {@link Version}. + */ + public static Version getReleaseVersion(Session session) { + + Assert.notNull(session, "Session must not be null"); + + ResultSet resultSet = session.execute("SELECT release_version FROM system.local;"); + Row row = resultSet.one(); + + return Version.parse(row.getString(0)); + } +} diff --git a/cassandra/util/src/main/java/example/springdata/cassandra/util/RequiresCassandraKeyspace.java b/cassandra/util/src/main/java/example/springdata/cassandra/util/RequiresCassandraKeyspace.java new file mode 100644 index 00000000..91046b17 --- /dev/null +++ b/cassandra/util/src/main/java/example/springdata/cassandra/util/RequiresCassandraKeyspace.java @@ -0,0 +1,124 @@ +/* + * 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 example.springdata.cassandra.util; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.ExternalResource; +import org.springframework.data.util.Version; +import org.springframework.util.Assert; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.NettyOptions; +import com.datastax.driver.core.Session; + +import io.netty.channel.EventLoopGroup; + +/** + * Implementation of an {@link ExternalResource} to verify Apache Cassandra is running and listening on the given + * contact point. This rule also creates a keyspace if it not exists. + * + * @author Mark Paluch + */ +public class RequiresCassandraKeyspace extends ExternalResource { + + private final int timeout = 30; + private final String host; + private final int port; + private final String keyspaceName; + private Version requiresVersion; + + private RequiresCassandraKeyspace(String host, int port, String keyspaceName) { + + this.host = host; + this.port = port; + this.keyspaceName = keyspaceName; + } + + /** + * Require a running Cassandra instance on {@code localhost:9042}. + * + * @return the {@link RequiresCassandraKeyspace} rule + */ + public static RequiresCassandraKeyspace onLocalhost() { + return new RequiresCassandraKeyspace("localhost", 9042, "example"); + } + + /** + * Setup a version requirement. + * + * @param version must not be {@literal null}. + * @return the {@link RequiresCassandraKeyspace} rule + */ + public RequiresCassandraKeyspace atLeast(Version version) { + + Assert.notNull(version, "Required version must not be null!"); + + this.requiresVersion = version; + return this; + } + + /* + * (non-Javadoc) + * @see org.junit.rules.ExternalResource#before() + */ + @Override + protected void before() throws Throwable { + + try (Socket socket = new Socket()) { + socket.setTcpNoDelay(true); + socket.setSoLinger(true, 0); + socket.connect(new InetSocketAddress(host, port), timeout); + + } catch (Exception e) { + throw new AssumptionViolatedException(String.format("Seems as Cassandra is not running at %s:%s.", host, port), + e); + } + + Cluster cluster = Cluster.builder().addContactPoint(host).withPort(port).withNettyOptions(new NettyOptions() { + @Override + public void onClusterClose(EventLoopGroup eventLoopGroup) { + eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS).syncUninterruptibly(); + } + }).build(); + + Session session = cluster.newSession(); + + try { + + if (requiresVersion != null) { + + Version cassandraReleaseVersion = CassandraVersion.getReleaseVersion(session); + + if (cassandraReleaseVersion.isLessThan(requiresVersion)) { + throw new AssumptionViolatedException( + String.format("Cassandra at %s:%s runs in Version %s but we require at least %s", host, port, + cassandraReleaseVersion, requiresVersion)); + } + } + + session.execute(String.format("CREATE KEYSPACE IF NOT EXISTS %s \n" + + "WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", keyspaceName)); + } finally { + session.close(); + cluster.close(); + } + } + +} diff --git a/pom.xml b/pom.xml index b8281e98..0b03dba3 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ Ingalls-M1 0.21.0.RELEASE 2.9.0 + 3.0.3