Search code examples
springspring-bootjpaspring-data-restspring-hateoas

How to expose relations of an embedded id in a join table as links with Spring Data Rest


Intro

Let's say in a Spring Boot application with the Spring Data Rest module there are two main entities (e. g. Student and LegalGuardian). They are joined via an "association entity" (e. g. Guardianship) that is identified by an embedded id (e. g. GuardianshipId). Further, this embedded id consists of relations to the two main entities (not the id's of the main entities - the entities themself).

// The Main entities

@Entity
public class Student extends AbstractPersistable<Long> {

  private String name;
  
  @OneToMany(mappedBy = "guardianshipId.student")
  private List<Guardianship> guardianships;
  
  // getters and setters
  
}

@Entity
public class LegalGuardian extends AbstractPersistable<Long> {

  private String name;
  
  @OneToMany(mappedBy = "guardianshipId.legalGuardian")
  private List<Guardianship> guardianships;
  
  // getters and setters
  
}

// The association entity

@Entity
public class Guardianship implements Serializable {

  @EmbeddedId
  private GuardianshipId guardianshipId;
  
  private String name;
  
  // getters, setters, equals and hashCode

  @Embeddable
  public static class GuardianshipId implements Serializable {

    @ManyToOne
    private Student student;
    
    @ManyToOne
    private LegalGuardian legalGuardian;
    
    // getters, setters, equals and hashCode

  }

}

For all those entities there exists separate repositories:

  • StudentRepository : JpaRepository<Student, Long>,
  • LegalGuardianRepository : JpaRepository<LegalGuardian, Long> and
  • GuardianshipRepository : JpaRepository<Guardianship, Guardianship.GuardianshipId>

To query single Guardianships of GuardianshipRepository by id via REST, also a BackendIdConverter is implemented (so that the id then looks like {studentId}_{legalGuardianId}).

If the repository of the association entity is requested, by default the embedded id itself (and its attributes) is not serialized, so that the response looks like this:

$ curl "http://localhost:8080/guardianships/1_2"
{
  "name" : "Cool father",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/guardianships/1_2"
    },
    "guardianship" : {
      "href" : "http://localhost:8080/guardianships/1_2"
    }
  }
}

Quesion/Problem

What has to be done, so that response includes links to the entities that are defined inside the embedded id and looks like this:

$ curl "http://localhost:8080/guardianships/1_2"
{
  "name" : "Cool father",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/guardianships/1_2"
    },
    "guardianship" : {
      "href" : "http://localhost:8080/guardianships/1_2"
    },
    "student" : {
      "href" : "http://localhost:8080/guardianships/1_2/student"
    },
    "legalGuardian" : {
      "href" : "http://localhost:8080/guardianships/1_2/legalGuardian"
    }
  }
}

(Naive and successless) Attempt/Try

The first thought was to make the nested relations accessible by delegating to the embedded id:

@Entity
public class Guardianship implements Serializable {

  @EmbeddedId
  private GuardianshipId guardianshipId;
  
  public Student getStudent() { return guardianshipId.getStudent(); }
  
  public LegalGuardian getLegalGuardian() { return guardianshipId.getLegalGuardian(); }
  
  // the same as before

}

But doing this, both entities are completely serialized and the response looks like this:

$ curl "http://localhost:8080/guardianships/1_2"
{
  "name" : "Cool father",
  "student" : {
    "name" : "Hans",
    "new" : false
  },
  "legalGuardian" : {
    "name" : "Peter",
    "new" : false
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/guardianships/1_2"
    },
    "guardianship" : {
      "href" : "http://localhost:8080/guardianships/1_2"
    }
  }
}

For a full example, I created an executable sample project.


Solution

  • After some search I found two possible ways to expose the ID relations as links:

    1. Providing a RepresentationModelProcessor

    Implementing RepresentationModelProcessor lets me add custom links to the response representation.

    @Component
    public class GuardianshipProcessor
        implements RepresentationModelProcessor<EntityModel<Guardianship>> {
    
      @Autowired
      private RepositoryEntityLinks repositoryEntityLinks;
    
      @Override
      public EntityModel<Guardianship> process(EntityModel<Guardianship> model) {
        Link studentLink = repositoryEntityLinks.linkToItemResource(Student.class,
            model.getContent().getGuardianshipId().getStudent().getId());
        model.add(studentLink);
        Link legalGuardianLink = repositoryEntityLinks.linkToItemResource(LegalGuardian.class,
            model.getContent().getGuardianshipId().getLegalGuardian().getId());
        model.add(legalGuardianLink);
        return model;
      }
    
    }
    
    $ curl "http://localhost:8080/guardianships/1_2"
    {
      "name" : "Cool father",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/guardianships/1_2"
        },
        "guardianship" : {
          "href" : "http://localhost:8080/guardianships/1_2"
        },
        "student" : {
          "href" : "http://localhost:8080/students/1"
        },
        "legalGuardian" : {
          "href" : "http://localhost:8080/legalGuardians/2"
        }
      }
    }
    

    Pro:

    • matches the desired response representation exactly

    Con:

    • more association classes leads to more implementations of RepresentationModelProcessor doing more or less the same

    2. Configure the RepositoryRestConfiguration to expose ID's

    By default, ID's are not exposed by Spring Data Rest and although the topic is about embedded id's, these are also ID's. This behavior is configurable class by class.

    @Configuration
    public class RepositoryConfig implements RepositoryRestConfigurer {
    
      @Override
      public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(Guardianship.class);
      }
      
    }
    
    $ curl "http://localhost:8080/guardianships/1_2"
    {
      "guardianshipId" : {
        "_links" : {
          "student" : {
            "href" : "http://localhost:8080/students/1"
          },
          "legalGuardian" : {
            "href" : "http://localhost:8080/legalGuardians/2"
          }
        }
      },
      "name" : "Cool father",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/guardianships/1_2"
        },
        "guardianship" : {
          "href" : "http://localhost:8080/guardianships/1_2"
        }
      }
    }
    

    Pro:

    • less "implementation"

    Con:

    • (in this form) does not match the original desired response representation exactly (see the guardianshipId-wrapper around the links)

    Edit

    For the way two: To expose all ID's for entities, that are using embedded (composite) ids something like the following is possible:

    @Configuration                                                                                
    public class RepositoryRestConfig implements RepositoryRestConfigurer {                       
                                                                                                  
      @Autowired                                                                                  
      Repositories repositories;                                                                  
                                                                                                  
      @Override                                                                                   
      public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {      
        repositories.forEach(repository -> {                                                      
          Field embeddedIdField =                                                                 
              ReflectionUtils.findField(repository, new AnnotationFieldFilter(EmbeddedId.class)); 
          if (embeddedIdField != null) {                                                          
            config.exposeIdsFor(repository);                                                      
          }                                                                                       
        });                                                                                       
      }                                                                                           
                                                                                                  
    }