Ejemplo de MongoRepository con Spring Boot

De ChuWiki


Vamos a ver un ejemplo de cómo hacer operaciones CRUD en MongoDB usando instancias de la interface Repository de Spring Boot. Tienes el proyecto completo del ejemplo en spring-boot-mongo. Las operaciones con MongoRepository están en la clase BeanWithMongoRepository.java

Los detalles de como conseguir el esqueleto del proyecto Maven con Spring Boot y MongoDB los tienes en Crear Proyecto Maven con Spring Boot y MongoDB. Sobre este esqueleto se ha desarrollado el código de este ejemplo.

Para ejecutarlo, necesitarás una instalación de MongoDB en localhost sin autentificación. No obstante, puedes tocar el fichero src/main/resources/application.properties del proyecto para conectar a la base de datos MongoDB que tengas disponible.

Creación de un POJO para insertar en MongoDB[editar]

Primero tenemos que crear las clases Java que queramos que se guarden en MongoDB. Estas clases deben ser POJO (Plain Old Java Object) normales de Java. Unicamente, uno de los campos debe estar anotado con @Id, para indicar a MongoDB que debe usar ese campo como clave primaria de la colección donde guardará el objeto Java. El siguiente código puede ser un ejemplo

import org.springframework.data.annotation.Id;

public class Person {
    @Id
    private String name;
    private Double height;

    public Person(String name, Double height) {
        this.name = name;
        this.height = height;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", height=" + height +
                '}';
    }

    // getter y setter
}

Para facilitarnos el resto del código, hemos puesto un constructor con parámetros para rellenar fácilmente los campos del objeto Java y un método toString() para mostrar su contenido fácilmente en la pantalla. También tiene los distintos métodos get() y set() para cada campo. Cualquier IDE decente nos generará automáticamente todo esto, una vez hayamos puesto los campos, si se lo pedimos.

Clave primaria para la colección en MongoDB[editar]

Hay un detalle importante que tienes que tener en cuenta. La clase Person se guardará por defecto en una colección de MongoDB de nombre person. Esta colección necesita una clave primaria. Si hacemos lo que a MongoDB le gusta, nuestra clave primaria debería ser un campo de nombre _id, de tipo org.bson.types.ObjectId y anotado con @Id. Si ponemos este tipo ObjectId, tendremos la ventaja de que MongoDB rellenará automáticamente esta valor, si no está relleno ya, cada vez que hagamos un save() en base datos. No nos tenemos que preocupar nosotros de generar valores para la clave primaria. Si la dejamos vacía, se insertará una persona nueva en la colección y se le asignará automáticamente un valor a ObjectId. Si va relleno, se entiende que nos referimos a una persona ya existente en la colección de MongoDB y se hará un update.

En nuestra clase Person, sin embargo, hemos puesto como @Id un String name. MongoDB no va a rellenar este campo automáticamente si no está relleno cuando hagamos un save(). Es responsabilidad nuestra darle un valor antes de hacer un save(). Y no va a poder haber dos personas con el mismo nombre en la colección person de MongoDB.

Creación de una interface Repository[editar]

El siguiente paso es crear una interface Java con los métodos que queramos usar para las operaciones CRUD con Person en MongoDB. Una de las formas más fáciles es hacer que nuestra interface herede de la interface CRUDRepository que nos ofrece Spring Boot. De esta forma, ya tendremos bastantes métodos CRUD habituales disponibles. Un ejemplo sería

import org.springframework.data.repository.CrudRepository;
import java.util.List;

public interface IfzPersonRepository extends CrudRepository<Person, String> {
    List<Person> findByName(String name);
    Person findFirstByHeighGreaterThan(double heigh);
}

CRUDRepository es un tipo genérico de Java (o Template). Está parametrizado y conviene darle esos parámetros al heredar de él. Los parámetros son los dos valores <Person, String> que aparecen detrás de CRUDRepository. El primero es el objeto Java que queremos persistir en base de datos. En nuestro caso, el POJO Person. El segundo es el tipo del campo que hace de clave primaria en Person. En nuestro caso, sería un String, el nombre.

Al heredar de CRUDRepository, ya tenemos automáticamente los siguientes métodos para operaciones CRUD sobre la coleción de MongoDB

