Spring Data REST Projections and Excerpts

Tutorial on How to enable and configure Projections and Excerpts in a Spring data REST application with examples.

Overview

Spring Data REST detects the Spring Data repository interfaces and entities in our application and exposes the repositories as RESTFull resources. This tutorial focuses on the projections and excerpts in Spring Data REST.

First, we will create a couple of entities and repositories that we will use as an example, throughout the article. Next, we will understand the concept of projections in Spring Data REST and the different possibilities projection offers. Lastly, we will study excerpts with the help of an example.

Entities and Repositories

In order to demonstrate the Projections and Excerpts, first we will create our entities and repositories. Our entities are Student and Course, where both of them have a many-to-many relationship, based on enrolment table.

Student Entity

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private Integer year;

    @ManyToMany(mappedBy = "students")
    public List<Course> courses;
    
    // Getters & Setters
}Code language: Java (java)

Course Entity

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(
            name = "enrolment",
            inverseJoinColumns =
            @JoinColumn(name = "student_id", referencedColumnName = "id"),
            joinColumns =
            @JoinColumn(name = "course_id", referencedColumnName = "id")
    )
    private List<Student> students;
    
    // Getters and Setters
}Code language: Java (java)

Now, we will define repository interfaces for both of these entities and mark them with Spring Data REST @RepositoryRestResource annotation.

StudentRepository

@RepositoryRestResource
public interface StudentRepository
        extends CrudRepository<Student, Long> {
}Code language: Java (java)

Course Repository

@RepositoryRestResource
public interface CourseRepository
        extends CrudRepository<Course, Long> {
}Code language: Java (java)

Now, we can launch our Spring Data REST application and create some students, courses and make the students enrol the courses. However, we won’t cover that part here.

Without Projections

In this section we will confirm the default view of an entity. Then in the next section, we will create projections to customise this default view.

When we access a student resource, for example /students/1 we can see students first name, last name and year fields are listed.

