Introducció

Projecte de suport: https://gitlab.com/xtec/kotlin/mongodb

Entorn de treball

Crea la carpeta mongodb i entra dins la carpeta

> md mongodb
> cd mongodb

Executa gradle init amb els paràmetres que es mostren a continuació per generar una aplicació Kotlin amb el nom mongodb:

gradle init --package mongodb --project-name mongodb --java-version 21 --type kotlin-application --dsl kotlin --test-framework kotlintest --no-split-project --no-incubating --overwrite

Modifica el fitxer app/build.gradle.kts:

dependencies {
    // ...
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
    implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.3.0")
}

Modifica el fitxer App.kt:

package mongodb

import com.mongodb.kotlin.client.coroutine.MongoClient
import kotlinx.coroutines.runBlocking
import org.bson.BsonInt64
import org.bson.Document

fun main() = runBlocking {

    val client = MongoClient.create("mongodb://localhost:27017")
    val database = client.getDatabase("pets")

    // Provem que tenim connexió
    database.runCommand(Document("ping", BsonInt64(1)))
    println("Successfully connected to MongoDB")


    client.close()
}

La connexió amb la nostra base de dades es pot dividir en dos passos:

  • Primer, creem una instància de MongoClient amb Connection URI.
  • I en segon lloc, utilitzeu el client per connectar-vos amb la base de dades pets.

Executa l'aplicació.

Si no tens arrencat la base de dades MongoDB al cap d'una estona tens un error de tipus MongoTimeoutException:

Exception in thread "main" com.mongodb.MongoTimeoutException: Timed out while waiting for a server that matches ReadPreferenceServerSelector{readPreference=primary}. Client view of cluster state is {type=UNKNOWN, servers=[{address=localhost:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.io.IOException: The remote computer refused the network connection}}]
...

Arrenca la base de dades local tal com s'explica a MongoDB - Fonaments i verifica que el codi funciona correctament

> mogodb

Insert

Crea una classe de dades Dog:

data class Dog(
    @BsonId
    val id: ObjectId,
    val name: String
)

L'anotació @BsonId representa la identitat única o _id del document.

A continuació inserta un gos amb el mètode insertOne():

fun main() = runBlocking {
    // ...

    val collection = database.getCollection<Dog>("dogs")

    val result = collection.insertOne(Dog(ObjectId(),"Trufa"))
    println(result.insertedId)
}

Com que la col.leció dogs encara no existeix, el servidor la crea automàticament.

Si la inserció és correcta, insertOne() retorna una instància de InsertOneResult que et permet recuperar informació com ara el camp _id del document que heu inserit amb insertId.

Si la vostra operació d'inserció falla, el controlador genera una excepció.

A continuació inserta diversos gossos en una sola operació amb el mètode insertMany().

fun main() = runBlocking {
    // ...

    val collection = database.getCollection<Dog>("dogs")
    collection.drop()

    val dogs = listOf(Dog(ObjectId(), "Trufa"), Dog(ObjectId(), "Ketzu"),
                      Dog(ObjectId(), "Marilyn"))

    val result = collection.insertMany(dogs)
    println(result.insertedIds)

}

Per evitar tenir documents duplicats, primer eliminem la col.lecció.

Després d'una inserció correcta, insertMany() retorna una instància de InsertManyResult.

Find

Modifica la classe de dades Dog per incloure la raça del gos:

data class Dog(
    @BsonId
    val id: ObjectId,
    val name: String,
    val breed: String?
)

El mètode find() et permet buscar documents dins una col.lecció.

Si no inclous cap filtre, el mètode find() torna un cursor que itera tots els element de la col.lecció:

// ...

    collection.insertMany(
        listOf(
            Dog(ObjectId(), "Trufa", "Rough Collie"),
            Dog(ObjectId(), "Ketzu", "Shiba Inu"),
            Dog(ObjectId(), "Marilyn", null),
            Dog(ObjectId(), "Duc", "Rough Collie"),
            Dog(ObjectId(), "Milo", "Border Collie"),
        )   
    ).also { println(it.insertedIds)}

    collection.find<Dog>().collect { println(it) }

Pots veure que tots els gossos s'han guardat a la base de dades:

Successfully connected to MongoDB
{0=BsonObjectId{value=67853f94ab522d5b4fa362b2}, 1=BsonObjectId{value=67853f94ab522d5b4fa362b3}, 2=BsonObjectId{value=67853f94ab522d5b4fa362b4}, 3=BsonObjectId{value=67853f94ab522d5b4fa362b5}, 4=BsonObjectId{value=67853f94ab522d5b4fa362b6}}
Dog(id=67853f94ab522d5b4fa362b2, name=Trufa, breed=Rough Collie)
Dog(id=67853f94ab522d5b4fa362b3, name=Ketzu, breed=Shiba Inu)
Dog(id=67853f94ab522d5b4fa362b4, name=Marilyn, breed=null)
Dog(id=67853f94ab522d5b4fa362b5, name=Duc, breed=Rough Collie)
Dog(id=67853f94ab522d5b4fa362b6, name=Milo, breed=Border Collie)

El mètode find() retorna una instància de FindFlow, una classe que ofereix diversos mètodes per accedir, organitzar i recórrer els resultats com ara first() i firstOrNull().

El mètode firstOrNull() retorna el primer document dels resultats recuperats o null si no hi ha resultats.

import kotlinx.coroutines.flow.firstOrNull

// ...

    val dog = collection.find<Dog>(eq(Dog::name.name,"Lassie")).firstOrNull()
    println(dog?.name)

El mètode first() retorna el primer document o llança una excepció NoSuchElementException si cap document coincideix amb la consulta.

import com.mongodb.client.model.Filters.eq
import kotlinx.coroutines.flow.first

