Dev. To /peholmst/bu…

Earlier we learned how to use Spring Data to build aggregations. Now, when we have summaries, we need to build a repository to store and retrieve them.

Building repositories using Spring Data is easy. All you need to do is declare your repository interface and have it extend the Spring Data interface JpaRepository. However, it also makes it easy to accidentally create repositories for local entities (which might happen if your developers are not familiar with DDD but familiar with JPA). Therefore, I always declare my base repository interface like this:

@NoRepositoryBean / / < 1 >
public interface BaseRepository<Aggregate extends BaseAggregateRoot<ID>, ID extends Serializable> / / < 2 >extends JpaRepository<Aggregate.ID> / / < 3 >JpaSpecificationExecutor<Aggregate> { / / < 4 >

    default @NotNull T getById(@NotNull ID id) { / / < 5 >
        return findById(id).orElseThrow(() -> new EmptyResultDataAccessException(1)); }}Copy the code
  1. This comment tells Spring Data not to attempt to instantiate the interface directly.
  2. We limit the entity of the repository service to aggregation roots only.
  3. We extend JpaRepository.
  4. I personally prefer the specification to the query approach. We’ll return to why later.
  5. The built-in findById method returns Optional. In many cases, when you get an aggregation by its ID, you assume that the aggregation will exist. Having to Optional every time is a waste of time and code, so you’re better off doing it directly in the repository.

With this base interface, the repository for the Customer aggregation root might look like this:

public interface CustomerRepository extends BaseRepository<Customer.CustomerId> {
    // No need for additional methods
}
Copy the code

This is all you need to retrieve and save the aggregation. Now let’s look at how to implement the query.

Query methods and specifications

The most straightforward way to create queries in Spring Data is to define the carefully named FindBy-methods (see the Spring Data Reference documentation if you’re not familiar with this method)

I find these useful for simple queries based on just one or two key lookup aggregates; For example, in the PersonRepository you can use the called method findBySocialSecurityNumber; In CustomerRepository, you can use a method called findByCustomerNumber. However, FOR more advanced or complex queries, I try to avoid using findby-methods.

I do this for two main reasons: First, method names tend to get long and pollute the code no matter where they are used.

Second, very specific requirements from application services can creep into the repository, and before long, your repository will be full of query methods that perform nearly the same function but change little. I want to keep the domain model as clean as possible. Instead, I prefer to use specifications to construct queries.

When querying by specification, you first build a specification object that describes the desired query result. Specification objects can also be combined with and or using logical operators. For maximum flexibility, I try to keep the specs as small as possible. If needed, I create composite specifications for commonly used specifications combinations.

Spring Data has built-in support for the specification. To create the Specification, you must implement the Specification interface. This interface relies on the JPA Criteria API, so familiarize yourself with it if you haven’t used it before.

The Specification interface contains a single method that you must implement. It generates a JPA Criteria predicate and takes as input all the necessary objects needed to create the predicate.

The easiest way to create a specification is to set up a specification factory. It is best illustrated by an example:

public class CustomerSpecifications {

    public @NotNull Specification<Customer> byName(@NotNull String name) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like( / / < 1 >
            root.get(Customer_.name), / / < 2 >
            name
        );
    }

    public @NotNull Specification<Customer> byLastInvoiceDateAfter(@NotNull LocalDate date) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.greaterThan(root.get(Customer_.lastInvoiceDate), date);
    }

    public @NotNull Specification<Customer> byLastInvoiceDateBefore(@NotNull LocalDate date) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Customer_.lastInvoiceDate), date);
    }

    public @NotNull Specification<Customer> activeOnly(a) {
        return(root, query, criteriaBuilder) -> criteriaBuilder.isTrue(root.get(Customer_.active)); }}Copy the code
  1. Here, I’m just doing a simple like query, but in a real-world specification, you might want to be more comprehensive, paying attention to wildcards, case matching, and so on.
  2. Customer_ is the metamodel class generated by the JPA implementation

You can then use the specification in the following ways:

public class CustomerService {

    private final CustomerRepository repository;
    private final CustomerSpecifications specifications;

    public CustomerService(CustomerRepository repository, CustomerSpecifications specifications) {
        this.repository = repository;
        this.specifications = specifications;
    }

    public Page<Customer> findActiveCustomersByName(String name, Pageable pageable) { / / < 1 >
        return repository.findAll(
            specifications.byName(name).and(specifications.activeOnly()), / / < 2 >pageable ); }}Copy the code
  1. Never write a method that returns an unlimited result set (at least in production code). Use paging, as I’ve done here, or a limited and reasonable limit on the number of records a query can return.
  2. The AND operator is used here to combine the two specifications.

A description of the repository and QueryDSL

Spring Data also supports QueryDSL. In this case, instead of using the specification, you use the QueryDSL predicate directly. The design principles are pretty much the same, so if you’re more comfortable with QueryDSL than the JPA Criteria API, there’s no reason to change.

Specifications and Tests

There is an obvious drawback to using specifications to support query methods, and it has to do with unit testing. Because the specification uses the JPA Criteria API behind the back, there is no easy way to assert the content of a given object without the Criteria constructing and analyzing its JPA predicates – it is a remarkable process.

However, there are some solutions. The most obvious approach is to simply ignore checking the incoming specification when simulating the repository in a unit test, and instead test your specification with a separate integration test (for example, using an in-memory H2 database). In many cases, that may be enough.

There is another way to avoid using integration tests, but it requires some extra upfront work. If you look closely at the specification factory, you’ll see that the factory methods are not static, but the instance methods and classes themselves are not final. This means you can simulate or stub an entire factory. Also, because the factory method only returns the object that implements the Specification interface, you can emulate or stub the interface as well. This means that as long as you avoid the Specification interface (using the JPA Criteria API), you can build a mock Specification factory that returns a mock Specification that can then be analyzed and used as a basis for testing assertions. Unfortunately, this article is not the right place to dive into this, so I’ll leave it as an exercise for the reader.