From 22242644997ca56465d10935ea9fa0cc26ea9284 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 Sep 2018 15:02:32 +0200 Subject: [PATCH] Add Kotlin example for Spring Data MongoDB. --- mongodb/kotlin/README.md | 56 ++++++++ mongodb/kotlin/pom.xml | 55 ++++++++ .../people/ApplicationConfiguration.kt | 29 ++++ .../springdata/mongodb/people/Person.kt | 28 ++++ .../mongodb/people/PersonRepository.kt | 41 ++++++ .../mongodb/people/RepositoryTests.kt | 85 ++++++++++++ .../mongodb/people/TemplateTests.kt | 126 ++++++++++++++++++ mongodb/pom.xml | 3 +- 8 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 mongodb/kotlin/README.md create mode 100644 mongodb/kotlin/pom.xml create mode 100644 mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/ApplicationConfiguration.kt create mode 100644 mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/Person.kt create mode 100644 mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/PersonRepository.kt create mode 100644 mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/RepositoryTests.kt create mode 100644 mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/TemplateTests.kt diff --git a/mongodb/kotlin/README.md b/mongodb/kotlin/README.md new file mode 100644 index 00000000..08ff9464 --- /dev/null +++ b/mongodb/kotlin/README.md @@ -0,0 +1,56 @@ +# Spring Data MongoDB - Kotlin examples + +This project contains samples of Kotlin-specific features of Spring Data (MongoDB). + +## 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 +data class Person (@Id val id: String?, val firstname: String? = "Walter", val lastname: String) + +operations.insert().inCollection("person").one(Document("lastname", "White")) + +val walter = operations.findOne(query(where("lastname").isEqualTo("White")), "person") + +assertThat(walter.firstname).isEqualTo("Walter") +``` + +## 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.getCollectionName() + +operations.getCollectionName(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/mongodb/kotlin/pom.xml b/mongodb/kotlin/pom.xml new file mode 100644 index 00000000..27a367b3 --- /dev/null +++ b/mongodb/kotlin/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + + org.springframework.data.examples + spring-data-mongodb-examples + 2.0.0.BUILD-SNAPSHOT + + + spring-data-mongodb-kotlin + Spring Data MongoDB - Kotlin features + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-reflect + + + + + ${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/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/ApplicationConfiguration.kt b/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/ApplicationConfiguration.kt new file mode 100644 index 00000000..34836623 --- /dev/null +++ b/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/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.mongodb.people + +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/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/Person.kt b/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/Person.kt new file mode 100644 index 00000000..f5cede6a --- /dev/null +++ b/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/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.mongodb.people + +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.PersistenceConstructor + +/** + * An entity to represent a Person. + * + * @author Mark Paluch + */ +data class Person @PersistenceConstructor constructor(@Id val id: String?, val firstname: String? = "Walter", val lastname: String = "") { + constructor(firstname: String?, lastname: String) : this(null, firstname, lastname); +} diff --git a/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/PersonRepository.kt b/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/PersonRepository.kt new file mode 100644 index 00000000..7c2eb6ca --- /dev/null +++ b/mongodb/kotlin/src/main/kotlin/example/springdata/mongodb/people/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.mongodb.people + +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/mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/RepositoryTests.kt b/mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/RepositoryTests.kt new file mode 100644 index 00000000..3eb0908d --- /dev/null +++ b/mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/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.mongodb.people + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Before +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.test.context.junit4.SpringRunner + +/** + * Tests showing Kotlin usage of Spring Data Repositories. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner::class) +@SpringBootTest +class RepositoryTests { + + @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) + } + + @Test + fun `should accept nullable parameters`() { + + repository.save(Person(null, "Heisenberg")) + + val heisenberg = repository.findNullableByFirstname(null) + + assertThat(heisenberg).isNotNull() + assertThat(heisenberg!!.lastname).isEqualTo("Heisenberg") + } +} diff --git a/mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/TemplateTests.kt b/mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/TemplateTests.kt new file mode 100644 index 00000000..90f5cc3d --- /dev/null +++ b/mongodb/kotlin/src/test/kotlin/example/springdata/mongodb/people/TemplateTests.kt @@ -0,0 +1,126 @@ +/* + * 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.mongodb.people + +import org.assertj.core.api.Assertions.assertThat +import org.bson.Document +import org.junit.Before +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.mongodb.core.* +import org.springframework.data.mongodb.core.query.Criteria.where +import org.springframework.data.mongodb.core.query.Query.query +import org.springframework.data.mongodb.core.query.isEqualTo +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 { + + @Autowired + lateinit var operations: MongoOperations + + @Before + fun before() { + operations.dropCollection() + } + + @Test + fun `should create collection leveraging reified type parameters`() { + + operations.createCollection() + + assertThat(operations.collectionNames).contains("person") + } + + @Test + fun `should insert and find person in a fluent API style`() { + + operations.insert().inCollection("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().inCollection("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().inCollection("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.find(query(where("firstname").isEqualTo("Walter"))) + + assertThat(people).hasSize(1).extracting("firstname").containsOnly("Walter") + } + + @Test + fun `should apply defaulting for absent properties`() { + + operations.insert().inCollection("person").one(Document("lastname", "White")) + + val person = operations.query() + .matching(query(where("lastname").isEqualTo("White"))) + .firstValue()!! + + assertThat(person.firstname).isEqualTo("Walter") + assertThat(person.lastname).isEqualTo("White") + + + val walter = operations.findOne(query(where("lastname").isEqualTo("White")), "person") + + assertThat(walter).isNotNull.containsEntry("lastname", "White").containsOnlyKeys("_id", "lastname") + } + + interface FirstnameOnly { + fun getFirstname(): String + } +} diff --git a/mongodb/pom.xml b/mongodb/pom.xml index 140c1b82..a290f922 100644 --- a/mongodb/pom.xml +++ b/mongodb/pom.xml @@ -24,6 +24,7 @@ geo-json gridfs java8 + kotlin query-by-example reactive security @@ -32,7 +33,7 @@ schema-validation util - + 2.1.1