// ...

    val dog = collection.find<Dog>(eq(Dog::name.name,"Lassie")).first()
    println(dog.name)

Filter

Els filtres són operacions que s'utilitzen per limitar els resultats d'una consulta en funció de condicions especificades.

La classe Filters proporciona mètodes estàtics per a tots els operadors de consulta de MongoDB. Cada mètode retorna una instància del tipus BSON , que podeu passar a qualsevol mètode que espere un filtre de consulta.

import com.mongodb.client.model.Filters.*

Per exemple, pots buscar tots els gossos de raça "Rough Collie" amb l'operador eq:

    collection.find<Dog>(eq(Dog::breed.name, "Rough Collie")).collect { print("${it.name} ") }

O tots els gossos de raça "Rough Collie" o "BorderCollie" amb l'operador eq:

    collection.find<Dog>(
        or(
            listOf(
                eq(Dog::breed.name, "Rough Collie"), eq(Dog::breed.name, "Border Collie")
            )
        )
    ).collect { print("${it.name} ") } // Milo Trufa Duc Lassie 

Encara que aquest exemple seria millor amb l'operador in ja que estem filtrant la mateixa propietat:

 collection.find<Dog>(
        `in`(Dog::breed.name, listOf("Rough Collie", "Border Collie"))
    ).collect { print("${it.name} ") } // Milo Trufa Duc Lassie

Paginació

Amb els operadors limit i skip pots afegir paginació als resultats:

    // ...
    
    collection.find<Dog>().limit(2).collect { print("${it.name} ") }
    collection.find<Dog>().limit(2).skip(2).collect { print("${it.name} ") }

    // Trufa Ketzu Marilyn Duc 

Però amb aquest enfocament, sovint, el temps de resposta de la consulta augmenta amb el valor de offset.

Per superar-ho, ens podem beneficiar creant un Index, com es mostra a continuació:

    // ...

    val collection = database.getCollection<Dog>("dogs")
    collection.drop()
    collection.createIndex(
        keys = Indexes.ascending(Dog::breed.name)
    )


    collection.insertMany(
        listOf(
            Dog(ObjectId(), "Trufa", "Rough Collie"),
            Dog(ObjectId(), "Ketzu", "Shiba Inu"),
            Dog(ObjectId(), "Marilyn", null),
            Dog(ObjectId(), "Duc", "Rough Collie"),
            Dog(ObjectId(), "Milo", "Border Collie"),
            Dog(ObjectId(), "Lassie", "Rough Collie"),
        )
    )

    collection.find<Dog>(eq(Dog::breed.name, "Rough Collie")).limit(2).collect { print("${it.name} ") }
    collection.find<Dog>(eq(Dog::breed.name, "Rough Collie")).limit(2).skip(2).collect { print("${it.name} ") }

    // Trufa Duc Lassie

Update

Modifica la classe Dog per tal que un cos pugui tenir un propietari:

data class Dog(
    @BsonId
    val id: ObjectId,
    val name: String,
    val breed: String?,
    val owner: Owner?
)

data class Owner(
    val name: String,
    val emai: String?
)

Si tenim aquest conjunt de documents a la base de dades:

    collection.insertMany(
        listOf(
            Dog(ObjectId(), "Trufa", "Rough Collie", Owner(name = "David", email = "david@gmail.com")),
            Dog(ObjectId(), "Ketzu", "Shiba Inu", Owner(name = "Laura", null)),
            Dog(ObjectId(), "Marilyn", null, Owner(name = "Roser", "roser@gmail.com")),
            Dog(ObjectId(), "Duc", "Rough Collie", null),
            Dog(ObjectId(), "Milo", "Border Collie", null),
            Dog(ObjectId(), "Lassie", "Rough Collie", Owner(name = "Miquel", null)),
        )
    )

I has trobat un propietari pel "Duc", pots actualitzar el document corresponent amb updateOne.

    collection.updateOne(
        filter = eq(Dog::name.name, "Duc"),
        update = set(Dog::owner.name, Owner(name = "David", "david@gmail.com"))
    ).also { println(it) }

    collection.find<Dog>(eq(Dog::name.name,"Duc")).first().also { print(it) }

La classe Updates proporciona mètodes estàtics per a tots els operadors de consulta de MongoDB.

import com.mongodb.client.model.Updates.*

El resultat és l'esperat:

Successfully connected to MongoDB
AcknowledgedUpdateResult{matchedCount=1, modifiedCount=1, upsertedId=null}
Dog(id=67858d99749e816534cc68db, name=Duc, breed=Rough Collie, owner=Owner(name=David, email=david@gmail.com)

Ara la "Trufa" i el "Duc" són del mateix propietari.

Imagina't que el "David" ha canviar el seu email.

Per aquest casos està l'operador updateMany.

Primera crea un filtre que seleccioni tots els gossos del "David" i verifica que funciona:

    collection.find<Dog>(eq("owner.name","David")).collect{ print("${it.name} ")}

I ja pots actualitzar les dades:

    collection.updateMany(
        filter = eq("owner.name", "David"),
        update = set("owner.email", "david@xtec.dev")
    )

    collection.find<Dog>(eq("owner.name", "David")).collect { println(it) }

Delete

Per esborrar un sol document podem utilitzar findOneAndDelete en lloc de deleteOne amb l'avantatge addicional que també retorna el document suprimit com a sortida.

    collection.findOneAndDelete(eq(Dog::name.name,"Milo")).also { print(it) }

Per eliminar diversos documents, podem utilitzar deleteMany:

    collection.deleteMany(eq(Dog::breed.name, "Rough Collie"))

    collection.find<Dog>().collect { print("${it.name} ")} // Ketzu Marilyn 

TODO