Spring Data MongoDB: Implementación de un repositorio a medida
Introducción
Spring Data facilita mucho el proceso de trabajo con entidades de datos y ofrece una implementación específica para MongoDB. Se pueden definir consultas simplemente creando interfaces con métodos que siguen una convención de nombres o anotándolos con @Query
y Spring automágicamente generará una implementación por nosotros. En la mayoría de ocasiones esto nos bastará para operaciones CRUD o de consultas sencillas y no será necesario definir métodos adicionales. Esto nos permitirá finalizar el desarrollo de la aplicación o de la funcionalidad de acceso a datos de forma muy rápida sin tener que escribir código repetitivo.
Sin embargo, en muchas ocasiones esto no será suficiente y la interfaz que define el repositorio necesitará disponer de métodos no estándar e implementaciones específicas que nos permitirán dar respuesta a funcionalidades menos estándar.
En esta publicación veremos como desarrollar estas implementaciones a medida para un repositorio de libros en MongoDB y cómo definir tests unitarios que probarán dicha implementación a medida.
Definiendo la Entidad/Documento
El primer paso será definir el Documento principal de MongoDB que se devolverá en las consultas. Para el caso del ejemplo de este tutorial será un documento definido dentro de la clase Book
con la información básica de la publicación de un libro.
1/* ... */
2@Document(collection = "books")
3public class Book {
4
5 @Id
6 private String id;
7 private String title;
8 private String isbn;
9 private List<String> authorNames;
10 private Date publishDate;
11 private List<String> subjects;
12 /* ... */
13}
Las partes más importantes de esta clase son la anotación @Document
que indica que esta clase representa un documento dentro de la colección "books" de MongoDB, y la anotación @Id
que indica a Spring que este campo debería de emplearse como el identificador único del documento/entidad.
Tal como se ha comentado, esta entidad contendrá información básica (título, número ISBN, nombres de autor, etc.) de un libro.
Definiendo el repositorio base
El siguiente paso es definir la interfaz base para el repositorio de libros.
1/* ... */
2public interface BookRepository extends MongoRepository<Book, String> {
3
4 List<Book> findByTitleContainingOrderByTitle(String titleContains);
5
6}
El repositorio extiende la interfaz MongoRepository
, indicando a Spring que se trata de un repositorio específico de MongoDB, heredando todos los métodos disponibles en las interfaces padre (PagingAndSortingRepository
, CrudRepository
…).
Añadiremos un método findByTitleContainingOrderByTitle
a la interfaz para mostrar como Spring auto-implementará este método sin necesidad de hacerlo en nuestra implementación a medida.
Definiendo los métodos personalizados del repositorio a medida
Para este tutorial queremos añadir la capacidad de hacer consultas dinámicas sobre la colección de libros. Con este propósito definiremos una clase DynamicQuery
que contendrá los campos opcionales para los que se quiere poder filtrar la colección.
1/* ... */
2public class DynamicQuery {
3
4 private String authorNameLike;
5 private Date publishDateBefore;
6 private Date publishDateAfter;
7 private String subject;
8 /* ... */
9}
A continuación definiremos una interfaz que consuma un objeto tipo DynamicQuery
y que devuelva una lista de Book
s. Esta interfaz se llamará BookRepositoryCustom, Es muy importante seguir la convención de nombres para que Spring pueda instanciar la implementación específica de esta nueva interfaz.
1/* ... */
2public interface BookRepositoryCustom {
3
4 List<Book> query(DynamicQuery dynamicQuery);
5
6}
Implementando los métodos del repositorio a medida
Es muy importante seguir la convención de nombres si queremos que Spring detecte nuestras implementaciones customizadas. Por defecto Spring escaneará el paquete donde se define la interfaz a medida y los paquetes por debajo de éste para encontrar una clase que extienda dicha interfaz. Si la configuración para la convención de nombres no se ha modificado, Spring por defecto buscará una clase con un sufijo "Impl" en el nombre. Por tanto, crearemos una clase BookRepositoryImpl que implemente la interfaz a medida BookRepositoryCustom.
1/* ... */
2public class BookRepositoryImpl implements BookRepositoryCustom {
3
4 private final MongoTemplate mongoTemplate;
5
6 @Autowired
7 public BookRepositoryImpl(MongoTemplate mongoTemplate) {
8 this.mongoTemplate = mongoTemplate;
9 }
10
11 @Override
12 public List<Book> query(DynamicQuery dynamicQuery) {
13 final Query query = new Query();
14 final List<Criteria> criteria = new ArrayList<>();
15 if(dynamicQuery.getAuthorNameLike() != null) {
16 criteria.add(Criteria.where("authorNames").regex(MongoRegexCreator.INSTANCE.toRegularExpression(
17 dynamicQuery.getAuthorNameLike(), Part.Type.CONTAINING
18 ), "i"));
19 }
20 if(dynamicQuery.getPublishDateBefore() != null) {
21 criteria.add(Criteria.where("publishDate").lte(dynamicQuery.getPublishDateBefore()));
22 }
23 if(dynamicQuery.getPublishDateAfter() != null) {
24 criteria.add(Criteria.where("publishDate").gte(dynamicQuery.getPublishDateAfter()));
25 }
26 if(dynamicQuery.getSubject() != null) {
27 criteria.add(Criteria.where("subjects").regex(MongoRegexCreator.INSTANCE.toRegularExpression(
28 dynamicQuery.getSubject(), Part.Type.SIMPLE_PROPERTY
29 ), "i"));
30 }
31 if(!criteria.isEmpty()) {
32 query.addCriteria(new Criteria().andOperator(criteria.toArray(new Criteria[criteria.size()])));
33 }
34 return mongoTemplate.find(query, Book.class);
35 }
36
37}
Esta clase se comportará como cualquier otro Bean de Spring por lo que podemos autowire sus dependencias. En este caso necesitaremos un MongoTemplate
para construir nuestra query dinámica. Emplearemos la inyección basada en el constructor para inyectar una instancia de MonogTemplate.
Como puede observarse, esta clase implementa el método query
y emplea el MongoTemplate
inyectado para construir una consulta basada en Criteria
.
La implementación del método comienza por creat una lista vacía de Criteria
s y comprueba si alguno de los campos proporcionados por DynamicQuery
se han definido. Para cada uno de los campos definidos añade una nueva condición Criteria
where
. A continuación se fusionan todos los Criterias en la lista empleando un andOperator
y añadiéndolos a la Query
. Por último el método ejecuta la consulta en MongoTemplate para la colección especificada en la clase Book
.
En este método también se puede observar como emplear una query con filtrado por expresiones regulares para consultar un campo (subjects) ignorando mayúsculas y minúsculas, o si un array (authorNames) contiene determinado texto.
Modificar el repositorio inicial para que extienda la interfaz a medida
El último paso será modificar el repositorio que hemos definido inicialmente en la interfaz BookRepository para que también extienda la interfaz a medida:
1/* ... */
2public interface BookRepository extends MongoRepository<Book, String>, BookRepositoryCustom {
3/* ... */
Probando el repositorio a medida
La prueba unitaria de repositorios empleando una base de datos MongoDB embebida se mostrará en otro tutorial.
Vamos a crear una clase BookRepositoryTest
que probará el método auto-implementado por Spring y nuestro método a medida query
.
1/* ... */
2@RunWith(SpringJUnit4ClassRunner.class)
3@ContextConfiguration(classes = {BookRepository.class})
4@EnableMongoRepositories()
5@Import(EmbeddedMongoConfiguration.class)
6public class BookRepositoryTest {
7/* ... */
8 @Test
9 public void findByTitleContainingOrderByTitle_existingTitle_shouldReturnList() {
10 // Given
11 // DB with default books
12 final String existingBookPartialTitle = "lean Code";
13
14 // When
15 final List<Book> books = bookRepository.findByTitleContainingOrderByTitle(existingBookPartialTitle);
16
17 // Then
18 final int expectedCount = 1;
19 Assert.assertEquals(expectedCount, books.size());
20 Assert.assertEquals(books.size(), books.stream().filter(
21 b -> b.getTitle().contains(existingBookPartialTitle)).count());
22 }
23/* ... */
24 @Test
25 public void query_combinedQuery_shouldReturnList() {
26 // Given
27 // DB with default books
28 final String authorName = "Laakmann McDow";
29 final Date dateAfter = Date.from(LocalDate.of(2011, 8, 22)
30 .atStartOfDay().atZone(ZoneId.of(GMT_ZONE_ID)).toInstant());
31 final Date dateBefore = Date.from(LocalDate.of(2011, 8, 22)
32 .atTime(LocalTime.MAX).atZone(ZoneId.of(GMT_ZONE_ID)).toInstant());
33 final String subject = "JOB HUNTING";
34 final DynamicQuery dynamicQuery = new DynamicQuery();
35 dynamicQuery.setAuthorNameLike(authorName);
36 dynamicQuery.setPublishDateAfter(dateAfter);
37 dynamicQuery.setPublishDateBefore(dateBefore);
38 dynamicQuery.setSubject(subject);
39
40 // When
41 final List<Book> books = bookRepository.query(dynamicQuery);
42
43 // Then
44 final int expectedCount = 1;
45 Assert.assertEquals(expectedCount, books.size());
46 }
47/* ... */
48}
El primer test del fragmento mostrado (findByTitleContainingOrderByTitle_existingTitle_shouldReturnList
) comprueba que el método auto-implementado findByTitleContainingOrderByTitle
funciona como se espera, devolviendo una lista de libros cuyo título contenga una parte del texto proporcionado teniendo en cuenta mayúsculas y minúsculas.
El segundo test del fragmento (query_combinedQuery_shouldReturnList
) comprueba que nuestra implementación a medida del método query
funciona para un DynamicQuery
con varios campos definidos.
Desde una perspectiva a más bajo nivel, podemos ver que el objeto representando la implementación de la interfaz BookRepository
es sencillamente un Proxy:
Dependiendo del método que invoquemos, el proxy llamará a una implementación u otra (i.e. SimpleMongoRepository
o BookRepositoryImpl
).
Conclusión
En esta publicación hemos visto como añadir métodos a medida para un repositorio MongoDB de Spring-Data y como implementarlos.
Una de las principales cuestiones a tener en cuenta, si no se va a proporcionar una configuración específica, es la de respetar las convenciones de nombres para las interfaces a medida y sus respectivas implementaciones.
El código fuente completo de este artículo puede encontrarse en GitHub.