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:
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user