Person save(Person entity);
Iterable<Person> saveAll(Iterable<Person> entities);
Optional<Person> findById(String id);
boolean existsById(String id);
Iterable<Person> findAll();
Iterable<Person> findAllById(Iterable<String> ids);
long count();
void deleteById(String id);
void delete(Person entity);
void deleteAllById(Iterable<String> ids);
void deleteAll(Iterable<Person> entities);
void deleteAll();

En nuestra interface IfzPersonRepository podemos añadir, si lo queremos, métodos que necesitemos y que no estén ya en esta lista proporcionada por CRUDRepository. En nuestro ejemplo, hemos añadido dos

List<Person> findByName(String name);
Person findFirstByHeighGreaterThan(double heigh);

Existe una nomenclatura formal para los nombres de los métodos. No ponemos aquí una referncia completa, pero si algunos ejemplos para que te hagas idea del alcance de todo esto.

  • findByXXX() donde XXX sea cualquiera de los campos que tenemos en nuestra clase Person. Como la consulta puede devolver varios resultados, debemos devolver una Iterable<Person>.
  • findByXXXGreaterThan() donde XXX sea cualquiera de los campos que tenemos en nuestra clase Person y GreaterThan si queremos buscar aquellas Person cuyo valor del campo sea mayor o igual que el valor que pasemos como parámetro. Por ejemplo, findByHeightGreaterThan(1.70) nos devolvería todas las personas de altura mayor o igual a 1.70. Como puede haber cualquier número de resultados, debemos devolver un Iterable<Person>.
  • findByXXXAndYYY() donde XXX es cualquiera de los campos de Person e YYY cualquier otro. Nos devolverá las personas cuyos dos campos coincidan con los parámetros que pasemos. Al igual que antes, debemos devolver un Iterable<Person>
  • findFirstByXXXX() para obtener solo un resultado, el primero que enecuentre. Como puede haber un solo resultado o ninguno en caso de que no se cumpla la condición, debemos devolver un Optional<Person>.

La ventaja de hacerlo con esta nomemclatura es que Spring Boot la entenderá y no necesitaremos rellenar el código de los métodos. Recuerda que estamos creando una interface Java, ahí no se implementan los métodos. Y tampoco tendremos que hacer una clase que los implemente. Spring Boot lo hará por nosotros al vuelo siempre que sigamos la nomenclatura correctamente y Spring Boot sea capaz de interpretarla.

Los valores a devolver correctos serían Iterable<Person> o Optional<Person> según espermos una lista de resultados o un solo resultado. Se usan estos tipos porque son los más genéricos. En cualquier caso, podemos devolver lo que queramos que cuadre, Spring Boot hará lo mejor posible para cumplirlo. Por ejemplo, podemos devolver List<Person> o Person. Si Spring Boot no puede cumplirlo, tendremos una excepción al realizar la consulta. En nuestro ejemplo, en los métodos que hemos definido, devolvemos estos tipos aunque no son los correctos, simplemente para verificar que es posible.

La pega evidente de este mecanismo es que tenemos que prever todos los posibles métodos que vamos a necesitar en nuestra aplicación. No sería útil en aplicaciones donde el usuario define sus consultas de forma dinámica o donde nuestra aplicación debe hacerlo en función de otras entradas aparte del usuario.

Utilización de la interface Repository de Spring Boot[editar]

Puesto que Spring Boot creará al vuelo e instanciará una clase que implemente la interface IfzPersonRepository, necesitamos obtener esta instancia en nuestro código, para poder usarla. La forma de hacer esto es inyectarla en alguno de nuestros Bean de Spring. Una forma es la siguiente

@Component
public class BeanWithMongoRepository {

    /** personRepository lo inyecta Spring Boot */
    @Autowired
    IfzPersonRepository personRepository;

    ...
}

Basta con poner un campo con la anotación @Autowired y de tipo IfzPersonRepository. Spring Boot se encargará de inyectar ahí la instancia de la clase que ha generado que implementa la interaface IfzPersonRepository.

Un detalle importante, para que Spring Boot haga esto, es neceario que sepa de la existencia de la clase donde debe inyectarla, en nuestro ejemplo, la clase BeanWithMongoRepository que estmos escribiendo. Por ello, es Spring Boot el que tiene que instanciar esta clase y eso se consigue con la anotación @Component en la clase. Spring Boot instanciará nuestra clase BeanWithMongoRepository, instanciará una implementación de IfzPersonRepository e inyectará esta implementación en la instancia de BeanWithMongoRepository.

