For the latest stable version, please use Spring Data REST 4.4.0! |
Projections and Excerpts
Spring Data REST presents a default view of the domain model you export. However, sometimes, you may need to alter the view of that model for various reasons. This section covers how to define projections and excerpts to serve up simplified and reduced views of resources.
Projections
Consider the following domain model:
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String firstName, lastName;
@OneToOne
private Address address;
…
}
The Person
object in the preceding example has several attributes:
-
id
is the primary key. -
firstName
andlastName
are data attributes. -
address
is a link to another domain object.
Now assume that we create a corresponding repository, as follows:
interface PersonRepository extends CrudRepository<Person, Long> {}
By default, Spring Data REST exports this domain object, including all of its attributes. firstName
and lastName
are exported as the plain data objects that they are. There are two options regarding the address
attribute. One option is to also define a repository for Address
objects, as follows:
interface AddressRepository extends CrudRepository<Address, Long> {}
In this situation, a Person
resource renders the address
attribute as a URI to its corresponding Address
resource. If we were to look up “Frodo” in the system, we could expect to see a HAL document like this:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/persons/1"
},
"address" : {
"href" : "http://localhost:8080/persons/1/address"
}
}
}
There is another way. If the Address
domain object does not have its own repository definition, Spring Data REST includes the data fields inside the Person
resource, as the following example shows:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"address" : {
"street": "Bag End",
"state": "The Shire",
"country": "Middle Earth"
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/persons/1"
}
}
}
But what if you do not want address
details at all? Again, by default, Spring Data REST exports all of its attributes (except the id
). You can offer the consumer of your REST service an alternative by defining one or more projections. The following example shows a projection that does not include the address:
@Projection(name = "noAddresses", types = { Person.class }) (1)
interface NoAddresses { (2)
String getFirstName(); (3)
String getLastName(); (4)
}
1 | The @Projection annotation flags this as a projection. The name attribute provides
the name of the projection, which we cover in more detail shortly. The types attributes targets this projection to apply only to Person objects. |
2 | It is a Java interface, making it declarative. |
3 | It exports the firstName . |
4 | It exports the lastName . |
The NoAddresses
projection only has getters for firstName
and lastName
, meaning that it does not serve up any address information. Assuming you have a separate repository for Address
resources, the default view from Spring Data REST differs slightly from the previous representation, as the following example shows:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/persons/1{?projection}", (1)
"templated" : true (2)
},
"address" : {
"href" : "http://localhost:8080/persons/1/address"
}
}
}
1 | This resource has a new option: {?projection} . |
2 | The self URI is a URI Template. |
To view the projection to the resource, look up localhost:8080/persons/1?projection=noAddresses
.
The value supplied to the projection query parameter is the same as that specified in @Projection(name = "noAddress") . It has nothing to do with the name of the projection’s interface.
|
You can have multiple projections.
See Projections to see an example project. We encourage you to experiment with it. |
Spring Data REST finds projection definitions as follows:
-
Any
@Projection
interface found in the same package as your entity definitions (or one of its sub-packages) is registered. -
You can manually register a projection by using
RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…)
.
In either case, the projection interface must have the @Projection
annotation.
Finding Existing Projections
Spring Data REST exposes Application-Level Profile Semantics (ALPS) documents, a micro metadata format. To view the ALPS metadata, follow the profile
link exposed by the root resource. If you navigate down to the ALPS document for Person
resources (which would be /alps/persons
), you can find many details about Person
resources. Projections are listed, along with the details about the GET
REST transition, in blocks similar to the following example:
{ …
"id" : "get-person", (1)
"name" : "person",
"type" : "SAFE",
"rt" : "#person-representation",
"descriptors" : [ {
"name" : "projection", (2)
"doc" : {
"value" : "The projection that shall be applied when rendering the response. Acceptable values available in nested descriptors.",
"format" : "TEXT"
},
"type" : "SEMANTIC",
"descriptors" : [ {
"name" : "noAddresses", (3)
"type" : "SEMANTIC",
"descriptors" : [ {
"name" : "firstName", (4)
"type" : "SEMANTIC"
}, {
"name" : "lastName", (4)
"type" : "SEMANTIC"
} ]
} ]
} ]
},
…
1 | This part of the ALPS document shows details about GET and Person resources. |
2 | This part contais the projection options. |
3 | This part contains the noAddresses projection. |
4 | The actual attributes served up by this projection include firstName and lastName . |
Projection definitions are picked up and made available for clients if they are:
|
Bringing in Hidden Data
So far in this section, we have covered how projections can be used to reduce the information that is presented to the user. Projections can also bring in normally unseen data. For example, Spring Data REST ignores fields or getters that are marked up with @JsonIgnore
annotations. Consider the following domain object:
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@JsonIgnore private String password; (1)
private String[] roles;
…
1 | Jackson’s @JsonIgnore is used to prevent the password field from being serialized into JSON. |
The User
class in the preceding example can be used to store user information as well as integration with Spring Security. If you create a UserRepository
, the password
field would normally have been exported, which is not good. In the preceding example, we prevent that from happening by applying Jackson’s @JsonIgnore
on the password
field.
Jackson also does not serialize the field into JSON if @JsonIgnore is on the field’s corresponding getter function.
|
However, projections introduce the ability to still serve this field. It is possible to create the following projection:
@Projection(name = "passwords", types = { User.class })
interface PasswordProjection {
String getPassword();
}
If such a projection is created and used, it sidesteps the @JsonIgnore
directive placed on User.password
.
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 REST cannot discern the sensitivity of such data, it is up to you to avoid such situations. |
Projections can also generate virtual data. Imagine you had the following entity definition:
@Entity
public class Person {
...
private String firstName;
private String lastName;
...
}
You can create a projection that combines the two data fields in the preceding example together, as follows:
@Projection(name = "virtual", types = { Person.class })
public interface VirtualProjection {
@Value("#{target.firstName} #{target.lastName}") (1)
String getFullName();
}
1 | Spring’s @Value annotation lets you plug in a SpEL expression that takes the target object and splices together its firstName and lastName attributes to render a read-only fullName . |
Excerpts
An excerpt is a projection that is automatically applied to a resource collection. For example, you can alter the PersonRepository
as follows:
@RepositoryRestResource(excerptProjection = NoAddresses.class)
interface PersonRepository extends CrudRepository<Person, Long> {}
The preceding example directs Spring Data REST to use the NoAddresses
projection when embedding Person
resources into collections or related resources.
Excerpt projections are not automatically applied to single resources. They have to be applied deliberately. Excerpt projections are meant to provide a default preview of collection data but not when fetching individual resources. See Why is an excerpt projection not applied automatically for a Spring Data REST item resource? for a discussion on the subject. |
In addition to altering the default rendering, excerpts have additional rendering options as shown in the next section.
Excerpting Commonly Accessed Data
A common situation with REST services arises when you compose domain objects. For example, a Person
is stored in one table and their related Address
is stored in another. By default, Spring Data REST serves up the person’s address
as a URI the client must navigate. But if it is common for consumers to always fetch this extra piece of data, an excerpt projection can put this extra piece of data inline, saving you an extra GET
. To do so, you can define another excerpt projection, as follows:
@Projection(name = "inlineAddress", types = { Person.class }) (1)
interface InlineAddress {
String getFirstName();
String getLastName();
Address getAddress(); (2)
}
1 | This projection has been named inlineAddress . |
2 | This projection adds getAddress , which returns the Address field. When used inside a projection, it causes the information to be included inline. |
You can plug it into the PersonRepository
definition, as follows:
@RepositoryRestResource(excerptProjection = InlineAddress.class)
interface PersonRepository extends CrudRepository<Person, Long> {}
Doing so causes the HAL document to appear as follows:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"address" : { (1)
"street": "Bag End",
"state": "The Shire",
"country": "Middle Earth"
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/persons/1"
},
"address" : { (2)
"href" : "http://localhost:8080/persons/1/address"
}
}
}
1 | The address data is directly included inline, so you do not have to navigate to get it. |
2 | The link to the Address resource is still provided, making it still possible to navigate to its own resource. |
Note that the preceding example is a mix of the examples shown earlier in this chapter. You may want to read back through them to follow the progression to the final example.
Configuring @RepositoryRestResource(excerptProjection=…) for a repository alters the default behavior. This can potentially cause breaking changes to consumers of your service if you have already made a release.
|