MongoDB nos permite gestionar datos documentales.
Introducción
https://gitlab.com/xtec/kotlin/mongodb
Entorno de trabajo
Crea la carpeta mongodb
y entra en la carpeta:
> md mongodb
> cd mongodb
Ejecuta gradle init
con los parámetros que se muestran a continuación para generar una aplicación Kotlin con el nombre 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 fichero 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 fichero 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 conexión con nuestra base de datos puede dividirse en dos pasos:
- Primero, creamos una instancia de MongoClient con Connection URI.
- Y en segundo lugar, utilice el cliente para conectarse con la base de datos
pets
.
Ejecuta la aplicación.
Si no tienes arrancado la base de datos MongoDB al cabo de un rato tienes un error de tipo 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}}]
...
...
Arranca la base de datos local tal y como se explica en MongoDB - Fonaments y verifica que el código funciona correctamente
> mogodb
Insert
Crea una clase de datos Dog
:
data class Dog(
@BsonId
val id: ObjectId,
val name: String
)
La anotación @BsonId
representa la identidad única o _id
del documento.
A continuación inserta un perro con el método insertOne()
:
fun main() = runBlocking {
// ...
val collection = database.getCollection<Dog>("dogs")
val result = collection.insertOne(Dog(ObjectId(),"Trufa"))
println(result.insertedId)
}
Dado que la colección dogs
todavía no existe, el servidor la crea automáticamente.
Si la inserción es correcta, insertOne()
devuelve una instancia de InsertOneResult
que te permite recuperar información como el campo _id
del documento que has insertado con insertId
.
Si su operación de inserción falla, el controlador genera una excepción.
A continuación inserta varios perros en una sola operación con el método 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)
}
Para evitar tener documentos duplicados, primero eliminamos la colección.
Después de una inserción correcta, insertMany()
devuelve una instancia de InsertManyResult
.
Find
Modifica la clase de datos Dog
para incluir la raza del perro:
data class Dog(
@BsonId
val id: ObjectId,
val name: String,
val breed: String?
)
El método find()
te permite buscar documentos en una colección.
Si no incluyes ningún filtro, el método find()
devuelve un cursor que itera todos los elementos de la colección:
// ...
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) }
Puedes ver que todos los perros se han guardado en la base de datos:
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étodo find()
devuelve una instancia de FindFlow
, una clase que ofrece varios métodos para acceder, organizar y recorrer los resultados tales como first()
y firstOrNull()
.
El método firstOrNull()
devuelve el primer documento de los resultados recuperados o null
si no hay resultados.
import kotlinx.coroutines.flow.firstOrNull
// ...
val dog = collection.find<Dog>(eq(Dog::name.name,"Lassie")).firstOrNull()
println(dog?.name)
El método first()
devuelve el primer documento o lanza una excepción NoSuchElementException
si ningún documento coincide con 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
Los filtros son operaciones utilizadas para limitar los resultados de una consulta en función de condiciones especificadas.
La clase Filters
proporciona métodos estáticos para todos los operadores de consulta de MongoDB. Cada método devuelve una instancia del tipo BSON, que puede pasar a cualquier método que espere un filtro de consulta.
import com.mongodb.client.model.Filters.*
Por ejemplo, puedes buscar todos los perros de raza "Rough Collie" con el operador eq
:
collection.find<Dog>(eq(Dog::breed.name, "Rough Collie")).collect { print("${it.name} ") }
O todos los perros de raza "Rough Collie" o "BorderCollie" con el 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
Aunque este ejemplo sería mejor con el operador in
puesto que estamos filtrando la misma propiedad:
collection.find<Dog>(
`in`(Dog::breed.name, listOf("Rough Collie", "Border Collie"))
).collect { print("${it.name} ") } // Milo Trufa Duc Lassie
Paginació
Con los operadores limit
y skip
puedes añadir paginación a los resultados:
// ...
collection.find<Dog>().limit(2).collect { print("${it.name} ") }
collection.find<Dog>().limit(2).skip(2).collect { print("${it.name} ") }
// Trufa Ketzu Marilyn Duc
Pero con este enfoque, a menudo, el tiempo de respuesta de la consulta aumenta con el valor de offset
.
Para superarlo, podemos beneficiarnos creando un Index
, como se muestra a continuación:
// ...
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 clase Dog
para que un perro pueda tener un propietario:
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 tenemos este conjunto de documentos en la base de datos:
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)),
)
)
Y has encontrado un propietario para "Duc", puedes actualizar el documento correspondiente con 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 clase Updates
proporciona métodos estáticos para todos los operadores de consulta de MongoDB.
import com.mongodb.client.model.Updates.*
El resultado es el esperado:
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)
Ahora "Trufa" y "Duc" son del mismo propietario.
Imagínate que "David" debe cambiar su email.
Por estos casos está el operador updateMany
.
Primera crea un filtro que seleccione todos los perros de "David" y verifica que funciona:
collection.find<Dog>(eq("owner.name","David")).collect{ print("${it.name} ")}
Y ya puedes actualizar los datos:
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
Para borrar un solo documento puedes utilizar findOneAndDelete
en lugar de deleteOne
con la ventaja adicional de que también devuelve el documento suprimido como salida.
collection.findOneAndDelete(eq(Dog::name.name,"Milo")).also { print(it) }
Para eliminar varios documentos, puedes utilizar deleteMany
:
collection.deleteMany(eq(Dog::breed.name, "Rough Collie"))
collection.find<Dog>().collect { print("${it.name} ")} // Ketzu Marilyn
TODO
- https://phauer.com/2018/kotlin-mongodb-perfect-match/
- https://www.mongodb.com/resources/languages/kotlin
- https://dev.to/codewithmohit/getting-started-guide-for-kotlin-multiplatform-mobile-kmm-with-flexible-sync-342o
- MongoDB Kotlin Drivers
- MongoDB Kotlin Driver
- https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/fundamentals/data-formats/document-data-format-extended-json/