Spring Data: Haciendo fácil la persistencia

Persistencia en Java

Históricamente en Java se han implementado  diversos frameworks y librerías para interactuar con la capa de pesistencia, tipicamentente con una base de datos relacional y un poco más reciente con bases de datos no relacionales. Algunos de estos ejemplos son:

  • JDBC
  • EJB 2.0
  • MyBatis
  • JDBC Template
  • JDO
  • Hibernate
  • JPA
  • Spring Data

Spring  Data

Si bien con el paso de los años diversos frameworks han ido madurando, algunos de ellos en su momento fueron muy exitosos hoy por hoy Spring Data es uno de los frameworks que más ventajas nos ofrecen sobre otras opciones, a continuación se destacan algunas de sus características:

  • Es un proyecto de Spring Framework que contiene subproyectos por cada base de datos distinta por ejemplo para MongoDB, MySQL, etc.
  • Nos ofrece una capa de abstracción a través de componentes de software llamados “repositorios» la cual es una solución al patrón de diseño ‘Generic DAO’ para implementar el CRUD.
  • Spring Data es una capa encima de JPA, así que ambas tecnologías se complementan como veremos más adelante.
  • No es necesario escribir clases de implementación, el framework escribe esas clases por nosotros.
  • Trabaja con fuentes de datos relacionales y no relacionales.

Ejercicio de demostración

Para este ejercicio vamos a hacer un servicio REST que obtenga una lista de empleados, dicha lista la vamos a poder filtrar por diversos criterios además de que también vamos a regresar la información paginada, todo con ayuda de Spring Data.

Los datos los almacenaremos en una base de datos de tipo NoSQL en este caso MongoDB, además para hacernos la vida mas sencilla vamos a utilizar Spring Boot que nos ayudará a hacer las configuraciones base del proyecto o ‘boilerplate’ como se le conoce en inglés.

El ejemplo consiste en los siguientes pasos:

  • Configurar y correr proyecto base (Spring Boot).
  • Configurar MongoDB.
  • Implementar las operaciones CRUD de una entidad sinple usando ‘CrudRepository’.
  • Utilizar ‘Automatic Custom Queries’ para filtrar resultados de búsqueda.
  • Utilizar ‘QueryByExample’ para filtrar por varios campos en una búsqueda.
  • Paginar los resultados de una búsqueda.

Prerequisitos

Configurar MongoDB

1 Levantar Mongo

2 Crear la base de datos

3 Crear una colección

En este caso yo le llamé a mi colección ‘enployees’.

Estructura del proyecto

Pueden descargar de mi repositorio de github el código base del proyecto.

La estructura del proyecto esta separada en diversos paquetes para tener mejor organización de los componentes.

Configurar Mongo desde la App

1 Modificar el pom.xml

Necesitamos agregar la dependencia de mongodb para Spring Data:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>

2 Conexión a la Base de Datos

Vamos a modificar el archivo application.properties para hacer las conexiones a la base de datos.

#mongodb
spring.data.mongodb.host=localhostspring.data.mongodb.port=27017
spring.data.mongodb.database=tvshows
#logging
logging.level.org.springframework.data=debuglogging.level.=info

Obtener los datos de la colección desde Mongo

1 Modificar POJO

Necesitamos ahora realizar las anotaciones de un objeto Java que nos represente la colección a dónde se almacenarán los datos en MongoDB, en este caso información básica de un empleado y adicionalmente una lista de habilidades por cada empleado:

package com.example.microservicedemo.model;

import java.util.List;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;


@Document(collection = «employees»)
public class Employee {

@Id
private String id;

@Field()
private String name;

@Field()
private Integer age;

@Field()
private List<Skill> skills;

2. Crear repository de Spring Data

Crear en el paquete com\example\microservicedemo\dao\repository la clase ‘EmployeesRepository’.

Aquí es donde realmente ponemos a nuestro beneficio la magia de Spring Data ya que solamente tenemos que crear una interfaz que herede de la clase ‘CrudRepository’ de Spring Data’ en este caso el framework implementará por nosotros la funcionalidad para hacer CRUD de nuestra colección de empleados. Con el soporte de Spring Data, la tarea repetitiva de crear las implementaciones concretas del patrón DAO se simplifica porque solo vamos a necesitar definir la interfaz y no más.

La interfaz CrudRepository nos va a proporcionar métodos genéricos cómo save, findAll, findById, delete, count, etc.

package com.example.microservicedemo.dao.repository;

import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.microservicedemo.model.Employee;

@Repository
public interface EmployeesRepository extends CrudRepository<Employee, String>{ }

3. Implementar lógica en el servicio

Dentro de la lógica del servicio se inyectará la dependencia al repositorio de Spring Data y utilizar algunos de sus métodos como findOne, findAll, findById, y save.

package com.example.microservicedemo.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import com.example.microservicedemo.dao.repository.EmployeesRepository;
import com.example.microservicedemo.model.Employee;
import com.example.microservicedemo.service.IEmployeesService;

public class EmployeesServiceImpl implements IEmployeesService<Employee, String> {

@Autowired
EmployeesRepository employeeRepository;

@Override
public Employee findById(String id) {
return employeeRepository.findOne(id);
}

@Override
public Iterable<Employee> findAll(Employee employeeExample){
Iterable<Employee> employees; employees = employeeRepository.findAll();
return employees;
}

public Employee save(Employee employee) {
return employeeRepository.save(employee);
}

@Override
public void delete(String id) {
//TODO
}
}

4. Insertar algunos registros desde Postman:

A continuación vamos a insertar algunos registros en la colección de empleados:  

POST localhost:8080/api/employees/

Mensaje POST

{    
«name»: «Snoopy»,    
«age»: 33,    
«skills»: [       
 {  «name»: «Java»,            «experience»: 3        },
  {  «name»: «RAML»,            «experience»: 1 }
]
}

5. Levantar la app e invocar a endPoints

Vamos a realizar una prueba para invocar a dos endPoints REST en este caso para obtener una lista de empleados y la segunda para obtener un empleado en concreto:

