Escriu per cercar…

MongoDB

MongoDB et permet gestionar dades documentals.

Introducció

https://gitlab.com/xtec/kotlin/mongodb

Entorn de treball

Crea la carpeta mongodb i entra a la carpeta:

ps
> 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:

ps
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:

kotlin
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:

kotlin
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 la Connection URI.
  • I en segon lloc, fem servir el client per connectar-nos amb la base de dades pets.

Executa l’aplicació.

Si no tens arrencada la base de dades MongoDB, al cap d’una estona tens un error de tipus MongoTimeoutException:

ps
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 {% link “p:/data/mongodb/basics/” %} i verifica que el codi funciona correctament

ps
> mogodb

Insert

Crea una classe de dades Dog:

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

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

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

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

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

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

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

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

Si l’operació d’inserció falla, el controlador llança una excepció.

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

kotlin
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 d’InsertManyResult.

Find

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

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

El mètode find() et permet cercar documents en una col·lecció.

Si no hi inclous cap filtre, el mètode find() retorna un cursor que itera tots els elements de la col·lecció:

kotlin
// ...

    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 desat a la base de dades:

ps
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.

kotlin
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 no coincideix amb la consulta.

kotlin
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 segons unes 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 pots passar a qualsevol mètode que esperi un filtre de consulta.

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

Per exemple, pots cercar tots els gossos de raça “Rough Collie” amb l’operador eq:

kotlin
    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:

kotlin
    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 quedaria millor amb l’operador in, ja que estem filtrant la mateixa propietat:

kotlin
 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:

kotlin
    // ...
    
    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 l’offset.

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

kotlin
    // ...

    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 perquè un gos pugui tenir un propietari:

kotlin
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:

kotlin
    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 per a “Duc”, pots actualitzar el document corresponent amb updateOne.

kotlin
    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.

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

El resultat és el que esperem:

ps
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 “Trufa” i “Duc” són del mateix propietari.

Imagina’t que “David” ha de canviar el seu email.

Per a aquests casos hi ha l’operador updateMany.

Primer crea un filtre que seleccioni tots els gossos de “David” i verifica que funciona:

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

I ja pots actualitzar les dades:

kotlin
    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 pots utilitzar findOneAndDelete en lloc de deleteOne, amb l’avantatge addicional que també retorna com a sortida el document suprimit.

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

Per eliminar diversos documents, pots utilitzar deleteMany:

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

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

TODO