DATAMONGO-2138 - Add Type-safe Kotlin query extension.

We now support type-safe queries using Kotlin's DSL capabilities by accepting property references. Property references map to property paths and are translated to Criteria objects.

mongoOperations.find<Book>(
  Query(Book::title isEqualTo "Moby-Dick")
)

mongoOperations.find<Book>(
  Query(Book::title exists true)
)

mongoOperations.find<Book>(
 Query(Criteria().andOperator(
  Book::price gt 5,
  Book::price lt 10)
))

Original pull request: #622.
This commit is contained in:
Tjeu Kayim
2018-12-07 07:58:02 +01:00
committed by Mark Paluch
parent bbd02107b2
commit 8d969ef41d
7 changed files with 982 additions and 1 deletions

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.query
import kotlin.reflect.KProperty
/**
* Extension for [Criteria.is] providing an `isEqualTo` alias since `is` is a reserved keyword in Kotlin.
*
@@ -38,3 +40,18 @@ fun <T: Any?> Criteria.inValues(c: Collection<T>) : Criteria = `in`(c)
* @since 2.0
*/
fun Criteria.inValues(vararg o: Any?) : Criteria = `in`(*o)
/**
* Creates a Criteria using a KProperty as key.
* Supports nested field names with [NestedProperty].
* @author Tjeu Kayim
* @since 2.2
*/
fun where(key: KProperty<*>): Criteria = Criteria.where(nestedFieldName(key))
/**
* Add new key to the criteria chain using a KProperty.
* Supports nested field names with [NestedProperty].
* @author Tjeu Kayim
* @since 2.2
*/
infix fun Criteria.and(key: KProperty<*>): Criteria = and(nestedFieldName(key))

View File

@@ -0,0 +1,55 @@
/*
* 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 org.springframework.data.mongodb.core.query
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
/**
* Refer to a field in an embedded/nested document.
* @author Tjeu Kayim
* @since 2.2
*/
class NestedProperty<T, U>(
internal val parent: KProperty<U>,
internal val child: KProperty1<U, T>
) : KProperty<T> by child
/**
* Recursively construct field name for a nested property.
* @author Tjeu Kayim
*/
internal fun nestedFieldName(property: KProperty<*>): String {
return when (property) {
is NestedProperty<*, *> ->
"${nestedFieldName(property.parent)}.${property.child.name}"
else -> property.name
}
}
/**
* Builds [NestedProperty] from Property References.
* Refer to a field in an embedded/nested document.
*
* For example, referring to the field "book.author":
* ```
* Book::author / Author::name isEqualTo "Herman Melville"
* ```
* @author Tjeu Kayim
* @since 2.2
*/
operator fun <T, U> KProperty<T>.div(other: KProperty1<T, U>) =
NestedProperty(this, other)

View File

