diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 7ec0cc640..822981d9a 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -15,6 +15,7 @@ include::intro.adoc[] include::getting-started.adoc[] include::repository-resources.adoc[] include::representations.adoc[] +include::projections-excerpts.adoc[] include::validation.adoc[] include::events.adoc[] include::metadata.adoc[] diff --git a/src/main/asciidoc/projections-excerpts.adoc b/src/main/asciidoc/projections-excerpts.adoc new file mode 100644 index 000000000..b93a2378e --- /dev/null +++ b/src/main/asciidoc/projections-excerpts.adoc @@ -0,0 +1,321 @@ +[[projections-excerpts]] += Projections and Excerpts + +Spring Data REST presents a default view of the domain model you are exporting. But sometimes, you may need to +alter the view of that model for various reasons. In this section, you will learn how to define *projections* and +*excerpts* to serve up simplified and reduced views of resources. + +== Projections + +Look at the following domain model: + +[source,java] +---- +@Entity +public class Person { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String firstName; + + private String lastName; + + @OneToOne + private Address address; + ... +} +---- + +This `Person` has several attributes: + +* *id* is the primary key +* *firstName* and *lastName* are data attributes +* *address* is a link to another domain object + +Now assume we create a corresponding repository as follows: + +[source,java] +---- +public interface PersonRepository extends CrudRepository {} +---- + +By default, Spring Data REST will export this domain object including all of its attributes. *firstName* and *lastName* will be 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 like this: + +[source,java] +---- +public interface AddressRepository extends CrudRepository {} +---- + +In this situation, a `Person` resource will render the *address* attribute as a URI to it's corresponding `Address` resource. If we were +to look up "Frodo" in the system, we could expect to see a HAL document like this: + +[source,javascript] +---- +{ + "firstName" : "Frodo", + "lastName" : "Baggins", + "_links" : { + "self" : { + "href" : "http://localhost:8080/persons/1" + }, + "address" : { + "href" : "http://localhost:8080/persons/1/address" + } + } +} +---- + +There is another route. If the `Address` domain object does not have it's own repository definition, Spring Data REST will inline the data +fields right inside the `Person` resource. + +[source,javascript] +---- +{ + "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 don't want *address* details at all? Again, by default, Spring Data REST will export all its attributes (except the *id*). +You can offer the consumer of your REST service an alternative by defining one or more *projections*. + +[source,java] +---- +@Projection(name = "noAddresses", types = { Person.class }) <1> +public interface NoAddresses { <2> + + public String getFirstName(); <3> + + public String getLastName(); <4> + +} +---- + +This projection has the following details: + +<1> The `@Projection` annotation flags this as a projection. The *name* atrributes provides +the name of the projection, which you'll see how to use shortly. The *types* attributes targets this projection to only apply to +`Person` objects. +<2> It's 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 won't serve up any address information. + +Assuming you have a separate repository for `Address` resources, the default view from Spring Data REST is slightly different as +shown below: + +[source,javascript] +---- +{ + "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> There is a new option listed for this resource, *{?projection}*. +<2> The *self* URI is a URI Template. + +To view apply the projection to the resource, look up `http://localhost:8080/persons/1/projection=noAddresses`. + +NOTE: The value supplied to the `projection` query parameter is the same as specified in `@Projection(name = "noAddress")`. It has +nothing to do with the name of the projection's interface. + +It's possible to have multiple projections. + +=== Finding existing projections + +Spring Data REST provides https://spring.io/blog/2014/07/14/spring-data-rest-now-comes-with-alps-metadata[ALPS], a micro metadata format. +To view the ALPS metadata, visit `http://localhost:8080/alps`. 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 will be listed along with the details about the *GET* REST transitions, something like this: + +[source,javascript] +---- +... +"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> Further down are the *projection* options. +<3> Further down you can see projection *noAddresses* listed. +<4> The actual attributes served up by this projection include *firstName* and *lastName*. + +=== Bringing in hidden data + +So far, you have seen 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 will ignore fields or getters that are marked up with `@JsonIgnore` annotations. +Look at the following domain object: + +[source,java] +---- +@Entity +public class User { + + @Id @GeneratedValue + private Long id; + + private String name; + + @JsonIgnore <1> + private String password; + + private String[] roles; + ... +---- + +<1> Jackson's `@JsonIgnore` is used to prevent the *password* field from getting serialized into JSON. + +This `User` class 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. Not good! In this example, we prevent that from happening by applying Jackson's +`@JsonIgnore` on the *password* field. + +NOTE: Jackson will also 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's possible to create a projection like this: + +[source,java] +---- +@Projection(name = "passwords", types = { User.class }) <2> +public interface PasswordProjection { + + public String getPassword(); + +} +---- + +If such a projection is created and used, it will side step the `@JsonIgnore` directive placed on `User.password`. + +IMPORTANT: This example may seem a bit contrived, but it's 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 the developers to avoid such situations. + +== Excerpts + +An excerpt is a projection that is applied to a repository automatically. For an example, you can alter the `PersonRepository` as follows: + +[source,java] +---- +@RepositoryRestResource(excerptProjection = NoAddresses.class) +public interface PersonRepository extends CrudRepository {} +---- + +This directs Spring Data REST to use the `NoAddresses` projection when embedding `Person` resources into collections or related resources. + +NOTE: Excerpt projections do NOT apply when rendering a single resource. + +In addition to altering the default rendering, excerpts have additional rednering options as shown below. + +== 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 will serve up the person's *address* as a URI the client must +navigate. But if it's common for consumers to always fetch this extra piece of data, an excerpt projection can go ahead and inline +this extra piece of data, saving you an extra *GET*. + +To do so, let's define another excerpt projection: + +[source,java] +---- +@Projection(name = "inlineAddress", types = { Person.class }) <1> +public interface InlineAddress { + + public String getFirstName(); + + public String getLastName(); + + public Address getAddress(); <2> + +} +---- + +<1> This projection has been named *inlineAddress*. +<2> This projection adds in `getAddress` which returns the `Address` field. When used inside a projection, it causes the information +to be inlined. + +We can plug it into the `PersonRepository` definition as follows: + +[source,java] +---- +@RepositoryRestResource(excerptProjection = InlineAddress.class) +public interface PersonRepository extends CrudRepository {} +---- + +This will cause the HAL document to appear as follows: + +[source,javascript] +---- +{ + "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" + } + } +} +---- + +This should appear as a mix of what you've seen so far. + +<1> The *address* data is inlined directly, so you don't 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. + +WARNING: Configuring `@RepositoryRestResource(excerptProjection=...)` for a repository alters the default behavior. This can potentially +case breaking change to consumers of your service if you have already made a release. Use with caution.