{
  "firstName" : "Eddard",
  "lastName" : "Stark",
  "year" : 2020,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/1"
    },
    "student" : {
      "href" : "http://localhost:8080/students/1"
    },
    "courses" : {
      "href" : "http://localhost:8080/students/1/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

The Spring Data REST returns a default view of our entities that excludes the Id field.

Spring Data REST Projections

As we have seen, the default representation of any entity contains all fields, except Id. Entity Projections allow us to create a custom view on top of existing entities. This is useful when a client wants to view only a subset of the fields.

With the projections in Spring Data REST, we can:

  • Chose a subset of fields to be returned as part of a custom view.
  • Return fields that are not included in the original Entity view (for example Id field)
  • Derive new fields based on the values of other fields in the view
  • Add fields from relational mapping (association) between entities.

In the coming sections we will demonstrate each of them with examples. For now, make a note that projections are meant for viewing a resource in a particular form and they are always read-only. Thus, only HTTP GET requests support projections.

Enable Projections

In order to enable projection on an entity, we need to define an interface with @Projection annotation.
Let’s create a custom view for our Student entity.

@Projection(name = "studentView",
        types = Student.class)
public interface StudentView {
    String getFirstName();
}Code language: Java (java)

Next, we need to place this interface under the package of Student entity and Spring Data REST will automatically enable the projection.

However, if you wish to keep the view in separate package, you can provide a custom implementation to RepositoryRestConfigurer.

@Configuration
public class RestConfiguration 
    implements RepositoryRestConfigurer {
 
  @Override
  public void configureRepositoryRestConfiguration(
      RepositoryRestConfiguration config) {
    config
      .getProjectionConfiguration()
      .addProjection(StudentView.class);
  }
}Code language: Java (java)

Once the projections are enabled, Spring Data REST introduces a query string parameter of projection.

Let’s execute http://localhost:8080/students/1. and see the outcome.

{
  "firstName" : "Eddard",
  "lastName" : "Stark",
  "year" : 2020,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/1"
    },
    "student" : {
      "href" : "http://localhost:8080/students/1{?projection}",
      "templated" : true
    },
    "courses" : {
      "href" : "http://localhost:8080/students/1/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

It is evident that the Student resource now supports projections.
Thus, we can now view a projection by passing its name into the query string parameter.

For example, we will access the studentView by using query String

http://localhost:8080/students/1?projection=studentView

Adding, the projection parameter and providing name of the view, we get projected response.

{
  "firstName" : "Eddard",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/1"
    },
    "student" : {
      "href" : "http://localhost:8080/students/1{?projection}",
      "templated" : true
    },
    "courses" : {
      "href" : "http://localhost:8080/students/1/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

This time we got only the the firstName field of the Student entity.

Note that, with Spring Data REST projection we can create multiple custom views of an entity. Having that, clients can decide which projection they want to view.

Projecting a Subset of Fields

Spring data REST Projection supports projecting only a selected fields from an entity representation. To do that, we can define those specific fields into our @Projection interface.

Let’s create a custom view of our Student entity with first name and last name fields.

@Projection(name = "studentView",
        types = Student.class)
public interface StudentView {
    String getFirstName();

    String getLastName();
}Code language: Java (java)

Now when we access the view by proving the name of the view –

http://localhost:8080/students/1?projection=studentView
{
  "firstName" : "Eddard",
  "lastName" : "Stark",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/1"
    },
    "student" : {
      "href" : "http://localhost:8080/students/1{?projection}",
      "templated" : true
    },
    "courses" : {
      "href" : "http://localhost:8080/students/1/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

We can see both firstName and lastName fields are now returned.

Projecting Hidden Fields

As discussed earlier, Spring Data REST doesn’t return Id field of the Entities. However, if we explicitly wish to return that, we can add it to our projection.

@Projection(name = "studentView",
        types = Student.class)
public interface StudentView {
    Long getId();

    String getFirstName();

    String getLastName();
}Code language: Java (java)

Now, accessing our projection we can see the Id field is part of the response.

{
  "id" : 1,
  "firstName" : "Eddard",
  "lastName" : "Stark",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/1"
    },
    "student" : {
      "href" : "http://localhost:8080/students/1{?projection}",
      "templated" : true
    },
    "courses" : {
      "href" : "http://localhost:8080/students/1/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

Project Derived Fields

Additionally, with Spring data REST it is possible to create new fields that are calculated or derived from the values of other fields.

For example, we will add a new field displayName which follows a pattern – ‘lastName, firstName‘.

@Projection(name = "studentView",
        types = Student.class)
public interface StudentView {
    Long getId();

    String getFirstName();

    String getLastName();

    @Value("#{target.lastName}, #{target.firstName}")
    String getDisplayName();
}Code language: Java (java)

In order to include derived or calculated fields we can use Spring @Value annotation along with SpEL expression. Let’s fetch the studentView projection to verify.

{
  "id" : 1,
  "firstName" : "Eddard",
  "lastName" : "Stark",
  "displayName" : "Stark, Eddard",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/1"
    },
    "student" : {
      "href" : "http://localhost:8080/students/1{?projection}",
      "templated" : true
    },
    "courses" : {
      "href" : "http://localhost:8080/students/1/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

The newly inserted field is now present in the projection and its value is derived from two different fields of the entity.

Projecting Relational Fields

As our Student entity is mapped (many-to-many) to Course entity, we can include these relationally mapped (or referenced entities) in the output. The Spring Data REST Projection allows us to include fields from other entities that are referenced.

@Projection(name = "studentView",
        types = Student.class)
public interface StudentView {
    Long getId();

    String getFirstName();

    String getLastName();

    @Value("#{target.lastName}, #{target.firstName}")
    String getDisplayName();

    List<Course> getCourses();
}Code language: Java (java)

With this we have added a List of Course entities to our Student projection.

{
  "id" : 1,
  "firstName" : "Eddard",
  "lastName" : "Stark",
  "displayName" : "Stark, Eddard",
  "courses" : [ {
    "name" : "Spring Boot Introduction"
  }, {
    "name" : "Spring Data REST Guide"
  } ],

 // Skipped
}Code language: JSON / JSON with Comments (json)

The projection response contains nested list of Course entities.

Finding Available Projections

As discussed above, with Spring Data REST we have define a multiple projections for a resource. However, to view a resources in a specific form, the clients need to know the names of all projections that are available for a resource.

In order to do that, Spring Data REST includes the projection details in /profile resource by default. Let’s view the profile of our student entity located at /profile/students.

{
  "id":"get-students",
  "name":"students",
  "type":"SAFE",
  "descriptor":[
    {
      "name":"projection",
      "type":"SEMANTIC",
      "doc":{
        "format":"TEXT",
        "value":"The projection that shall be applied when rendering the response. Acceptable values available in nested descriptors."
      },
      "descriptor":[
        {
          "name":"studentView",
          "type":"SEMANTIC",
          "descriptor":[
            {
              "name":"id",
              "type":"SEMANTIC"
            },
            {
              "name":"firstName",
              "type":"SEMANTIC"
            },
            {
              "name":"lastName",
              "type":"SEMANTIC"
            },
            {
              "name":"displayName",
              "type":"SEMANTIC"
            },
            {
              "name":"courses",
              "type":"SEMANTIC"
            }
          ]
        }
      ]
    }
  ],
  "rt":"#student-representation"
}Code language: JSON / JSON with Comments (json)

Above is the snippet from the complete response of /profile/students resource. We can see the get-students descriptor has nested projection descriptor that lists all the available projections.

Spring Data REST Excerpts

So far we have seen that, projections are accessed by using an additional query string. However, Spring Data REST Excerpts allow us to configure a projection as the default view of an entity. Similar to projections, excerpts are a read-only views and they cannot be used to modify underlying resource using HTTP PUT etc.

In order to enable excerpts, we need to apply the projection on the @RepositoryRestResource annotation on the repository interface.

@RepositoryRestResource(excerptProjection = StudentView.class)
public interface StudentRepository
        extends CrudRepository<Student, Long> {
}Code language: PHP (php)

We have applied studentView projection on the student repository. Now we can access the collection resource –/students/` and confirm that.

{
  "_embedded" : {
    "students" : [ {
      "id" : 1,
      "firstName" : "Eddard",
      "lastName" : "Stark",
      "displayName" : "Stark, Eddard",
      "courses" : [ {
        "name" : "Spring Boot Introduction"
      }, {
        "name" : "Spring Data REST Guide"
      } ],
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/students/1"
        },
        "student" : {
          "href" : "http://localhost:8080/students/1{?projection}",
          "templated" : true
        },
        "courses" : {
          "href" : "http://localhost:8080/students/1/courses"
        }
      }
    },
    .....
}Code language: JSON / JSON with Comments (json)

This is a snippet of the collection resource of Student.

It is important to note that Spring Data REST Excerpts are by default applied to the collection resource of an entity. Single Item Resources in Spring Data REST do not support excerpts. Reason behind this is that, if single item resources start serving excerpts (aka custom views), the clients will have no idea about the original form of the entities. That will make it impossible for them to issue a PUT request. Thus, if we try to access a single item entity (for example /students/6), we will get the entity in its default form.

If we access a particular resource through its relationally mapped entity we get the resource in the form of excerpts.
For example accessing a particular students from a course resource (/courses/2/students/4).

{
  "id" : 4,
  "firstName" : "Tomund",
  "lastName" : "Giantsbane",
  "displayName" : "Giantsbane, Tomund",
  "courses" : [ {
    "name" : "Spring Data REST Guide"
  } ],
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/4"
    },
    "student" : {
      "href" : "http://localhost:8080/students/4{?projection}",
      "templated" : true
    },
    "courses" : {
      "href" : "http://localhost:8080/students/4/courses"
    }
  }
}Code language: JSON / JSON with Comments (json)

Summary

That was about Spring Data REST Projections and Excerpts. This tutorial gave a detailed overview of the projections and excerpts concepts. With the help of practical examples, we understood how the projections helps us creating custom views for our entity resources. Doing so, we can hide or unhide fields, introduce new fields that are derived from the existing ones, or introduce fields from relationally mapped resources. In the last section, we created an example of excerpts and understood that Spring Data REST Excerpts are only available on collection resources, and why.

For full source of the examples used here, please visit our Github Repository.