Skip to content

Latest commit

 

History

History
281 lines (216 loc) · 7.77 KB

repository-projections.adoc

File metadata and controls

281 lines (216 loc) · 7.77 KB

Projections

Spring Data query methods usually return one or multiple instances of the aggregate root managed by the repository. However, it might sometimes be desirable to rather project on certain attributes of those types. Spring Data allows to model dedicated return types to more selectively retrieve partial views onto the managed aggregates.

Imagine a sample repository and aggregate root type like this:

Example 1. A sample aggregate and repository
class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<Person> findByLastname(String lastname);
}

Now imagine we’d want to retrieve the person’s name attributes only. What means does Spring Data offer to achieve this?

Interface-based projections

The easiest way to limit the result of the queries to expose the name attributes only is by declaring an interface that will expose accessor methods for the properties to be read:

Example 2. A projection interface to retrieve a subset of attributes
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

The important bit here is that the properties defined here exactly match properties in the aggregate root. This allows a query method to be added like this:

Example 3. A repository using an interface based projection with a query method
interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

The query execution engine will create proxy instances of that interface at runtime for each element returned and forward calls to the exposed methods to the target object.

Projections can be used recursively. If you wanted to include some of the Address information as well, create a projection interface for that and return that interface from the declaration of getAddress().

Example 4. A projection interface to retrieve a subset of attributes
interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}

On method invocation, the address property of the target instance will be obtained and wrapped into a projecting proxy in turn.

Closed projections

A projection interface whose accessor methods all match properties of the target aggregate are considered closed projections.

Example 5. A closed projection
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

If a closed projection is used, Spring Data modules can even optimize the query execution as we exactly know about all attributes that are needed to back the projection proxy. For more details on that, please refer to the module specific part of the reference documentation.

Open projections

Accessor methods in projection interfaces can also be used to compute new values by using the @Value annotation on it:

Example 6. An Open Projection
interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
  …
}

The aggregate root backing the projection is available via the target variable. A projection interface using @Value an open projection. Spring Data won’t be able to apply query execution optimizations in this case as the SpEL expression could use any attributes of the aggregate root.

The expressions used in @Value shouldn’t become too complex as you’d want to avoid programming in Strings. For very simple expressions, one option might be to resort to default methods:

Example 7. A projection interface using a default method for custom logic
interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname.concat(" ").concat(getLastname());
  }
}

This approach requires you to be able to implement logic purely based on the other accessor methods exposed on the projection interface. A second, more flexible option is to implement the custom logic in a Spring bean and then simply invoke that from the SpEL expression:

Example 8. Sample Person object
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

Note, how the SpEL expression refers to myBean and invokes the getFullName(…) method forwarding the projection target as method parameter. Methods backed by SpEL expression evaluation can also use method parameters which can then be referred to from the expression. The method parameters are available via an Object array named args.

Example 9. Sample Person object
interface NamesOnly {

  @Value("#{args[0] + ' ' + target.firstname + '!'}")
  String getSalutation(String prefix);
}

Again, for more complex expressions rather use a Spring bean and let the expression just invoke a method as described above.

Class-based projections (DTOs)

Another way of defining projections is using value type DTOs that hold properties for the fields that are supposed to be retrieved. These DTO types can be used exactly the same way projection interfaces are used, except that no proxying is going on here and no nested projections can be applied.

In case the store optimizes the query execution by limiting the fields to be loaded, the ones to be loaded are determined from the parameter names of the constructor that is exposed.

Example 10. A projecting DTO
class NamesOnly {

  private final String firstname, lastname;

  NamesOnly(String firstname, String lastname) {

    this.firstname = firstname;
    this.lastname = lastname;
  }

  String getFirstname() {
    return this.firstname;
  }

  String getLastname() {
    return this.lastname;
  }

  // equals(…) and hashCode() implementations
}
Tip
Avoiding boilerplate code for projection DTOs

The code that needs to be written for a DTO can be dramatically simplified using Project Lombok, which provides an @Value annotation (not to mix up with Spring’s @Value annotation shown in the interface examples above). The sample DTO above would become this:

@Value
class NamesOnly {
	String firstname, lastname;
}

Fields are private final by default, the class exposes a constructor taking all fields and automatically gets equals(…) and hashCode() methods implemented.

Dynamic projections

So far we have used the projection type as the return type or element type of a collection. However, it might be desirable to rather select the type to be used at invocation time. To apply dynamic projections, use a query method like this:

Example 11. A repository using a dynamic projection parameter
interface PersonRepository extends Repository<Person, UUID> {

  Collection<T> findByLastname(String lastname, Class<T> type);
}

This way the method can be used to obtain the aggregates as is, or with a projection applied:

Example 12. Using a repository with dynamic projections
void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}