diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java index 975a6a1e17..5c3119b865 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java @@ -404,7 +404,7 @@ public Update filterArray(CriteriaDefinition criteria) { /** * Filter elements in an array that match the given criteria for update. {@code expression} is used directly with the - * driver without further further type or field mapping. + * driver without further type or field mapping. * * @param identifier the positional operator identifier filter criteria name. * @param expression the positional operator filter expression. diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt new file mode 100644 index 0000000000..d35c1d74cd --- /dev/null +++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2018-2024 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.query + +import org.springframework.data.mapping.toDotPath +import org.springframework.data.mongodb.core.query.Update.Position +import kotlin.reflect.KProperty + +/** + * Static factory method to create an Update using the provided key + * + * @author Pawel Matysek + * @see Update.update + */ +fun update(key: KProperty, value: T?) = + Update.update(key.toDotPath(), value) + +/** + * Update using the {@literal $set} update modifier + * + * @author Pawel Matysek + * @see Update.set + */ +fun Update.set(key: KProperty, value: T?) = + set(key.toDotPath(), value) + +/** + * Update using the {@literal $setOnInsert} update modifier + * + * @author Pawel Matysek + * @see Update.setOnInsert + */ +fun Update.setOnInsert(key: KProperty, value: T?) = + setOnInsert(key.toDotPath(), value) + +/** + * Update using the {@literal $unset} update modifier + * + * @author Pawel Matysek + * @see Update.unset + */ +fun Update.unset(key: KProperty) = + unset(key.toDotPath()) + +/** + * Update using the {@literal $inc} update modifier + * + * @author Pawel Matysek + * @see Update.inc + */ +fun Update.inc(key: KProperty, inc: Number) = + inc(key.toDotPath(), inc) + +fun Update.inc(key: KProperty) = + inc(key.toDotPath()) + +/** + * Update using the {@literal $push} update modifier + * + * @author Pawel Matysek + * @see Update.push + */ +fun Update.push(key: KProperty>, value: T?) = + push(key.toDotPath(), value) + +/** + * Update using {@code $push} modifier.
+ * Allows creation of {@code $push} command for single or multiple (using {@code $each}) values as well as using + * + * {@code $position}. + * @author Pawel Matysek + * @see Update.push + */ +fun Update.push(key: KProperty) = + push(key.toDotPath()) + +/** + * Update using {@code $addToSet} modifier.
+ * Allows creation of {@code $push} command for single or multiple (using {@code $each}) values * {@code $position}. + * + * @author Pawel Matysek + * @see Update.addToSet + */ +fun Update.addToSet(key: KProperty) = + addToSet(key.toDotPath()) + +/** + * Update using the {@literal $addToSet} update modifier + * + * @author Pawel Matysek + * @see Update.addToSet + */ +fun Update.addToSet(key: KProperty>, value: T?) = + addToSet(key.toDotPath(), value) + +/** + * Update using the {@literal $pop} update modifier + * + * @author Pawel Matysek + * @see Update.pop + */ +fun Update.pop(key: KProperty, pos: Position) = + pop(key.toDotPath(), pos) + +/** + * Update using the {@literal $pull} update modifier + * + * @author Pawel Matysek + * @see Update.pull + */ +fun Update.pull(key: KProperty, value: Any) = + pull(key.toDotPath(), value) + +/** + * Update using the {@literal $pullAll} update modifier + * + * @author Pawel Matysek + * @see Update.pullAll + */ +fun Update.pullAll(key: KProperty>, values: Array) = + pullAll(key.toDotPath(), values) + +/** + * Update given key to current date using {@literal $currentDate} modifier. + * + * @author Pawel Matysek + * @see Update.currentDate + */ +fun Update.currentDate(key: KProperty) = + currentDate(key.toDotPath()) + +/** + * Update given key to current date using {@literal $currentDate : { $type : "timestamp" }} modifier. + * + * @author Pawel Matysek + * @see Update.currentTimestamp + */ +fun Update.currentTimestamp(key: KProperty) = + currentTimestamp(key.toDotPath()) + +/** + * Multiply the value of given key by the given number. + * + * @author Pawel Matysek + * @see Update.multiply + */ +fun Update.multiply(key: KProperty, multiplier: Number) = + multiply(key.toDotPath(), multiplier) + +/** + * Update given key to the {@code value} if the {@code value} is greater than the current value of the field. + * + * @author Pawel Matysek + * @see Update.max + */ +fun Update.max(key: KProperty, value: T) = + max(key.toDotPath(), value) + +/** + * Update given key to the {@code value} if the {@code value} is less than the current value of the field. + * + * @author Pawel Matysek + * @see Update.min + */ +fun Update.min(key: KProperty, value: T) = + min(key.toDotPath(), value) + +/** + * The operator supports bitwise {@code and}, bitwise {@code or}, and bitwise {@code xor} operations. + * + * @author Pawel Matysek + * @see Update.bitwise + */ +fun Update.bitwise(key: KProperty) = + bitwise(key.toDotPath()) + +/** + * Filter elements in an array that match the given criteria for update. {@code expression} is used directly with the + * driver without further type or field mapping. + * + * @author Pawel Matysek + * @see Update.filterArray + */ +fun Update.filterArray(identifier: KProperty, expression: Any) = + filterArray(identifier.toDotPath(), expression) + +/** + * Determine if a given {@code key} will be touched on execution. + * + * @author Pawel Matysek + * @see Update.modifies + */ +fun Update.modifies(key: KProperty) = + modifies(key.toDotPath()) + diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensionsTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensionsTests.kt new file mode 100644 index 0000000000..994f629270 --- /dev/null +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensionsTests.kt @@ -0,0 +1,251 @@ +/* + * Copyright 2018-2024 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.query + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.springframework.data.mapping.div +import java.time.Instant + +/** + * Unit tests for [Update] extensions. + * + * @author Pawel Matysek + */ +class TypedUpdateExtensionsTests { + + @Test + fun `update() should equal expected Update`() { + + val typed = update(Book::title, "Moby-Dick") + val expected = Update.update("title", "Moby-Dick") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `set() should equal expected Update`() { + + val typed = Update().set(Book::title, "Moby-Dick") + val expected = Update().set("title", "Moby-Dick") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `setOnInsert() should equal expected Update`() { + + val typed = Update().setOnInsert(Book::title, "Moby-Dick") + val expected = Update().setOnInsert("title", "Moby-Dick") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `unset() should equal expected Update`() { + + val typed = Update().unset(Book::title) + val expected = Update().unset("title") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `inc(key, inc) should equal expected Update`() { + + val typed = Update().inc(Book::price, 5) + val expected = Update().inc("price", 5) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `inc(key) should equal expected Update`() { + + val typed = Update().inc(Book::price) + val expected = Update().inc("price") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `push(key, value) should equal expected Update`() { + + val typed = Update().push(Book::categories, "someCategory") + val expected = Update().push("categories", "someCategory") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `push(key) should equal expected Update`() { + + val typed = Update().push(Book::categories) + val expected = Update().push("categories") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `addToSet(key) should equal expected Update`() { + + val typed = Update().addToSet(Book::categories).each("category", "category2") + val expected = Update().addToSet("categories").each("category", "category2") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `addToSet(key, value) should equal expected Update`() { + + val typed = Update().addToSet(Book::categories, "someCategory") + val expected = Update().addToSet("categories", "someCategory") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `pop() should equal expected Update`() { + + val typed = Update().pop(Book::categories, Update.Position.FIRST) + val expected = Update().pop("categories", Update.Position.FIRST) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `pull() should equal expected Update`() { + + val typed = Update().pull(Book::categories, "someCategory") + val expected = Update().pull("categories", "someCategory") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `pullAll() should equal expected Update`() { + + val typed = Update().pullAll(Book::categories, arrayOf("someCategory", "someCategory2")) + val expected = Update().pullAll("categories", arrayOf("someCategory", "someCategory2")) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `currentDate() should equal expected Update`() { + + val typed = Update().currentDate(Book::releaseDate) + val expected = Update().currentDate("releaseDate") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `currentTimestamp() should equal expected Update`() { + + val typed = Update().currentTimestamp(Book::releaseDate) + val expected = Update().currentTimestamp("releaseDate") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `multiply() should equal expected Update`() { + + val typed = Update().multiply(Book::price, 2) + val expected = Update().multiply("price", 2) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `max() should equal expected Update`() { + + val typed = Update().max(Book::price, 200) + val expected = Update().max("price", 200) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `min() should equal expected Update`() { + + val typed = Update().min(Book::price, 100) + val expected = Update().min("price", 100) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `bitwise() should equal expected Update`() { + + val typed = Update().bitwise(Book::price).and(2) + val expected = Update().bitwise("price").and(2) + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `filterArray() should equal expected Update`() { + + val typed = Update().filterArray(Book::categories, "someCategory") + val expected = Update().filterArray("categories", "someCategory") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `typed modifies() should equal expected modifies()`() { + + val typed = update(Book::title, "Moby-Dick") + + assertThat(typed.modifies(Book::title)).isEqualTo(typed.modifies("title")) + assertThat(typed.modifies(Book::price)).isEqualTo(typed.modifies("price")) + } + + @Test + fun `One level nested should equal expected Update`() { + + val typed = update(Book::author / Author::name, "Herman Melville") + val expected = Update.update("author.name", "Herman Melville") + + assertThat(typed).isEqualTo(expected) + } + + @Test + fun `Two levels nested should equal expected Update`() { + + data class Entity(val book: Book) + + val typed = update(Entity::book / Book::author / Author::name, "Herman Melville") + val expected = Update.update("book.author.name", "Herman Melville") + + assertThat(typed).isEqualTo(expected) + } + + data class Book( + val title: String = "Moby-Dick", + val price: Int = 123, + val available: Boolean = true, + val categories: List = emptyList(), + val author: Author = Author(), + val releaseDate: Instant, + ) + + data class Author( + val name: String = "Herman Melville", + ) +} diff --git a/src/main/antora/modules/ROOT/pages/kotlin/extensions.adoc b/src/main/antora/modules/ROOT/pages/kotlin/extensions.adoc index 6ad73541a1..8c03fa176e 100644 --- a/src/main/antora/modules/ROOT/pages/kotlin/extensions.adoc +++ b/src/main/antora/modules/ROOT/pages/kotlin/extensions.adoc @@ -19,7 +19,7 @@ val characters : Flux = template.query().inTable("star-wars").all() As in Java, `characters` in Kotlin is strongly typed, but Kotlin's clever type inference allows for shorter syntax. [[mongo.query.kotlin-support]] -== Type-safe Queries for Kotlin +== Type-safe Queries and Updates for Kotlin Kotlin embraces domain-specific language creation through its language syntax and its extension system. Spring Data MongoDB ships with a Kotlin Extension for `Criteria` using https://kotlinlang.org/docs/reference/reflection.html#property-references[Kotlin property references] to build type-safe queries. @@ -63,3 +63,19 @@ mongoOperations.find( <2> For bitwise operators, pass a lambda argument where you call one of the methods of `Criteria.BitwiseCriteriaOperators`. <3> To construct nested properties, use the `/` character (overloaded operator `div`). ==== + +Similar syntax can be used while updating a document: +==== +[source,kotlin] +---- +mongoOperations.updateMulti( + Query(Book::title isEqualTo "Moby-Dick"), + update(Book:title, "The Whale") <1> + .inc(Book::price, 100) <2> + .addToSet(Book::authors, "Herman Melville") <3> +) +---- +<1> `update()` is a factory function with receiver type `KProperty` that returns `Update`. +<2> Most methods from `Update` have a matching Kotlin extension. +<3> Functions with `KProperty` can be used as well on collections types +====