Interface-based Projections
The easiest way to limit the result of the queries to only the name attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example:
interface NamesOnly {
String getFirstname();
String getLastname();
}
The important bit here is that the properties defined here exactly match properties in the aggregate root. Doing so lets a query method be added as follows:
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.
Projections can be used recursively. If you want to include some of the Address
information as well, create a projection interface for that and return that interface from the declaration of getAddress()
, as shown in the following example:
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
On method invocation, the address
property of the target instance is obtained and wrapped into a projecting proxy in turn.
Closed Projections
A projection interface whose accessor methods all match properties of the target aggregate is considered to be a closed projection. The following example (which we used earlier in this chapter, too) is a closed projection:
interface NamesOnly {
String getFirstname();
String getLastname();
}
If you use a closed projection, Spring Data can optimize the query execution, because we know about all the attributes that are needed to back the projection proxy. For more details on that, see 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, as shown in the following example:
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
The aggregate root backing the projection is available in the target
variable.
A projection interface using @Value
is an open projection.
Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.
The expressions used in @Value
should not be too complex — you want to avoid programming in String
variables.
For very simple expressions, one option might be to resort to default methods (introduced in Java 8), as shown in the following example:
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 invoke that from the SpEL expression, as shown in the following example:
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
Notice how the SpEL expression refers to myBean
and invokes the getFullName(…)
method and forwards the projection target as a 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 through an Object
array named args
. The following example shows how to get a method parameter from the args
array:
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
Again, for more complex expressions, you should use a Spring bean and let the expression invoke a method, as described earlier.