Cuando todo esto esté hecho, Spring Boot llamará al método anotado con @PostConstruct, si lo hay, de la clase BeanWithMongoRepository. Con esta llamada indica a BeanWithMongoRepository que ya tiene inyectada la dependencia.

Seguimos ahora con los ejemplos CRUD sobre MongoDB usando la instancia de IfzPersonRepository. Como queremos asegurarnos de tenerla inyectada, creamos un método con la anotación @PostConstruct y ahí haremos todo el código de ejemplo

@Component
public class BeanWithMongoRepository {

    /** personRepository lo inyecta Spring Boot */
    @Autowired
    IfzPersonRepository personRepository;

    /** Hace operaciones CRUD varias con MongoDB */
    @PostConstruct
    public void doCRUDOperations(){
       // Aqui codigo que use IfzPersonRepository 
    }

    ...
}

instertar objetos Java con save() y saveAll()[editar]

Primero instanciamos un par de objetos Java Person, los guardamos en una lista y llamamos al método saveAll().

// Insertar dos POJO
// Como no indicamos colección, lo inserta en una colección con el mismo nombre que el POJO
List<Person> persons = new ArrayList<>();
persons.add(new Person("Pedro", 1.70));
persons.add(new Person("Monica", 1.90));
Iterable<Person> people = personRepository.saveAll(persons);

Si sólo quisieramos insetar uno, llamaríamos a save() pasando como parámetro un único objeto Java Person.

Estos método sirven tanto para insertar como para actualizar un objeto ya salvado. Lo hará en función del campo que hemos marcado como @Id y si existe ya o no en la colección de MongoDB. El resultado del método es un Iterable en caso de saveAll() o un objeto Java Person en caso de save() con los valores actualizados después de la inserción.

Consultar objetos Java con findAll()[editar]

// Consulta de todos los POJO insertados
Iterable<Person> people = personRepository.findAll();
people.forEach(person -> System.out.println("Person Found : "+ person));

findAll() nos devuelve todos los objetos java en la colección en un Iterable. Nos bastaría recorrelo para ir obteniendo los objetos Java de uno en uno.

Buscar objetos Java en la colección con los métodos find()[editar]

Para buscar objetos Java Person en la colección, podemos usar los distintos métodos find() que nos proporciona CRUDRepository y los que hayamos definido nosotros en nuestra interface IfzPersonRepository. Veamos algunos ejemplos

// Consulta con query de POJO insertados, por nombre.
persons = personRepository.findByName("Pedro");
System.out.println(Arrays.toString(persons.toArray()));

// Dame la primera persona que encuentres de altura mayor o igual a 1.80
Person tallPerson = personRepository.findFirstByHeightGreaterThan(1.80);

// find by Id. En nuestro ejemplo, es equivalente a buscar por nombre, ya que el nombre es el Id
Optional<Person> monica = personRepository.findById("Monica");

Update del objeto Java en MongoDB con save()[editar]

Como hemos comentado, save() y saveAll() hacen tanto insert como update, dependiendo de si el objeto existe ya o no en la colección. En el caso de que hubieramos puesto como _id un OjectId, el insert se haría además si este campo fuera null, dándole automáticamente un nuevo valor ObjectId al campo _id. Veamos el siguiente ejemplo

// Update de un POJO en la colección MongoDB
Person tallPerson = personRepository.findFirstByHeightGreaterThan(1.80);
tallPerson.setHeight(2.0);
personRepository.save(tallPerson);

Hemos hecho que la primera persona que MongoDB encuentre en la colección que sea más alta de 1.70 crezca de golpe 30 cm. Cambiamos su altura a 2.0 y llamamos a save().

Borrar objetos de la colección MongoDB con deleteById() y deleteAll()[editar]

Nada especial que mecionar, deleteById() borra por el id que le pasemos (el nombre) y deleteAll() borra todos los objetos Java de la colección.

// Borrado de un POJO de la colección MongoDB
personRepository.deleteById("Pedro");

// Borrado de todos los objetos Java de la colección MongoDB
personRepository.deleteAll();

Por supuesto, podríamos haber definido en nuestra interface IfzPersonRepository, siguiendo la nomenclatura que indicamos, métodos delete() más selectivos.