Primeros pasos con KMongo

2
713

En este tutorial vamos a dar nuestros primeros pasos con la librería de KMongo, librería que nos sirve para realizar consultas a una base de datos MongoDB desde Kotlin.

Índice de contenidos

  1. Introducción
  2. Instalando MongoDB
  3. Preparando el proyecto
  4. Insertar registros
  5. Consultas
  6. Conclusiones

Introducción

KMongo es una librería que mediante el uso de las extensiones de Kotlin, extiende el API de Java para MongoDB, aportándonos facilidad en la construcción de las consultas, además de proporcionarnos consultas tipadas. En este tutorial vamos a ver algunos ejemplos de consultas

Instalando MongoDB

Lo primero es tener un servidor mongo al que poder acceder, para ello vamos a utilizar docker-compose para tener una instancia en local de mongo.

version: '3.9'

services:

  mongo:
    container_name: mongodb_kmongo
    image: mongo:5.0
    ports:
      - 27017:27017
    volumes:
      - mongodb:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: kuser
      MONGO_INITDB_ROOT_PASSWORD: password
      MONGO_INITDB_DATABASE: kmongo_tutorial

Preparando el proyecto

En primer lugar vamos a agregar la dependencia de KMongo:

<dependency>
    <groupId>org.litote.kmongo</groupId>
    <artifactId>kmongo</artifactId>
    <version>4.4.0</version>
</dependency>

Ahora vamos a generar una pequeña clase Address con el objetivo de disponer de algún atributo compuesto:

data class Address constructor(
    val country: String,
    val city: String
)

Creamos el data class User definiendo el modelo de datos:

data class User constructor(
    @BsonId
    val id: String = newId<User>().toString(),
    val name: String,
    val lastName: String,
    val age: Int,
    val address: Address,
    val active: Boolean = false
)

Para definir cuál es la clave del documento, tenemos dos opciones, o aplicar el decorador @BsonId encima del atributo de clase, o bien nombrar el atributo como _id

A la hora de definir el tipo del id, puedes hacerlo de diferentes maneras. Incluso funcionará si defines el id: String? como nullable y lo insertas con un nulo, automáticamente te generará el identificador.
También puedes definir el id como objectId de la siguiente forma:

val id: Id<User> = newId()

Insertar registros

Vamos a persistir algunos datos haciendo uso de la api de KMongo.
En primer lugar haremos un método privado que nos generará la conexión a nuestro MongoDB y nos devolverá la MongoCollection para poder realizar las consultas.

private fun getCollection(): MongoCollection<User> {
    val mongoDatabase =
        KMongo.createClient("mongodb://kuser:password@localhost:27017/kmongo_tutorial?authSource=admin&ssl=false")
            .getDatabase("kmongo_tutorial")
    return mongoDatabase.getCollection()
}

Generamos un test para guardar unos cuantos registros que usaremos para las consultas:

@Test
fun save_users() {
    val users = listOf(
        User(name = "Florence", lastName = "Lake", age = 20, address = Address("Spain", "Madrid"), active = true),
        User(name = "Isadora", lastName = "Boone", age = 35, address = Address("Spain", "Barcelona"), active = true),
        User(name = "Dexter", lastName = "Webber", age = 47, address = Address("France", "Paris"), active = false),
        User(name = "Abid", lastName = "Dyer", age = 55, address = Address("France", "Nice"), active = true)
    )
    users.forEach {
        getCollection().save(it)
    }
}

Si entramos a nuestra base de datos, podremos confirmar como se han generado los registros y se ha asignado su correspondiente id:

{"_id":"61cdcf7bbd9f49474a0d9a04","name":"Florence","lastName":"Lake","age":20,"address":{"country":"Spain","city":"Madrid"},"active":true}
{"_id":"61cdcf7bbd9f49474a0d9a05","name":"Isadora","lastName":"Boone","age":35,"address":{"country":"Spain","city":"Barcelona"},"active":true}
{"_id":"61cdcf7bbd9f49474a0d9a06","name":"Dexter","lastName":"Webber","age":47,"address":{"country":"France","city":"Paris"},"active":false}
{"_id":"61cdcf7bbd9f49474a0d9a07","name":"Abid","lastName":"Dyer","age":55,"address":{"country":"France","city":"Nice"},"active":true}

Consultas

La api de mongo nos proporciona muchas formas de realizar consultas y operaciones, podemos buscar y borrar con findOneAndDelete(), buscar por Id con findOneById(), tener garantizado un solo resultado aunque haya varios match para una condición con findOne(), y muchas más opciones.

Find

En el primer ejemplo vamos a usar find para hacer una consulta muy básica filtrando solamente por los registros que tengamos con la propiedad active a true:

@Test
fun find_all_active_users() {
    val activeUsers = getCollection().find(User::active eq true)
    assertEquals(3, activeUsers.count())
    activeUsers.forEach { assertTrue(it.active) }
}