 GET  localhost:8080/api/employees/

 GET  localhost:8080/api/employees/<id>

Filtrar los datos mediante un campo de entrada

Ahora lo que haremos es modificar nuestra clase de repositorio para agregar funcionalidad de filtrado. Los repositorios de Spring Data soportan agregar métodos personalizados que por medio del nombre del método se le diga al framework que se quiere hacer un query por algún criterio de búsqueda, en este caso queremos filtrar a los empleados por su edad así que declaramos un método findEmployeesByAge siguiendo una convención de nomenclatura podremos realizar el siguiente tipo de consultas:

1.Modificar el repositorio para usar un método de consulta

package com.example.microservicedemo.dao.repository;

import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.microservicedemo.model.Employee;

@Repository
public interface EmployeesRepository extends CrudRepository<Employee, String>{
List<Employee> findEmployeeByAge(int age);
}

2. Modificar el servicio para filtrar

En este caso vamos a a hacer uso del parámetro de entrada ‘employeeExample’ el cual utilizaremos para saber si el consumidor del endPoint desea filtrar por edad y así llamar a nuestro método personalizado ‘findEmployeeByAge’. En caso contrario de que no se filtre por edad entonces se obtendrán todos los empleados de la colección.

package com.example.microservicedemo.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import com.example.microservicedemo.dao.repository.EmployeesRepository;
import com.example.microservicedemo.model.Employee;
import com.example.microservicedemo.service.IEmployeesService;

public class EmployeesServiceImpl implements IEmployeesService<Employee, String> {

@Autowired
EmployeesRepository employeeRepository;

@Override
public Employee findById(String id) {
return employeeRepository.findOne(id);
}

@Override
public Iterable<Employee> findAll(Employee employeeExample){
Iterable<Employee> employees;

if(employeeExample != null && employeeExample.getAge() != null  &&  employeeExample.getAge() > 0){
employees = employeeRepository.findEmployeeByAge(employeeExample.getAge());
} else {
employees = employeeRepository.findAll();
}
return employees;

}

public Employee save(Employee employee) {
return employeeRepository.save(employee);
}

@Override
public void delete(String id) { //TODO }

}

3. Modificar el controller para recibir el parámetro

Ahora vamos a realizar la modificación al controller ‘EmployeesController’ ya que vamos a agregar el filtro por edad, para esto nos vamos a ayudar de la clase ‘GenericMapper’ la cual se encargará de crear una instancia de la clase ‘Employee’ que tiene seteado un valor para el atributo ‘age’ de tal manera que el método ‘findAll’ del servicio identifique el filtro.

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response findAll(@QueryParam(«age») final int age) {
int statusCode = 200;
Response response = null;
List<KeyValue> parameters = new ArrayList<KeyValue>();
if(age > 0) parameters.add(new KeyValue(«age», age));
Employee dto = (Employee) GenericMapper.mapping(new Employee(), parameters);
Iterable<Employee> employees = employeesService.findAll(dto);

response = Response.status(statusCode).entity(employees).build();
return response;
}

4. Probar

Probaremos el caso en el que el consumidor del endPoint desea filtrar por edad del empleado:

1 Levantar la app

2 invocar GET  localhost:8080/api/employees/?age=22

Filtrar los datos mediante más de un campo de entrada

Es común el desear filtrar por más de uno de los atributos de la entidad ‘Employee’ en este caso vamos a hacer la modificación para filtrar por edad y nombre aplicado las combinaciones posibles:

consulta sin filtro, consulta por edad, consulta por nombre, consulta por nombre y edad.

1 Modificar el controller para filtrar por más de un campo de entrada

Realizaremos la modificación al controller de la siguiente manera para poder soportar criterios de búsqueda múltiples:

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response findAll(@QueryParam(«age») final int age, @QueryParam(«name») final String name) {
int statusCode = 200;
Response response = null;
List parameters = new ArrayList();
if(age > 0) parameters.add(new KeyValue(«age», age));
if(name != null) parameters.add(new KeyValue(«name», name));
Employee dto = (Employee) GenericMapper.mapping(new Employee(), parameters);
Iterable employees = employeesService.findAll(dto);
response = Response.status(statusCode).entity(employees).build();
return response;
}

2. Modificar el servicio

Del lado del servicio ‘EmployeesServiceImpl’ vamos a realizar una modificación, usaremos la clase ‘Example’ de Spring Data la cual nos ayudará a poder hacer un query que contenga los criterios deseados:

@Override
public Iterable findAll(Employee employeeExample){
System.out.println(employeeExample);
Iterable employees;
employees = employeeRepository.findAll(Example.of(employeeExample));
return employees;
}

Con esos pequeños cambios ahora el endPoint va a soportar el obtener datos filtrados por dos campos.

3 Cambiar a MongoRepository

Para poder hacer cosas más avanzadas Spring Data nos da la posibilidad de extender su funcionamiento en este caso vamos a hacer uso de otra clase que se llama ‘MongoRepository’ la cual nos va a permitir hacer consultas mas complejas, en este caso poder filtrar de manera dinámica por diversos campos sin necesidad de estar programando queries por cada tipo de combinación posible:

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import com.example.microservicedemo.model.Employee;
@Repository
public interface EmployeesRepository extends MongoRepository{
List findEmployeeByAge(int age);
}

4 Probar

1 Levantar la app

2 invocar GET  localhost:8080/api/employees/

3 invocar GET  localhost:8080/api/employees/?age=22

4 invocar GET  localhost:8080/api/employees/?name=Jorge

5 invocar GET  localhost:8080/api/employees/?age=22 &name=Jorge

Regresar los datos paginados

Finalmente como parte de nuestro ejercicio vamos a regresar los datos de manera paginada.

1. Modificar la interfaz del servicio

Agregaremos un parámetro de entrada con el numero de pagina deseado:

package com.example.microservicedemo.service;

import com.example.microservicedemo.model.Employee;


public interface IEmployeesService<T, ID> {

public T findById(ID id);

public Iterable<T> findAll(T exampleEmployee, int page);

public T save(Employee employee);

public void delete(ID id);

}

2. Modificar la implementación del servicio para que pagine de 5 en 5 registros

package com.example.microservicedemo.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Page;


import com.example.microservicedemo.dao.repository.EmployeesRepository;
import com.example.microservicedemo.model.Employee;
import com.example.microservicedemo.service.IEmployeesService;

public class EmployeesServiceImpl implements IEmployeesService<Employee, String> {

@Autowired
EmployeesRepository employeeRepository;

@Override
public Employee findById(String id) {
return employeeRepository.findOne(id);
}

@Override
public Iterable<Employee> findAll(Employee exampleEmployee, int page){
Iterable<Employee> employees;

if(page > 0){
Pageable pageable = new PageRequest(page-1, 5);
employees = employeeRepository.findAll(Example.of(exampleEmployee), pageable);
} else {
employees = employeeRepository.findAll(Example.of(exampleEmployee));
}


return employees;
}

public Employee save(Employee employee) {
return employeeRepository.save(employee);
}

@Override
public void delete(String id) {
//TODO
}

}

3. Modificar el controller para implementar paginación

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response findAll(@QueryParam(«age») final int age, @QueryParam(«name») final String name, @QueryParam(«page») final int page) {
int statusCode = 200;
Response response = null;

List<KeyValue> parameters = new ArrayList<KeyValue>();
if(age > 0 ) parameters.add(new KeyValue(«age», age));
parameters.add(new KeyValue(«name», name));

Employee dto = (Employee) GenericMapper.mapping(new Employee(), parameters);

Iterable<Employee> employees = null;
employees = employeesService.findAll(dto, page);
response = Response.status(statusCode).entity(employees).build();

return response;
}

4. Levantar la app e invocar diversos escenarios

En caso de que no se proporcione el ‘query param’ ‘page’ eso significaría que se desea obtener la primer página.

GET  localhost:8080/api/employees/

GET  localhost:8080/api/employees/?page=1

GET  localhost:8080/api/employees/?page=2

GET  localhost:8080/api/employees/?age=22&page=1

Conclusiones

Hemos visto las ventajas de utilizar Spring Data, lo que podemos destacar de este ejercicio es que con poco esfuerzo pudimos implementar servicios REST con interacción a base de datos.

El framework de Spring Data nos ayudó a implementar funcionalidad por nosotros ya que por nuestra parte solo declaramos interfaces para utilizar los repositorios.

También aprendimos a utilizar algunas clases que Spring Data nos ofrece para hacer filtrados , queries dinámicos y aprendimos a implementar funcionalidad de paginado.

Sin duda Spring Data nos ayuda a interactuar con bases de datos de una manera sencilla sin demasiado esfuerzo de nuestra parte para implementar patrón DAO.