Skip to content

Typed Queries

Type-safe Queries

KMongo provides a type-safe query framework.

Kotlin property references are used to build mongo queries.

For example, here eq and regexp are infix functions provided by KMongo:

data class Jedi(val name: String)

val yoda = col.findOne(Jedi::name eq "Yoda")

//compile error as 2 is not a String ->
val error = col.findOne(Jedi::name eq 2)

//you can use property reference with instances
val yoda2 = col.findOne(yoda::name regex "Yo.*")

eq & contains

Mongo uses the eq operator both for document value and array value. In order to be type-safe, KMongo introduces the contains function for arrays:

data class Article(val title: String,val tags: List<String>)

col.aggregate<Article>(match(Article::tags contains "virus")) (...)

Nested properties and / operator

To define nested properties, KMongo introduces the / operator.

So for example:

class Friend(val coor: Coordinate?)
class Coordinate(val lat: Int, val lng : Int)

// the generated query is {"coor.lat":{$lt:0}}
col.findOne(Friend::coor / Coordinate::lat lt 0)

Unsafe nested properties and % operator

Sometimes you would need to bypass typesafe checking when writing nested property path. Then use the % operator - it has the same behaviour as / but without typesafe checks:

sealed class Shape(val type: String) {
    data class Circle(val radius: Int) : Shape("Circle")
}

data class Box(val shape: Shape)

val bson = setValue(Box::shape % Shape.Circle::radius, 0)

Debugging Typed queries

You can use the .json extension to print the json version of the query.

For example:

println(
    setValue(
        (Project::reports.filteredPosOp("report") / ProjectReport::points)
        .filteredPosOp("point") / ProjectReportPoint::published,
        now
    ).json
)

print

{"$set": {"reports.$[report].points.$[point].published": {"$date": "2020-12-06T20:36:21.576Z"}}}

Samples

Queries with and & or

col.findOne(or(Jedi::name eq "Yoda", Jedi::age gt 25))  
col.findOne(and(Jedi::name eq "Yoda", Jedi::age gt 25))

//The and is implicit
col.findOne(Jedi::name eq "Yoda", Jedi::age gt 25)

Update

//Both are equivalent
col.updateOne(friend::name eq "Paul", setValue(friend::name, "John"))   
col.updateOne(friend::name eq "Paul", Friend::name setTo "John")

//Multi fields update 
col.updateOne(friend::name eq "Paul", set( Friend::name setTo "John", Friend::age setTo 25))

//other operations are supported
col.updateOne(Friend::name eq "John", pull(Friend::tags, "t2"))

Array operators

You can use positional array operators:

//Both are equivalent
"accesses.0.timestamp""

Container::accesses.pos(0) / Access::timestamp 
data class EvaluationAnswer(val answers:List<MyAnswer>)
data class MyAnswer(val _id:String, val alreadyUsed: Boolean)

//Both are equivalent

col.updateMany( "{ \"answers._id\": { \$in: answerIds } }, { $set:{ \"answers.\$[].alreadyUsed\": true}}")

col.updateMany(
            (EvaluationAnswer::answers / MyAnswer::_id) `in` answerIds,
            setValue(EvaluationAnswer::answers.allPosOp / MyAnswer::alreadyUsed, true)
        )

Cheat Sheet:

.0.             = pos(0)
$               = posOp
$[]             = allPosOp
$[<identifier>] = filteredPosOp 

Map operators

data class Friend(val localeMap: Map<Locale, Gift>)
data class Gift(val amount: BigDecimal)

//returns true
assertEquals("localeMap.en.amount", (Friend::localeMap.keyProjection(Locale.ENGLISH) / Gift::amount).path())

Aggregation

You can chain aggregation operators:

data class Article(
        val title: String,
        val author: String,
        val tags: List<String>,
        val date: Instant = Instant.now(),
        val count: Int = 1,
        val ok: Boolean = true
    )

data class Result(
        @BsonId val title: String,
        val averageYear: Double = 0.0,
        val count: Int = 0,
        val friends: List<Friend> = emptyList()
    )

val r = col.aggregate<Result>(
            match(
                Article::tags contains "virus"
            ),
            group(
                Article::title, Result::friends.push(Friend::name from Article::author)
            ),
            sort(
                ascending(
                    Result::title
                )
            )
        )