@@ -0,0 +1,393 @@
/*
* 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 org.springframework.data.mongodb.core.query
import org.bson.BsonRegularExpression
import org.springframework.data.geo.Circle
import org.springframework.data.geo.Point
import org.springframework.data.geo.Shape
import org.springframework.data.mongodb.core.geo.GeoJson
import org.springframework.data.mongodb.core.schema.JsonSchemaObject
import java.util.regex.Pattern
import kotlin.reflect.KProperty
/**
* Creates a criterion using equality.
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.isEqualTo
*/
infix fun <T> KProperty<T>.isEqualTo(value: T) =
Criteria(nestedFieldName(this)).isEqualTo(value)
/**
* Creates a criterion using the $ne operator.
*
* See [MongoDB Query operator: $ne](https://docs.mongodb.com/manual/reference/operator/query/ne/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.ne
*/
infix fun <T> KProperty<T>.ne(value: T): Criteria =
Criteria(nestedFieldName(this)).ne(value)
/**
* Creates a criterion using the $lt operator.
*
* See [MongoDB Query operator: $lt](https://docs.mongodb.com/manual/reference/operator/query/lt/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.lt
*/
infix fun <T> KProperty<T>.lt(value: T): Criteria =
Criteria(nestedFieldName(this)).lt(value)
/**
* Creates a criterion using the $lte operator.
*
* See [MongoDB Query operator: $lte](https://docs.mongodb.com/manual/reference/operator/query/lte/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.lte
*/
infix fun <T> KProperty<T>.lte(value: T): Criteria =
Criteria(nestedFieldName(this)).lte(value)
/**
* Creates a criterion using the $gt operator.
*
* See [MongoDB Query operator: $gt](https://docs.mongodb.com/manual/reference/operator/query/gt/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.gt
*/
infix fun <T> KProperty<T>.gt(value: T): Criteria =
Criteria(nestedFieldName(this)).gt(value)
/**
* Creates a criterion using the $gte operator.
*
* See [MongoDB Query operator: $gte](https://docs.mongodb.com/manual/reference/operator/query/gte/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.gte
*/
infix fun <T> KProperty<T>.gte(value: T): Criteria =
Criteria(nestedFieldName(this)).gte(value)
/**
* Creates a criterion using the $in operator.
*
* See [MongoDB Query operator: $in](https://docs.mongodb.com/manual/reference/operator/query/in/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.inValues
*/
fun <T> KProperty<T>.inValues(vararg o: Any): Criteria =
Criteria(nestedFieldName(this)).`in`(*o)
/**
* Creates a criterion using the $in operator.
*
* See [MongoDB Query operator: $in](https://docs.mongodb.com/manual/reference/operator/query/in/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.inValues
*/
infix fun <T> KProperty<T>.inValues(value: Collection<T>): Criteria =
Criteria(nestedFieldName(this)).`in`(value)
/**
* Creates a criterion using the $nin operator.
*
* See [MongoDB Query operator: $nin](https://docs.mongodb.com/manual/reference/operator/query/nin/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.nin
*/
fun <T> KProperty<T>.nin(vararg o: Any): Criteria =
Criteria(nestedFieldName(this)).nin(*o)
/**
* Creates a criterion using the $nin operator.
*
* See [MongoDB Query operator: $nin](https://docs.mongodb.com/manual/reference/operator/query/nin/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.nin
*/
infix fun <T> KProperty<T>.nin(value: Collection<T>): Criteria =
Criteria(nestedFieldName(this)).nin(value)
/**
* Creates a criterion using the $mod operator.
*
* See [MongoDB Query operator: $mod](https://docs.mongodb.com/manual/reference/operator/query/mod/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.mod
*/
fun KProperty<Number>.mod(value: Number, remainder: Number): Criteria =
Criteria(nestedFieldName(this)).mod(value, remainder)
/**
* Creates a criterion using the $all operator.
*
* See [MongoDB Query operator: $all](https://docs.mongodb.com/manual/reference/operator/query/all/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.all
*/
fun KProperty<*>.all(vararg o: Any): Criteria =
Criteria(nestedFieldName(this)).all(*o)
/**
* Creates a criterion using the $all operator.
*
* See [MongoDB Query operator: $all](https://docs.mongodb.com/manual/reference/operator/query/all/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.all
*/
infix fun KProperty<*>.all(value: Collection<*>): Criteria =
Criteria(nestedFieldName(this)).all(value)
/**
* Creates a criterion using the $size operator.
*
* See [MongoDB Query operator: $size](https://docs.mongodb.com/manual/reference/operator/query/size/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.size
*/
infix fun KProperty<*>.size(s: Int): Criteria =
Criteria(nestedFieldName(this)).size(s)
/**
* Creates a criterion using the $exists operator.
*
* See [MongoDB Query operator: $exists](https://docs.mongodb.com/manual/reference/operator/query/exists/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.exists
*/
infix fun KProperty<*>.exists(b: Boolean): Criteria =
Criteria(nestedFieldName(this)).exists(b)
/**
* Creates a criterion using the $type operator.
*
* See [MongoDB Query operator: $type](https://docs.mongodb.com/manual/reference/operator/query/type/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.type
*/
infix fun KProperty<*>.type(t: Int): Criteria =
Criteria(nestedFieldName(this)).type(t)
/**
* Creates a criterion using the $type operator.
*
* See [MongoDB Query operator: $type](https://docs.mongodb.com/manual/reference/operator/query/type/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.type
*/
infix fun KProperty<*>.type(t: Collection<JsonSchemaObject.Type>): Criteria =
Criteria(nestedFieldName(this)).type(*t.toTypedArray())
/**
* Creates a criterion using the $type operator.
*
* See [MongoDB Query operator: $type](https://docs.mongodb.com/manual/reference/operator/query/type/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.type
*/
fun KProperty<*>.type(vararg t: JsonSchemaObject.Type): Criteria =
Criteria(nestedFieldName(this)).type(*t)
/**
* Creates a criterion using the $not meta operator which affects the clause directly following
*
* See [MongoDB Query operator: $not](https://docs.mongodb.com/manual/reference/operator/query/not/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.not
*/
fun KProperty<*>.not(): Criteria =
Criteria(nestedFieldName(this)).not()
/**
* Creates a criterion using a $regex operator.
*
* See [MongoDB Query operator: $regex](https://docs.mongodb.com/manual/reference/operator/query/regex/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.regex
*/
infix fun KProperty<String?>.regex(re: String): Criteria =
Criteria(nestedFieldName(this)).regex(re, null)
/**
* Creates a criterion using a $regex and $options operator.
*
* See [MongoDB Query operator: $regex](https://docs.mongodb.com/manual/reference/operator/query/regex/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.regex
*/
fun KProperty<String?>.regex(re: String, options: String?): Criteria =
Criteria(nestedFieldName(this)).regex(re, options)
/**
* Syntactical sugar for [isEqualTo] making obvious that we create a regex predicate.
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.regex
*/
infix fun KProperty<String?>.regex(re: Regex): Criteria =
Criteria(nestedFieldName(this)).regex(re.toPattern())
/**
* Syntactical sugar for [isEqualTo] making obvious that we create a regex predicate.
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.regex
*/
infix fun KProperty<String?>.regex(re: Pattern): Criteria =
Criteria(nestedFieldName(this)).regex(re)
/**
* Syntactical sugar for [isEqualTo] making obvious that we create a regex predicate.
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.regex
*/
infix fun KProperty<String?>.regex(re: BsonRegularExpression): Criteria =
Criteria(nestedFieldName(this)).regex(re)
/**
* Creates a geospatial criterion using a $geoWithin $centerSphere operation. This is only available for
* Mongo 2.4 and higher.
*
* See [MongoDB Query operator:
* $geoWithin](https://docs.mongodb.com/manual/reference/operator/query/geoWithin/)
*
* See [MongoDB Query operator:
* $centerSphere](https://docs.mongodb.com/manual/reference/operator/query/centerSphere/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.withinSphere
*/
infix fun KProperty<GeoJson<*>>.withinSphere(circle: Circle): Criteria =
Criteria(nestedFieldName(this)).withinSphere(circle)
/**
* Creates a geospatial criterion using a $geoWithin operation.
*
* See [MongoDB Query operator:
* $geoWithin](https://docs.mongodb.com/manual/reference/operator/query/geoWithin/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.within
*/
infix fun KProperty<GeoJson<*>>.within(shape: Shape): Criteria =
Criteria(nestedFieldName(this)).within(shape)
/**
* Creates a geospatial criterion using a $near operation.
*
* See [MongoDB Query operator: $near](https://docs.mongodb.com/manual/reference/operator/query/near/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.near
*/
infix fun KProperty<GeoJson<*>>.near(point: Point): Criteria =
Criteria(nestedFieldName(this)).near(point)
/**
* Creates a geospatial criterion using a $nearSphere operation. This is only available for Mongo 1.7 and
* higher.
*
* See [MongoDB Query operator:
* $nearSphere](https://docs.mongodb.com/manual/reference/operator/query/nearSphere/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.nearSphere
*/
infix fun KProperty<GeoJson<*>>.nearSphere(point: Point): Criteria =
Criteria(nestedFieldName(this)).nearSphere(point)
/**
* Creates criterion using `$geoIntersects` operator which matches intersections of the given `geoJson`
* structure and the documents one. Requires MongoDB 2.4 or better.
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.intersects
*/
infix fun KProperty<GeoJson<*>>.intersects(geoJson: GeoJson<*>): Criteria =
Criteria(nestedFieldName(this)).intersects(geoJson)
/**
* Creates a geo-spatial criterion using a $maxDistance operation, for use with $near
*
* See [MongoDB Query operator:
* $maxDistance](https://docs.mongodb.com/manual/reference/operator/query/maxDistance/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.maxDistance
*/
infix fun KProperty<GeoJson<*>>.maxDistance(d: Double): Criteria =
Criteria(nestedFieldName(this)).maxDistance(d)
/**
* Creates a geospatial criterion using a $minDistance operation, for use with $near or
* $nearSphere.
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.minDistance
*/
infix fun KProperty<GeoJson<*>>.minDistance(d: Double): Criteria =
Criteria(nestedFieldName(this)).minDistance(d)
/**
* Creates a criterion using the $elemMatch operator
*
* See [MongoDB Query operator:
* $elemMatch](https://docs.mongodb.com/manual/reference/operator/query/elemMatch/)
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.elemMatch
*/
infix fun KProperty<*>.elemMatch(c: Criteria): Criteria =
Criteria(nestedFieldName(this)).elemMatch(c)
/**
* Use [Criteria.BitwiseCriteriaOperators] as gateway to create a criterion using one of the
* [bitwise operators](https://docs.mongodb.com/manual/reference/operator/query-bitwise/) like
* `$bitsAllClear`.
*
* Example:
* ```
* bits { allClear(123) }
* ```
* @author Tjeu Kayim
* @since 2.2
* @see Criteria.bits
*/
infix fun KProperty<*>.bits(bitwiseCriteria: Criteria.BitwiseCriteriaOperators.() -> Criteria) =
Criteria(nestedFieldName(this)).bits().let(bitwiseCriteria)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-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.
@@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.query
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
@@ -24,6 +25,7 @@ import org.mockito.junit.MockitoJUnitRunner
/**
* @author Sebastien Deleuze
* @author Tjeu Kayim
*/
@RunWith(MockitoJUnitRunner::class)
class CriteriaExtensionsTests {
@@ -87,4 +89,43 @@ class CriteriaExtensionsTests {
Mockito.verify(criteria, Mockito.times(1)).`in`(c)
}
@Test
fun `and(KProperty) extension should call its Java counterpart`() {
criteria.and(Book::title)
Mockito.verify(criteria, Mockito.times(1)).and("title")
}
@Test
fun `and(KProperty) extension should support nested properties`() {
criteria.and(Book::author / Author::name)
Mockito.verify(criteria, Mockito.times(1)).and("author.name")
}
@Test
fun `where(KProperty) should equal Criteria where()`() {
class Book(val title: String)
val typedCriteria = where(Book::title)
val classicCriteria = Criteria.where("title")
assertThat(typedCriteria).isEqualTo(classicCriteria)
}
@Test
fun `where(KProperty) should support nested properties`() {
val typedCriteria = where(Book::author / Author::name)
val classicCriteria = Criteria.where("author.name")
assertThat(typedCriteria).isEqualTo(classicCriteria)
}
class Book(val title: String, val author: Author)
class Author(val name: String)
}