El método find() espera una expresión tipo Bson, esto nos permite montar cualquier tipo de consulta utilizando los diferentes operadores de los que disponemos. Además, nos aporta el tipado en la consulta, ya que al definir que queremos buscar por User::active, detecta que es un boolean y nos obliga a compararlo con un valor del mismo tipo.

Consultas en propiedades compuestas y operador and

Ahora vamos a realizar un ejemplo de una consulta con referencia a la propiedad country dentro del tipo Address, para ello KMongo introduce el operador / que nos permite hacer referencia a elementos anidados:

@Test
fun find_france_users_greater_than_50() {
    val frenchActiveUsers = getCollection().find(
        and(User::address / Address::country eq "France", User::age gt 50)
    )
    assertEquals(1, frenchActiveUsers.count())
    frenchActiveUsers.forEach { assertEquals("France", it.address.country) }
}

Para poder acceder a elementos anidados dentro de un documento, tan solo tenemos que ir accediendo a cada propiedad haciendo uso del operador /, además en esta consulta hemos filtrado también por los usuarios que son mayores de 50 con el operador gt (greater than) haciendo uso del operador and que nos permite introducir un vararg de expresiones Bson (también tenemos otros como or, nor, etc).

Aggregate

Vamos a ver como funciona el uso de agregadas en KMongo con una consulta muy sencilla donde ordenaremos por edad y aplicaremos un pequeño filtro:

@Test
fun aggregate_example() {
    val usersOrderByAge = getCollection().aggregate<User>(
        match(
            and(
                User::active eq true,
                User::age gt 30
            )
        ),
        sort(
            descending(
                User::age
            )
        )
    )
    assertEquals(2, usersOrderByAge.count())
    assertEquals(55, usersOrderByAge.elementAt(0).age)
    assertEquals(35, usersOrderByAge.elementAt(1).age)
}

Hemos utilizado dos bloques, el bloque match que es donde aplicamos los filtros que nos interesen, y el bloque sort, donde definimos cómo queremos que ordene los resultados.

Proyección + Group

Lo que queremos ahora es contar cuantos usuarios tenemos por cada país, para ello necesitamos hacer uso de dos operaciones distintas, por un lado necesitaremos agrupar por país, y por otro realizar una proyección para volcar la información a un modelo.

Primero vamos a generar el modelo, que tendrá un atributo con el nombre del país, y el total de usuarios que tenemos en nuestro sistema para ese país:

data class CountryCount constructor(
    val country: String,
    val count: Int
)

Una vez tenemos ya generado el modelo donde guardar la información, realizamos la consulta.

@Test
fun count_by_country() {
    val countryCounts = getCollection().aggregate<CountryCount>(
        match( User::active eq true ),
        group(
            User::address / Address::country,
            Accumulators.sum("count", 1)
        ),
        project(
            CountryCount::country from "\$_id",
            CountryCount::count from "\$count"
        )
    ).toList()
    assertEquals(2, countryCounts.count())
    assertEquals(2, countryCounts.first { it.country == "Spain" }.count)
    assertEquals(1, countryCounts.first { it.country == "France" }.count)
}

Filtramos por usuarios activos, después aplicamos la operación group que nos permite agrupar por el campo que nos interese, en este caso agruparemos por country y crearemos una nueva variable «count» con el acumulador sum para el conteo.
Ahora tendríamos en nuestra pipeline la información que queremos tener almacenada, en una estructura tipo _id: (countyName), count: (total de coincidencias) por lo que solo nos queda proyectar esta información a la estructura de datos que nos interese con el operador project, en nuestro caso asociamos el _id a la propiedad country y el count a la propiedad count del modelo CountryCount.

Conclusiones

Como hemos visto, KMongo nos permite realizar de forma sencilla nuestras consultas,  además nos permite tipar cada una de ellas, por lo que eso nos da un punto extra de robustez.
Aunque este tutorial solo es un primer acercamiento, disponemos de multiples opciones y métodos con los que poder dar solución a diferentes escenarios, podéis encontrar más información en la web de KMongo.

 

2 COMENTARIOS

  1. Hola Miguel, aplaudo tu tiempo dedicado y la calidad que aplicaste en tu artículo. ¿Crees que exista la posibilidad de explicar cómo se maneja dentro de KMongo la función $facet con el objetivo de lograr un conteo global de items y a la vez ofrecer un resultado de paginado con $sort, $skip y $limit….?

    Saudos!

    • Hola Felipe, ¡muchas gracias por tus palabras y por tu pregunta!.
      Siento la demora en mi respuesta, y en cuanto a tu pregunta me parece una estupenda idea la que comentas, si te parece bien puedo preparar un artículo donde el objetivo sea realizar un paginado con KMongo, y en cuanto lo tenga publicado te comparto por este mismo hilo el link del artículo.

      ¡Muchas gracias por el aporte!

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad