Have you heard? 📣 We released a full-feature implemented auth server built on Spring-Boot 2. State-of-the-art OAuth2 provider and on top of that - fully open sourced! 🎉🛠

Go check out the blog post and then the repository as well! Happy hacking! 💻

Using Spring Data REST can get your API up and running in minutes and save you tons of work and coding…until you have a special use-case requiring you to spend a whole afternoon crawling the web for how to achieve that one missing piece of functionality.

In this post, I would like to share with you how to use projections. My use-case was quite simple:

Let’s have two entities: Cycle and Record.

Cycle contains list of records:

@Entity
@Table(name="cycles")
public class Cycle {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  
  ...
  
  @OneToMany(mappedBy = "cycle", cascade = CascadeType.ALL)
  private List<Record> records;
}

@Entity
@Table(name = "records")
public class Record {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  ...

  @ManyToOne
  @JoinColumn(name = "cycle_id")
  private Cycle cycle;
}

Both these entities have corresponding repositories:

@RepositoryRestResource
public interface CycleRepository extends PagingAndSortingRepository<Cycle, Long> {
}

@RepositoryRestResource
public interface RecordRepository extends JpaRepository<Record, Long> {
}

Since we are using Spring Data REST, these repositories are automatically exposed via REST API in HAL/JSON format. This is what a sample response for GET /cycles/1 looks like by default:

{
    "id": 1,
    ...
    "_links": {
        "self": {
            "href": "http://localhost:8008/cycles/1"
        },
        "records": {
            "href": "http://localhost:8008/cycles/1/records"
        }
    }
} 

But what if we want to get all record objects as part of the cycle instead of providing just a HAL link? The answer is @Projection! Projections allow us to alter the view model of our data. Following the official documentation, we can define what we want the response to look like:

@Projection(name = "inlineRecords", types = { Cycle.class })
public interface InlineRecords {

  long getId();

  // other getters
  ...

  List<Record> getRecords();
}

Adding this projection to our CycleRepository is quite easy:

@RepositoryRestResource(excerptProjection = InlineRecords.class)

When sending a request to fetch a specific cycle, we can specify which projection to use: GET /cycles/1?projection=inlineRecords:

{
    "id": 1,
    ...
    "_records": [
        .. records
    ],
    "_links": {
       ... links
    }
} 

The only problem is that, by default, Spring Data REST applies these projections to all _embedded data so when you request all cycles using GET /cycles, our inline projection will be used. That means that we will have all our records unwrapped in every cycle which could potentially be a very long response.

Fortunately, there is an alternative way how to register a projection and that is by using RepositoryRestConfigurerAdapter:

@Configuration
public class RepositoryConfig extends RepositoryRestConfigurerAdapter {

  @Override
  public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
    config.getProjectionConfiguration().addProjection(InlineRecords.class);
  }
}

This way, we can choose when the projection should be applied:

  • GET /cycles
  • GET /cycles?projection=inlineRecords
  • GET /cycles/{id}
  • GET /cycles/{id}?projection=inlineRecords

It is not hundred percent clear from the documentation what is the default behavior when considering different ways of using projections so I hope that this post might have saved you some frenetic Googling when you found yourself in need of this functionality.