From d36fd2eb8b3482fe6878f511a25a541ee3ec672d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 Sep 2018 15:44:39 +0200 Subject: [PATCH] Add Kotlin example for Spring Data Cassandra. --- cassandra/kotlin/README.md | 59 ++++++++ cassandra/kotlin/pom.xml | 79 ++++++++++ .../ApplicationConfiguration.kt | 29 ++++ .../Person.kt | 28 ++++ .../PersonRepository.kt | 41 ++++++ .../src/main/resources/application.properties | 2 + .../cassandra/kotlin/RepositoryTests.kt | 85 +++++++++++ .../cassandra/kotlin/TemplateTests.kt | 139 ++++++++++++++++++ cassandra/pom.xml | 1 + 9 files changed, 463 insertions(+) create mode 100644 cassandra/kotlin/README.md create mode 100644 cassandra/kotlin/pom.xml create mode 100644 cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt create mode 100644 cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt create mode 100644 cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt create mode 100644 cassandra/kotlin/src/main/resources/application.properties create mode 100644 cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt create mode 100644 cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt diff --git a/cassandra/kotlin/README.md b/cassandra/kotlin/README.md new file mode 100644 index 00000000..fd3e71fe --- /dev/null +++ b/cassandra/kotlin/README.md @@ -0,0 +1,59 @@ +# Spring Data Cassandra - Kotlin examples + +This project contains samples of Kotlin-specific features of Spring Data (Cassandra). + +## Value defaulting on entity construction + +Kotlin allows defaulting for constructor- and method arguments. +Defaulting allows usage of substitute values if a field in the document is absent or simply `null`. +Spring Data inspects objects whether they are Kotlin types and uses the appropriate constructor. + +```kotlin +@Table +data class Person(@PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED) val firstname: String? = "", val lastname: String = "White") + +operations.cqlOperations.execute(QueryBuilder.insertInto("person").value("firstname", "Walter")) + +val person = operations.query() + .matching(query(where("firstname").isEqualTo("Walter"))) + .firstValue()!! + +assertThat(person.lastname).isEqualTo("White") +``` + +## Kotlin Extensions + +Spring Data exposes methods accepting a target type to either query for or to project results values on. +Kotlin represents classes with its own type, `KClass` which can be an obstacle when attempting to obtain a Java `Class` type. + +Spring Data ships with extensions that add overloads for methods accepting a type parameter by either leveraging generics or accepting `KClass` directly. + +```kotlin +operations.getTableName() + +operations.getTableName(Person::class) +``` + +## Nullability + +Declaring repository interfaces using Kotlin allows expressing nullability constraints on arguments and return types. Spring Data evaluates nullability of arguments and return types and reacts to these. Passing `null` to a non-nullable argument raises an `IllegalArgumentException`, as you're already used to from Kotlin. Spring Data helps you also to prevent `null` in query results. If you wish to return a nullable result, use Kotlin's nullability marker `?`. To prevent `null` results, declare the return type of a query method as non-nullable. In the case a query yields no result, a non-nullable query method throws `EmptyResultDataAccessException`. + +```kotlin +interface PersonRepository : CrudRepository { + + /** + * Query method declaring a nullable return type that allows to return null values. + */ + fun findOneOrNoneByFirstname(firstname: String): Person? + + /** + * Query method declaring a nullable argument. + */ + fun findNullableByFirstname(firstname: String?): Person? + + /** + * Query method requiring a result. Throws [org.springframework.dao.EmptyResultDataAccessException] if no result is found. + */ + fun findOneByFirstname(firstname: String): Person +} +``` diff --git a/cassandra/kotlin/pom.xml b/cassandra/kotlin/pom.xml new file mode 100644 index 00000000..88a3f157 --- /dev/null +++ b/cassandra/kotlin/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-cassandra-examples + 2.0.0.BUILD-SNAPSHOT + + + spring-data-cassandra-kotlin + Spring Data Cassandra - Kotlin features + + + Lovelace-BUILD-SNAPSHOT + + + + + + + + spring-data-next + + Lovelace-BUILD-SNAPSHOT + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-reflect + + + + ${project.groupId} + spring-data-cassandra-example-utils + ${project.version} + test + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.springframework.boot + spring-boot-maven-plugin + + + kotlin-maven-plugin + org.jetbrains.kotlin + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + diff --git a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt new file mode 100644 index 00000000..077c7626 --- /dev/null +++ b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/ApplicationConfiguration.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.kotlin + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +/** + * @author Mark Paluch + */ +@SpringBootApplication +class ApplicationConfiguration + +fun main(args: Array) { + runApplication(*args) +} diff --git a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt new file mode 100644 index 00000000..84495bd0 --- /dev/null +++ b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/Person.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.kotlin + +import org.springframework.data.cassandra.core.cql.PrimaryKeyType +import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn +import org.springframework.data.cassandra.core.mapping.Table + +/** + * An entity to represent a Person. + * + * @author Mark Paluch + */ +@Table +data class Person(@PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED) val firstname: String? = "", val lastname: String = "White") diff --git a/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt new file mode 100644 index 00000000..063ad681 --- /dev/null +++ b/cassandra/kotlin/src/main/kotlin/example.springdata.cassandra.kotlin/PersonRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.kotlin + +import org.springframework.data.repository.CrudRepository + +/** + * Repository interface to manage [Person] instances. + * + * @author Mark Paluch + */ +interface PersonRepository : CrudRepository { + + /** + * Query method declaring a nullable return type that allows to return null values. + */ + fun findOneOrNoneByFirstname(firstname: String): Person? + + /** + * Query method declaring a nullable argument. + */ + fun findNullableByFirstname(firstname: String?): Person? + + /** + * Query method requiring a result. Throws [org.springframework.dao.EmptyResultDataAccessException] if no result is found. + */ + fun findOneByFirstname(firstname: String): Person +} diff --git a/cassandra/kotlin/src/main/resources/application.properties b/cassandra/kotlin/src/main/resources/application.properties new file mode 100644 index 00000000..b94b9f18 --- /dev/null +++ b/cassandra/kotlin/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.data.cassandra.keyspace-name=example +spring.data.cassandra.schema-action=recreate diff --git a/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt new file mode 100644 index 00000000..37b8ccd6 --- /dev/null +++ b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/RepositoryTests.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.kotlin + +import example.springdata.cassandra.util.CassandraKeyspace +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +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.EmptyResultDataAccessException +import org.springframework.data.util.Version +import org.springframework.test.context.junit4.SpringRunner + +/** + * Tests showing Kotlin usage of Spring Data Repositories. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner::class) +@SpringBootTest +class RepositoryTests { + + companion object { + + @JvmField + @ClassRule + val CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost() + .atLeast(Version.parse("3.0")) + } + + @Autowired + lateinit var repository: PersonRepository + + @Before + fun before() { + repository.deleteAll() + } + + @Test + fun `should find one person`() { + + repository.save(Person("Walter", "White")) + + val walter = repository.findOneByFirstname("Walter") + + assertThat(walter).isNotNull() + assertThat(walter.firstname).isEqualTo("Walter") + assertThat(walter.lastname).isEqualTo("White") + } + + @Test + fun `should return null if no person found`() { + + repository.save(Person("Walter", "White")) + + val walter = repository.findOneOrNoneByFirstname("Hank") + + assertThat(walter).isNull() + } + + @Test + fun `should throw EmptyResultDataAccessException if no person found`() { + + repository.save(Person("Walter", "White")) + + assertThatThrownBy { repository.findOneByFirstname("Hank") }.isInstanceOf(EmptyResultDataAccessException::class.java) + } +} diff --git a/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt new file mode 100644 index 00000000..2909ce2b --- /dev/null +++ b/cassandra/kotlin/src/test/kotlin/example/springdata/cassandra/kotlin/TemplateTests.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.cassandra.kotlin + +import com.datastax.driver.core.Row +import com.datastax.driver.core.querybuilder.QueryBuilder +import example.springdata.cassandra.util.CassandraKeyspace +import org.assertj.core.api.Assertions.assertThat +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.* +import org.springframework.data.cassandra.core.cql.CqlIdentifier +import org.springframework.data.cassandra.core.query.Query.query +import org.springframework.data.cassandra.core.query.isEqualTo +import org.springframework.data.cassandra.core.query.where +import org.springframework.data.util.Version +import org.springframework.test.context.junit4.SpringRunner + +/** + * Tests showing Kotlin usage of [MongoTemplate] and its Kotlin extensions. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner::class) +@SpringBootTest +class TemplateTests { + + companion object { + + @JvmField + @ClassRule + val CASSANDRA_KEYSPACE = CassandraKeyspace.onLocalhost() + .atLeast(Version.parse("3.0")) + } + + @Autowired + lateinit var operations: CassandraOperations + + @Before + fun before() { + operations.truncate() + } + + @Test + fun `should create collection leveraging reified type parameters`() { + assertThat(operations.getTableName()).isEqualTo(CqlIdentifier.of("person")) + } + + @Test + fun `should insert and find person in a fluent API style`() { + + operations.insert().inTable("person").one(Person("Walter", "White")) + + val people = operations.query() + .matching(query(where("firstname").isEqualTo("Walter"))) + .all() + + assertThat(people).hasSize(1).extracting("firstname").containsOnly("Walter") + } + + @Test + fun `should insert and project query results`() { + + operations.insert().inTable("person").one(Person("Walter", "White")) + + val firstnameOnly = operations.query() + .asType() + .matching(query(where("firstname").isEqualTo("Walter"))) + .oneValue() + + assertThat(firstnameOnly?.getFirstname()).isEqualTo("Walter") + } + + @Test + fun `should insert and count objects in a fluent API style`() { + + operations.insert().inTable("person").one(Person("Walter", "White")) + + val count = operations.query() + .matching(query(where("firstname").isEqualTo("Walter"))) + .count() + + assertThat(count).isEqualTo(1) + } + + @Test + fun `should insert and find person`() { + + val person = Person("Walter", "White") + + operations.insert(person) + + val people = operations.select(query(where("firstname").isEqualTo("Walter"))) + + assertThat(people).hasSize(1).extracting("firstname").containsOnly("Walter") + } + + @Test + fun `should apply defaulting for absent properties`() { + + operations.cqlOperations.execute(QueryBuilder.insertInto("person").value("firstname", "Walter")) + + val person = operations.query() + .matching(query(where("firstname").isEqualTo("Walter"))) + .firstValue()!! + + assertThat(person.firstname).isEqualTo("Walter") + assertThat(person.lastname).isEqualTo("White") + + + val resultSet = operations.cqlOperations.queryForResultSet("SELECT * FROM person WHERE firstname = 'Walter'") + val walter = resultSet.one()!! + + assertThat(walter).isNotNull + assertThat(walter.getString("firstname")).isEqualTo("Walter") + assertThat(walter.getString("lastname")).isNull() + } + + interface FirstnameOnly { + fun getFirstname(): String + } +} diff --git a/cassandra/pom.xml b/cassandra/pom.xml index f45e35f6..bafa4bc1 100644 --- a/cassandra/pom.xml +++ b/cassandra/pom.xml @@ -20,6 +20,7 @@ util example java8 + kotlin reactive