Spring Data MongoDB: Custom repository implementation
Introduction
Spring Data makes really quick and easy the process of working with data entities, offering a specific implementation for MongoDB. You can merely define queries by creating interfaces with methods following a naming convention or annotating them with @Query
and Spring will automagically generate an implementation for you. Most of the times this is enough for simple CRUD and query operations and there is no need to define additional methods. This will allow you to get up and running really quickly avoiding to type boilerplate code.
However, there are many times where this is not enough and the repository interface will need additional non-standard methods and a custom implementation for them.
In this post we’ll see how to define such custom implementation for a book repository in MongoDB and how to write unit tests for this custom implementation.
Defining the Entity/Document
The first step will be to define the main MongoDB Document that will be retrieved in the queries. For the sake of this tutorial we are going to use a simple Book
document with basic information about a book publication.
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}
The main parts of this class are the @Document
annotation that indicates that this class represents a MongoDB Document of the books collection, and the @Id
annotation which tells Spring that this field should be used as the unique identification for the document.
As already stated, this entity will hold basic information about a book publication, such as title, ISBN, author names, etc.
Defining the base Repository
Next step is defining the base interface for the Book repository.
1/* ... */
2public interface BookRepository extends MongoRepository<Book, String> {
3
4 List<Book> findByTitleContainingOrderByTitle(String titleContains);
5
6}
The repository extends MongoRepository
interface, indicating Spring that this is a MongoDB specific repository and inheriting all of the methods available in the parent interfaces (PagingAndSortingRepository
, CrudRepository
…)
We are going to add a simple interface method findByTitleContainingOrderByTitle
to highlight how Spring will auto-implement this method.
Defining the custom Repository methods
For this tutorial we want to add the capability to perform dynamic queries on the Book collection. For this purpose we are going to define a DynamicQuery
class that will have optional fields for which we will be able to filter the collection.
1/* ... */
2public class DynamicQuery {
3
4 private String authorNameLike;
5 private Date publishDateBefore;
6 private Date publishDateAfter;
7 private String subject;
8 /* ... */
9}
Next we define an interface with a method that will consume a DynamicQuery
and will return a list of Book
s. This interface will be named BookRepositoryCustom, It’s really important to follow the naming convention so that Spring will be able to instantiate the specific implementation for this new interface.
1/* ... */
2public interface BookRepositoryCustom {
3
4 List<Book> query(DynamicQuery dynamicQuery);
5
6}
Implementing the custom Repository methods
As already stated, it’s important to follow the naming convention if we want Spring to detect our customized implementations. By default Spring will scan for a class below the package the repository is declared in. If the naming convention hasn’t been changed in the configuration, it will search for a class with an ‘Impl’ postfix in the name. We will create a BookRepositoryImpl implementing the BookRepositoryCustom interface.
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}
This class will act as a regular Bean so we can autowire dependencies. In this case we are going to need a MongoTemplate
to build our dynamic query, so we are using constructor based injection to autowire an instance.
As you can see, the class implements the query
method and uses the autowired MongoTemplate
to build a Criteria
based query.
The method starts by crating an empty list of Criteria
s and checks if any of the fields inside the provided DynamicQuery
is defined. For each of the defined fields it adds a new Criteria
where
clause. Next, the method merges all the Criterias in the list using andOperator
and adds them to the Query
. Finally the method runs the query in MongoTemplate for the collection specified in the Book
class.
You can also see how to use regex filtering to query a field (subjects) ignoring case or if an array (authorNames) contains a given string.
Modify the initial repository to extend the custom interface
Finally we have to modify our initial BookRepository
to extend the custom interface:
1/* ... */
2public interface BookRepository extends MongoRepository<Book, String>, BookRepositoryCustom {
3/* ... */
Testing the custom repository
Testing repositories with an embedded MongoDB will be covered in another tutorial.
We are going to create a BookRepositoryTest
class which will test the interface method auto-implemented by Spring and our custom query
method.
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}
The first method in the snippet (findByTitleContainingOrderByTitle_existingTitle_shouldReturnList
) checks that the auto implemented findByTitleContainingOrderByTitle
works as expected by providing a partial title with case matching.
The second method in the snippet (query_combinedQuery_shouldReturnList
) checks that our custom query
implementation works for a DynamicQuery
with several fields.
From a lower level perspective we can see that the injected object for our declared BookRepository
interface is just a Proxy:
Depending on the method we call, the proxy will call one implementation or the other (i.e. SimpleMongoRepository
or BookRepositoryImpl
).
Conclusion
In this post we’ve seen how to add custom methods to a Spring-Data repository for MongoDB and how to implement them.
One of the key things to remember if you are not going to provide customized configuration is to follow the naming conventions for your custom interfaces and implementations.
You can find the full source code for this article at GitHub.
Comments in "Spring Data MongoDB: Custom repository implementation"
When I try this I am getting property not found exception on method query.
So basically spring is trying to find property "query" on my bean T
Hi,
Could you please provide more information and the code that is giving you trouble.
Please also check that you named your classes correctly as most of the problems usually come by not following naming conventions and Spring being unable to find implementation classes.