View File

@@ -0,0 +1,65 @@
/*
* 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 org.springframework.data.mongodb.core.query
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
/**
* @author Tjeu Kayim
*/
class NestedPropertyTests {
@Test
fun `Convert normal KProperty to field name`() {
val property = nestedFieldName(Book::title)
assertThat(property).isEqualTo("title")
}
@Test
fun `Convert nested KProperty to field name`() {
val property = nestedFieldName(Book::author / Author::name)
assertThat(property).isEqualTo("author.name")
}
@Test
fun `Convert double nested KProperty to field name`() {
class Entity(val book: Book)
val property = nestedFieldName(Entity::book / Book::author / Author::name)
assertThat(property).isEqualTo("book.author.name")
}
@Test
fun `Convert triple nested KProperty to field name`() {
class Entity(val book: Book)
class AnotherEntity(val entity: Entity)
val property = nestedFieldName(AnotherEntity::entity / Entity::book / Book::author / Author::name)
assertThat(property).isEqualTo("entity.book.author.name")
}
class Book(val title: String, val author: Author)
class Author(val name: String)
}

View File

@@ -0,0 +1,375 @@
/*
* 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 org.springframework.data.mongodb.core.query
import org.assertj.core.api.Assertions.*
import org.bson.BsonRegularExpression
import org.junit.Test
import org.springframework.data.geo.Circle
import org.springframework.data.geo.Point
import org.springframework.data.mongodb.core.geo.GeoJsonPoint
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.*
import java.util.regex.Pattern
/**
* @author Tjeu Kayim
*/
class TypedCriteriaExtensionsTests {
@Test
fun `isEqualTo() should equal classic criteria`() {
val typed = Book::title isEqualTo "Moby-Dick"
val classic = Criteria("title").isEqualTo("Moby-Dick")
assertThat(typed).isEqualTo(classic)
}
@Test
fun `ne() should equal classic criteria`() {
val typed = Book::title ne "Moby-Dick"
val classic = Criteria("title").ne("Moby-Dick")
assertThat(typed).isEqualTo(classic)
}
@Test
fun `lt() should equal classic criteria`() {
val typed = Book::price lt 100
val classic = Criteria("price").lt(100)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `lte() should equal classic criteria`() {
val typed = Book::price lte 100
val classic = Criteria("price").lte(100)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `gt() should equal classic criteria`() {
val typed = Book::price gt 100
val classic = Criteria("price").gt(100)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `gte() should equal classic criteria`() {
val typed = Book::price gte 100
val classic = Criteria("price").gte(100)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `inValues(vararg) should equal classic criteria`() {
val typed = Book::price.inValues(1, 2, 3)
val classic = Criteria("price").inValues(1, 2, 3)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `inValues(list) should equal classic criteria`() {
val typed = Book::price inValues listOf(1, 2, 3)
val classic = Criteria("price").inValues(listOf(1, 2, 3))
assertThat(typed).isEqualTo(classic)
}
@Test
fun `nin(vararg) should equal classic criteria`() {
val typed = Book::price.nin(1, 2, 3)
val classic = Criteria("price").nin(1, 2, 3)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `nin(list) should equal classic criteria`() {
val typed = Book::price nin listOf(1, 2, 3)
val classic = Criteria("price").nin(listOf(1, 2, 3))
assertThat(typed).isEqualTo(classic)
}
@Test
fun `mod() should equal classic criteria`() {
val typed = Book::price.mod(2, 3)
val classic = Criteria("price").mod(2, 3)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `all(vararg) should equal classic criteria`() {
val typed = Book::categories.all(1, 2, 3)
val classic = Criteria("categories").all(1, 2, 3)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `all(list) should equal classic criteria`() {
val typed = Book::categories all listOf(1, 2, 3)
val classic = Criteria("categories").all(listOf(1, 2, 3))
assertThat(typed).isEqualTo(classic)
}
@Test
fun `size() should equal classic criteria`() {
val typed = Book::categories size 4
val classic = Criteria("categories").size(4)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `exists() should equal classic criteria`() {
val typed = Book::title exists true
val classic = Criteria("title").exists(true)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `type(Int) should equal classic criteria`() {
val typed = Book::title type 2
val classic = Criteria("title").type(2)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `type(List) should equal classic criteria`() {
val typed = Book::title type listOf(Type.STRING, Type.BOOLEAN)
val classic = Criteria("title").type(Type.STRING, Type.BOOLEAN)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `type(vararg) should equal classic criteria`() {
val typed = Book::title.type(Type.STRING, Type.BOOLEAN)
val classic = Criteria("title").type(Type.STRING, Type.BOOLEAN)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `not() should equal classic criteria`() {
val typed = Book::price.not().lt(123)
val classic = Criteria("price").not().lt(123)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `regex(string) should equal classic criteria`() {
val typed = Book::title regex "ab+c"
val classic = Criteria("title").regex("ab+c")
assertEqualCriteriaByJson(typed, classic)
}
@Test
fun `regex(string, options) should equal classic criteria`() {
val typed = Book::title.regex("ab+c", "g")
val classic = Criteria("title").regex("ab+c", "g")
assertEqualCriteriaByJson(typed, classic)
}
@Test
fun `regex(Regex) should equal classic criteria`() {
val typed = Book::title regex Regex("ab+c")
val classic = Criteria("title").regex(Pattern.compile("ab+c"))
assertEqualCriteriaByJson(typed, classic)
}
private fun assertEqualCriteriaByJson(typed: Criteria, classic: Criteria) {
assertThat(typed.criteriaObject.toJson()).isEqualTo(classic.criteriaObject.toJson())
}
@Test
fun `regex(Pattern) should equal classic criteria`() {
val value = Pattern.compile("ab+c")
val typed = Book::title regex value
val classic = Criteria("title").regex(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `regex(BsonRegularExpression) should equal classic criteria`() {
val expression = BsonRegularExpression("ab+c")
val typed = Book::title regex expression
val classic = Criteria("title").regex(expression)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `withinSphere() should equal classic criteria`() {
val value = Circle(Point(928.76, 28.345), 65.243)
val typed = Building::location withinSphere value
val classic = Criteria("location").withinSphere(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `within() should equal classic criteria`() {
val value = Circle(Point(5.43421, 12.456), 52.67)
val typed = Building::location within value
val classic = Criteria("location").within(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `near() should equal classic criteria`() {
val value = Point(57.431, 71.345)
val typed = Building::location near value
val classic = Criteria("location").near(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `nearSphere() should equal classic criteria`() {
val value = Point(5.4321, 12.345)
val typed = Building::location nearSphere value
val classic = Criteria("location").nearSphere(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `intersects() should equal classic criteria`() {
val value = GeoJsonPoint(5.481573, 51.451726)
val typed = Building::location intersects value
val classic = Criteria("location").intersects(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `maxDistance() should equal classic criteria`() {
val typed = Building::location maxDistance 3.0
val classic = Criteria("location").maxDistance(3.0)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `minDistance() should equal classic criteria`() {
val typed = Building::location minDistance 3.0
val classic = Criteria("location").minDistance(3.0)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `elemMatch() should equal classic criteria`() {
val value = Criteria("price").lt(950)
val typed = Book::title elemMatch value
val classic = Criteria("title").elemMatch(value)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `elemMatch(TypedCriteria) should equal classic criteria`() {
val typed = Book::title elemMatch (Book::price lt 950)
val classic = Criteria("title").elemMatch(Criteria("price").lt(950))
assertThat(typed).isEqualTo(classic)
}
@Test
fun `bits() should equal classic criteria`() {
val typed = Book::title bits { allClear(123) }
val classic = Criteria("title").bits().allClear(123)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `One level nested should equal classic criteria`() {
val typed = Book::author / Author::name isEqualTo "Herman Melville"
val classic = Criteria("author.name").isEqualTo("Herman Melville")
assertThat(typed).isEqualTo(classic)
}
@Test
fun `Two levels nested should equal classic criteria`() {
data class Entity(val book: Book)
val typed = Entity::book / Book::author / Author::name isEqualTo "Herman Melville"
val classic = Criteria("book.author.name").isEqualTo("Herman Melville")
assertThat(typed).isEqualTo(classic)
}
@Test
fun `typed criteria inside orOperator() should equal classic criteria`() {
val typed = (Book::title isEqualTo "Moby-Dick").orOperator(
Book::price lt 1200,
Book::price gt 240
)
val classic = Criteria("title").isEqualTo("Moby-Dick")
.orOperator(
Criteria("price").lt(1200),
Criteria("price").gt(240)
)
assertThat(typed).isEqualTo(classic)
}
@Test
fun `chaining gt & isEqualTo() should equal classic criteria`() {
val typed = (Book::title isEqualTo "Moby-Dick")
.and(Book::price).lt(950)
val classic = Criteria("title").isEqualTo("Moby-Dick")
.and("price").lt(950)
assertThat(typed).isEqualTo(classic)
}
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()
)
data class Author(
val name: String = "Herman Melville"
)
data class Building(
val location: GeoJsonPoint
)
}

View File

@@ -1792,6 +1792,41 @@ GeoResults<Jedi> results = mongoOps.query(SWCharacter.class)
----
====
[[mongo.query.kotlin-support]]
=== Type-safe Queries for Kotlin
https://kotlinlang.org/docs/reference/reflection.html#property-references[Kotlin property references]
can be used to build type-safe queries.
Most methods in `Criteria` have a matching Kotlin extension, like `inValues` and `regex`.
====
[source,kotlin]
----
import org.springframework.data.mongodb.core.query.*
mongoOperations.find<Book>(
Query(Book::title isEqualTo "Moby-Dick") <1>
)
Book::title exists true
Criteria().andOperator(
Book::price gt 5,
Book::price lt 10
)
// Binary operators
BinaryMessage::payload bits { allClear(0b101) } <2>
// Nested Properties (i.e. refer to "book.author")
Book::author / Author::name regex "^H" <3>
----
<1> `isEqualTo()` is an infix extension function with receiver type `KProperty<T>` that returns `Criteria`.
<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`).
====
[[mongo.query.additional-query-options]]
=== Additional Query Options