Browse Source

DATACMNS-1011 - Rewrote chapter on repository projections.

pull/242/head
Oliver Gierke 9 years ago
parent
commit
cdc6201fce
  1. 1
      src/main/asciidoc/index.adoc
  2. 288
      src/main/asciidoc/repository-projections.adoc

1
src/main/asciidoc/index.adoc

@ -19,6 +19,7 @@ include::preface.adoc[] @@ -19,6 +19,7 @@ include::preface.adoc[]
:leveloffset: +1
include::dependencies.adoc[]
include::repositories.adoc[]
include::repository-projections.adoc[]
include::query-by-example.adoc[]
include::auditing.adoc[]
:leveloffset: -1

288
src/main/asciidoc/repository-projections.adoc

@ -1,163 +1,281 @@ @@ -1,163 +1,281 @@
[[projections]]
= Projections
Spring Data Repositories usually return the domain model when using query methods. However, sometimes, you may need to alter the view of that model for various reasons. In this section, you will learn how to define projections to serve up simplified and reduced views of resources.
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.
Look at the following domain model:
Imagine a sample repository and aggregate root type like this:
[source,java]
.A sample aggregate and repository
====
[source, java]
----
@Entity
public class Person {
class Person {
@Id @GeneratedValue
private Long id;
private String firstName, lastName;
@Id UUID id;
String firstname, lastname;
Address address;
@OneToOne
private Address address;
static class Address {
String zipCode, city, street;
}
}
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String street, state, country;
interface PersonRepository extends Repository<Person, UUID> {
Collection<Person> findByLastname(String lastname);
}
----
====
This `Person` has several attributes:
Now imagine we'd want to retrieve the person's name attributes only.
What means does Spring Data offer to achieve this?
* `id` is the primary key
* `firstName` and `lastName` are data attributes
* `address` is a link to another domain object
[[projections.interfaces]]
== Interface-based projections
Now assume we create a corresponding repository as follows:
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:
[source,java]
.A projection interface to retrieve a subset of attributes
====
[source, java]
----
interface PersonRepository extends CrudRepository<Person, Long> {
interface NamesOnly {
Person findPersonByFirstName(String firstName);
String getFirstname();
String getLastname();
}
----
====
Spring Data will return the domain object including all of its attributes. There are two options just to retrieve the `address` attribute. One option is to define a repository for `Address` objects like this:
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:
[source,java]
.A repository using an interface based projection with a query method
====
[source, java]
----
interface AddressRepository extends CrudRepository<Address, Long> {}
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
----
====
In this situation, using `PersonRepository` will still return the whole `Person` object. Using `AddressRepository` will return just the `Address`.
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.
However, what if you do not want to expose `address` details at all? You can offer the consumer of your repository service an alternative by defining one or more projections.
[[projections.interfaces.nested]]
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()`.
.Simple Projection
.A projection interface to retrieve a subset of attributes
====
[source,java]
[source, java]
----
interface NoAddresses { <1>
interface PersonSummary {
String getFirstName(); <2>
String getFirstname();
String getLastname();
AddressSummary getAddress();
String getLastName(); <3>
interface AddressSummary {
String getCity();
}
}
----
This projection has the following details:
<1> A plain Java interface making it declarative.
<2> Export the `firstName`.
<3> Export the `lastName`.
====
The `NoAddresses` projection only has getters for `firstName` and `lastName` meaning that it will not serve up any address information. The query method definition returns in this case `NoAdresses` instead of `Person`.
On method invocation, the `address` property of the target instance will be obtained and wrapped into a projecting proxy in turn.
[[projections.interfaces.closed]]
=== Closed projections
[source,java]
A projection interface whose accessor methods all match properties of the target aggregate are considered closed projections.
.A closed projection
====
[source, java]
----
interface PersonRepository extends CrudRepository<Person, Long> {
interface NamesOnly {
NoAddresses findByFirstName(String firstName);
String getFirstname();
String getLastname();
}
----
====
Projections declare a contract between the underlying type and the method signatures related to the exposed properties. Hence it is required to name getter methods according to the property name of the underlying type. If the underlying property is named `firstName`, then the getter method must be named `getFirstName` otherwise Spring Data is not able to look up the source property. This type of projection is also called _closed projection_. Closed projections expose a subset of properties hence they can be used to optimize the query in a way to reduce the selected fields from the data store. The other type is, as you might imagine, an _open projection_.
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.
[[projections.remodelling-data]]
== Remodelling data
[[projections.interfaces.open]]
=== Open projections
So far, you have seen how projections can be used to reduce the information that is presented to the user. Projections can be used to adjust the exposed data model. You can add virtual properties to your projection. Look at the following projection interface:
Accessor methods in projection interfaces can also be used to compute new values by using the `@Value` annotation on it:
.Renaming a property
[[projections.interfaces.open.simple]]
.An Open Projection
====
[source,java]
[source, java]
----
interface RenamedProperty { <1>
String getFirstName(); <2>
interface NamesOnly {
@Value("#{target.lastName}")
String getName(); <3>
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
}
----
This projection has the following details:
====
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.
<1> A plain Java interface making it declarative.
<2> Export the `firstName`.
<3> Export the `name` property. Since this property is virtual it requires `@Value("#{target.lastName}")` to specify the property source.
The expressions used in `@Value` shouldn't become too complex as you'd want to avoid programming in ``String``s.
For very simple expressions, one option might be to resort to default methods:
[[projections.interfaces.open.default]]
.A projection interface using a default method for custom logic
====
[source, java]
----
interface NamesOnly {
The backing domain model does not have this property so we need to tell Spring Data from where this property is obtained.
Virtual properties are the place where `@Value` comes into play. The `name` getter is annotated with `@Value` to use http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/html/expressions.html[SpEL expressions] pointing to the backing property `lastName`. You may have noticed `lastName` is prefixed with `target` which is the variable name pointing to the backing object. Using `@Value` on methods allows defining where and how the value is obtained.
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname.concat(" ").concat(getLastname());
}
}
----
====
Some applications require the full name of a person. Concatenating strings with `String.format("%s %s", person.getFirstName(), person.getLastName())` would be one possibility but this piece of code needs to be called in every place the full name is required. Virtual properties on projections leverage the need for repeating that code all over.
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:
[source,java]
[[projections.interfaces.open.bean-reference]]
.Sample Person object
====
[source, java]
----
interface FullNameAndCountry {
@Component
class MyBean {
String getFullName(Person person) {
}
}
interface NamesOnly {
@Value("#{target.firstName} #{target.lastName}")
@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`.
@Value("#{target.address.country}")
String getCountry();
.Sample Person object
====
[source, java]
----
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
----
====
In fact, `@Value` gives full access to the target object and its nested properties. SpEL expressions are extremly powerful as the definition is always applied to the projection method. Let's take SpEL expressions in projections to the next level.
Again, for more complex expressions rather use a Spring bean and let the expression just invoke a method as described <<projections.interfaces.open.bean-reference, above>>.
[[projections.dtos]]
== Class-based projections (DTOs)
Imagine you had the following domain model definition:
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.
[source,java]
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.
.A projecting DTO
====
[source, java]
----
@Entity
public class User {
class NamesOnly {
@Id @GeneratedValue
private Long id;
private String name;
private final String firstname, lastname;
private String password;
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
}
----
====
IMPORTANT: This example may seem a bit contrived, but it is possible with a richer domain model and many projections, to accidentally leak such details. Since Spring Data cannot discern the sensitivity of such data, it is up to the developers to avoid such situations. Storing a password as plain-text is discouraged. You really should not do this. For this example, you could also replace `password` with anything else that is secret.
[TIP]
.Avoiding boilerplate code for projection DTOs
====
The code that needs to be written for a DTO can be dramatically simplified using https://projectlombok.org[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:
In some cases, you might keep the `password` as secret as possible and not expose it more than it should be. The solution is to create a projection using `@Value` together with a SpEL expression.
[source, java]
----
@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.
====
[source,java]
[[projection.dynamic]]
== 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:
.A repository using a dynamic projection parameter
====
[source, java]
----
interface PasswordProjection {
@Value("#{(target.password == null || target.password.empty) ? null : '******'}")
String getPassword();
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:
.Using a repository with dynamic projections
====
[source, java]
----
void someMethod(PersonRepository people) {
The expression checks whether the password is `null` or empty and returns `null` in this case, otherwise six asterisks to indicate a password was set.
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}
----
====

Loading…
Cancel
Save