Browse Source

Add Kotlin extension for type-safe Update API.

Closes: #3028
Original Pull Request: #4753
pull/4791/head
Pawel Matysek 1 year ago committed by Christoph Strobl
parent
commit
df08576b3a
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
  2. 208
      spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt
  3. 251
      spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensionsTests.kt
  4. 18
      src/main/antora/modules/ROOT/pages/kotlin/extensions.adoc

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java

@ -404,7 +404,7 @@ public class Update implements UpdateDefinition { @@ -404,7 +404,7 @@ public class Update implements UpdateDefinition {
/**
* 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.

208
spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt

@ -0,0 +1,208 @@ @@ -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 <T> update(key: KProperty<T>, value: T?) =
Update.update(key.toDotPath(), value)
/**
* Update using the {@literal $set} update modifier
*
* @author Pawel Matysek
* @see Update.set
*/
fun <T> Update.set(key: KProperty<T>, value: T?) =
set(key.toDotPath(), value)
/**
* Update using the {@literal $setOnInsert} update modifier
*
* @author Pawel Matysek
* @see Update.setOnInsert
*/
fun <T> Update.setOnInsert(key: KProperty<T>, value: T?) =
setOnInsert(key.toDotPath(), value)
/**
* Update using the {@literal $unset} update modifier
*
* @author Pawel Matysek
* @see Update.unset
*/
fun <T> Update.unset(key: KProperty<T>) =
unset(key.toDotPath())
/**
* Update using the {@literal $inc} update modifier
*
* @author Pawel Matysek
* @see Update.inc
*/
fun <T> Update.inc(key: KProperty<T>, inc: Number) =
inc(key.toDotPath(), inc)
fun <T> Update.inc(key: KProperty<T>) =
inc(key.toDotPath())
/**
* Update using the {@literal $push} update modifier
*
* @author Pawel Matysek
* @see Update.push
*/
fun <T> Update.push(key: KProperty<Collection<T>>, value: T?) =
push(key.toDotPath(), value)
/**
* Update using {@code $push} modifier. <br/>
* 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 <T> Update.push(key: KProperty<T>) =
push(key.toDotPath())
/**
* Update using {@code $addToSet} modifier. <br/>
* Allows creation of {@code $push} command for single or multiple (using {@code $each}) values * {@code $position}.
*
* @author Pawel Matysek
* @see Update.addToSet
*/
fun <T> Update.addToSet(key: KProperty<T>) =
addToSet(key.toDotPath())
/**
* Update using the {@literal $addToSet} update modifier
*
* @author Pawel Matysek
* @see Update.addToSet
*/
fun <T> Update.addToSet(key: KProperty<Collection<T>>, value: T?) =
addToSet(key.toDotPath(), value)
/**
* Update using the {@literal $pop} update modifier
*
* @author Pawel Matysek
* @see Update.pop
*/
fun <T> Update.pop(key: KProperty<T>, pos: Position) =
pop(key.toDotPath(), pos)
/**
* Update using the {@literal $pull} update modifier
*
* @author Pawel Matysek
* @see Update.pull
*/
fun <T> Update.pull(key: KProperty<T>, value: Any) =
pull(key.toDotPath(), value)
/**
* Update using the {@literal $pullAll} update modifier
*
* @author Pawel Matysek
* @see Update.pullAll
*/
fun <T> Update.pullAll(key: KProperty<Collection<T>>, values: Array<T>) =
pullAll(key.toDotPath(), values)
/**
* Update given key to current date using {@literal $currentDate} modifier.
*
* @author Pawel Matysek
* @see Update.currentDate
*/
fun <T> Update.currentDate(key: KProperty<T>) =
currentDate(key.toDotPath())
/**
* Update given key to current date using {@literal $currentDate : &#123; $type : "timestamp" &#125;} modifier.
*
* @author Pawel Matysek
* @see Update.currentTimestamp
*/
fun <T> Update.currentTimestamp(key: KProperty<T>) =
currentTimestamp(key.toDotPath())
/**
* Multiply the value of given key by the given number.
*
* @author Pawel Matysek
* @see Update.multiply
*/
fun <T> Update.multiply(key: KProperty<T>, 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 <T : Any> Update.max(key: KProperty<T>, 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 <T : Any> Update.min(key: KProperty<T>, 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 <T> Update.bitwise(key: KProperty<T>) =
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 <T> Update.filterArray(identifier: KProperty<T>, expression: Any) =
filterArray(identifier.toDotPath(), expression)
/**
* Determine if a given {@code key} will be touched on execution.
*
* @author Pawel Matysek
* @see Update.modifies
*/
fun <T> Update.modifies(key: KProperty<T>) =
modifies(key.toDotPath())

251
spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensionsTests.kt

@ -0,0 +1,251 @@ @@ -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<String> = emptyList(),
val author: Author = Author(),
val releaseDate: Instant,
)
data class Author(
val name: String = "Herman Melville",
)
}

18
src/main/antora/modules/ROOT/pages/kotlin/extensions.adoc

@ -19,7 +19,7 @@ val characters : Flux<SWCharacter> = template.query().inTable("star-wars").all() @@ -19,7 +19,7 @@ val characters : Flux<SWCharacter> = 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<Book>( @@ -63,3 +63,19 @@ mongoOperations.find<Book>(
<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<Book>(
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<T>` that returns `Update`.
<2> Most methods from `Update` have a matching Kotlin extension.
<3> Functions with `KProperty<T>` can be used as well on collections types
====

Loading…
Cancel
Save