Persistence top

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll see different ways to use Spring Data MongoDB to count documents in our collections. We'll use all the tools available in MongoRepository.

We'll use annotations, query methods, and methods from the CrudRepository. Also, we'll build a simple service to aggregate our different use cases.

2. Use Case Setup

Our use case consists of a model class, a repository, and a service class. Moreover, we'll create a test class to help us make sure everything is working as intended.

2.1. Creating Model

We'll start by creating our model class. It'll be based on a few properties of a car:

@Document
public class Car {
    private String name;

    private String brand;

    public Car(String brand) {
        this.brand = brand;
    }

    // getters and setters
}

We're omitting an ID property as we're not going to need it in our examples. Also, we're adding a constructor which takes a brand property as a parameter to make testing easier.

2.2. Defining Repository

Let's define our repository without any methods:

public interface CarRepository extends MongoRepository<Car, String> {
}

We're considering a String ID, even though we're not declaring an ID property in our model. This is because MongoDB creates a default unique ID that we can still access via findById() if we want.

2.3. Defining Service Class

Our service will take advantage of the Spring Data Repository interface in different ways.

Let's define it with a reference to our repository:

@Service
public class CountCarService {

    @Autowired
    private CarRepository repo;
}

We'll build upon this class in the next sections, covering examples.

2.4. Preparing Tests

All our tests will run upon our service class. We just need a little setup so we don't end up with duplicated code:

public class CountCarServiceIntegrationTest {
    @Autowired
    private CountCarService service;

    Car car1 = new Car("B-A");

    @Before
    public void init() {
        service.insertCar(car1);
        service.insertCar(new Car("B-B"));
    }
}

We'll run this block before each test to simplify our test scenarios. Also, we're defining car1 outside init() to make it accessible in later tests.

3. Using CrudRepository

When using MongoRepository, which extends CrudRepository, we have access to basic functionality, including a count() method.

3.1. count() Method

So, in our first count example, without any methods in our repository, we can just call it in our service:

public long getCountWithCrudRepository() {
    return repo.count();
}

And we can test it:

@Test
public void givenAllDocs_whenCrudRepositoryCount_thenCountEqualsSize() {
    List<Car> all = service.findCars();

    long count = service.getCountWithCrudRepository();

    assertEquals(count, all.size());
}

Consequently, we ensure that count() outputs the same number as the size of the list of all documents in our collection.

Most importantly, we have to remember that a count operation is more cost-effective than listing all documents. This is both in terms of performance and reduced code. It won't make a difference with small collections, but with a big one we might end up with an OutOfMemoryError. In short, it's not a good idea to count documents by listing the whole collection.

3.2. Filter Using Example Object

CrudRepository can also help if we want to count documents with a specific property value. The count() method has an overloaded version, which receives an Example object:

public long getCountWithExample(Car item) {
    return repo.count(Example.of(item));
}

As a result, this simplifies the task. Now we just fill an object with the properties we want to filter, and Spring will do the rest. Let's cover it in our tests:

@Test
public void givenFilteredDocs_whenExampleCount_thenCountEqualsSize() {
    long all = service.findCars()
      .stream()
      .filter(car -> car.getBrand().equals(car1.getBrand()))
      .count();

    long count = service.getCountWithExample(car1);

    assertEquals(count, all);
}

4. Using the @Query Annotation

Our next example will be based on the @Query annotation:

@Query(value = "{}", count = true)
Long countWithAnnotation();

We have to specify the value property, otherwise Spring will try creating a query from our method name. But, since we want to count all documents, we simply specify an empty query.

Then, we specify that the result of this query should be a count projection by setting the count property to true.

Let's test it:

@Test
public void givenAllDocs_whenQueryAnnotationCount_thenCountEqualsSize() {
    List<Car> all = service.findCars();

    long count = service.getCountWithQueryAnnotation();

    assertEquals(count, all.size());
}

4.1. Filter on a Property

We can expand on our example, filtering by brand. Let's add a new method to our repository:

@Query(value = "{brand: ?0}", count = true)
public long countBrand(String brand);

In our query value, we specify our full MongoDB style query. The “?0” placeholder represents the first parameter of our method, which will be our query parameter value.

MongoDB queries have a JSON structure where we specify field names along values we want to filter by. So, when we call countBrand(“A”), the query translates to {brand: “A”}. This means we would filter our collection by items whose brand property have a value of “A”.

5. Writing a Derived Query Method

A derived query method is any method inside our repository that doesn't include a @Query annotation with a value. These methods are parsed by name by Spring so we don't have to write the query.

Since we already have a count() method in our CrudRepository, let's create an example that counts by a specific brand:

Long countByBrand(String brand);

This method will count all documents whose brand property matches the parameter value.

Now, let's add it to our service:

public long getCountBrandWithQueryMethod(String brand) {
    return repo.countByBrand(brand);
}

Then we ensure our method is behaving correctly by comparing it to a filtered stream count operation:

@Test
public void givenFilteredDocs_whenQueryMethodCountByBrand_thenCountEqualsSize() {
    String filter = "B-A";
    long all = service.findCars()
      .stream()
      .filter(car -> car.getBrand().equals(filter))
      .count();

    long count = service.getCountBrandWithQueryMethod(filter);

    assertEquals(count, all);
}

This works great when we have to write only a few different queries. But, it can become hard to maintain if we need too many different count queries.

6. Using Dynamic Count Queries With Criteria

When we need something a little more robust, we can use Criteria with a Query object.

But, to run a Query, we need MongoTemplate. It's instantiated during startup and made available in SimpleMongoRepository in the mongoOperations field.

One way to access it would be to extend SimpleMongoRepository and create a custom implementation instead of simply extending MongoRepository. But, there's a simpler way. We can inject it into our service:

@Autowired
private MongoTemplate mongo;

Then we can create our new count method, passing the Query to the count() method in MongoTemplate:

public long getCountBrandWithCriteria(String brand) {
    Query query = new Query();
    query.addCriteria(Criteria.where("brand")
      .is(brand));
    return mongo.count(query, Car.class);
}

This approach is useful when we need to create dynamic queries. We have full control over how the projection is created.

6.1. Filter Using Example Object

The Criteria object also allows us to pass an Example object:

public long getCountWithExampleCriteria(Car item) {
    Query query = new Query();
    query.addCriteria(Criteria.byExample(item));
    return mongo.count(query, Car.class);
}

This makes filtering by property easier, while still allowing dynamic parts.

7. Conclusion

In this article, we saw different ways to use a count projection in Spring Data MongoDB with repository methods.

We used the methods available and also created new ones using different approaches. Furthermore, we created tests by comparing our count methods to listing all the objects in our collection. In the same vein, we learned why it's not a good idea to count documents like that.

Also, we went a little deeper and used MongoTemplate to create more dynamic count queries.

And as always, the source code is available over on GitHub.

Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Persistence footer banner
guest
0 Comments
Inline Feedbacks
View all comments