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>
andGuardianshipRepository : JpaRepository<Guardianship, Guardianship.GuardianshipId>
To query single Guardianship
s 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.
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:
Con:
RepresentationModelProcessor
doing more or less the same2. 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:
Con:
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);
}
});
}
}