Other example:

val result = col.aggregate<Result>(
            match(
                Article::tags contains "virus"
            ),
            project(
                Article::title from Article::title,
                Article::ok from cond(Article::ok, 1, 0),
                Result::averageYear from year(Article::date)
            ),
            group(
                Article::title,
                Result::count sum Article::ok,
                Result::averageYear avg Result::averageYear
            ),
            sort(
                ascending(
                    Result::title
                )
            )
        )

Lookup sample:

data class Answer(val evaluator: String, val alreadyUsed: Boolean, val answerDate: Instant)

data class EvaluationsForms(val questions: List<String>)

data class EvaluationsFormsWithResults(val questions: List<String>, val results: List<EvaluationRequest>)

data class EvaluationsAnswers(val questionId: String, val evaluated: String, val answers: List<Answer>)

data class EvaluationRequest(val userId: String, val evaluationDate: String) 

val bson = lookup(
            "evaluationsAnswers",
            listOf(EvaluationsForms::questions.variableDefinition()),
            EvaluationsFormsWithResults::results,
            match(
                expr(
                    and from listOf(
                        `in` from listOf(EvaluationsAnswers::questionId, EvaluationsForms::questions.variable),
                        eq from listOf(EvaluationsAnswers::evaluated, "id")
                    )
                )
            ),
            EvaluationsAnswers::answers.unwind(),
            match(
                expr(
                    and from listOf(
                        eq from listOf(EvaluationsAnswers::answers / Answer::alreadyUsed, false),
                        gte from listOf(EvaluationsAnswers::answers / Answer::answerDate, Instant.now())
                    )
                )
            ),
            group(
                fields(
                    EvaluationRequest::userId from (EvaluationsAnswers::answers / Answer::evaluator),
                    EvaluationRequest::evaluationDate from (
                            dateToString from (
                                    combine(
                                        "format" from "%Y-%m-%d",
                                        "date" from (EvaluationsAnswers::answers / Answer::answerDate)
                                    )
                                    )
                            )
                )
            ),
            replaceRoot("_id".projection)
        )

expr & equals

match(
    expr(
        "eq".projection from listOf( 
            (HistoryEventWrapper<HistoricVariableInstance>::event / HistoricVariableInstance::processInstanceId).projection,
            HistoricProcessInstance::processInstanceId.variable
        )
    )
)

generates

{"$match": {"$expr": {"$eq": ["$event.processInstanceId", "$$processInstanceId"]}}}

KMongo Annotation processor

Specifying the property references can be cumbersome, especially for nested properties.

KMongo provides an annotation processor to generate these property references.

You just have to annotate the data class with the @Data annotation.

Then declare the annotation processor

  • for Maven:
<plugin>
    <executions>
        <execution>
            <id>kapt</id>
            <goals>
                <goal>kapt</goal>
            </goals>
            <configuration>
                <annotationProcessorPaths>
                    <annotationProcessorPath>
                        <groupId>org.litote.kmongo</groupId>
                        <artifactId>kmongo-annotation-processor</artifactId>
                        <version>${kmongo.version}</version>
                    </annotationProcessorPath>
                </annotationProcessorPaths>
            </configuration>
        </execution>

        <execution>
            <id>compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>

    </executions>
</plugin>
  • or for Gradle
plugins {
    id "org.jetbrains.kotlin.kapt" version "${kotlin.version}"
}

dependencies {
    kapt 'org.litote.kmongo:kmongo-annotation-processor:${kmongo.version}'
}

(See the dedicated Kotlin page for more information about annotation processing.)

Now you can write:

import Friend_.Coor

@Data
class Friend(val coor: Coordinate?)

@Data
class Coordinate(val lat: Int, val lng : Int)

val col : Collection<Friend>
col.findOne(Coor.lat lt 0, Coor.lng gt 0)

@DataRegistry

You can use this annotation (that annotates a dedicated object for example) if you can't or don't want to annotate directly with @Data the target classes.

Limitations

For now, the annotation processor has the following known limitations:

  • all @Data annotated classes must have public visibility and public properties. If your classes are internal, you can use @Data(internal = true) but then classes that reference the target class must be internal also.
  • Collections of Nullable types (ie Collection<Any?>) are not yet supported