Use a projection to modify only one field but keeping default for all others - spring-data-rest

First of all I'd like to say I love what i've seen so far from Spring Data JPA and Spring Data REST. Thanks a lot to all people involved.
Problem description
I have an entity model similar to the classes below. One parent and two different child entities referencing the parent als a ManyToOne Assoziation. For one of the childs i like to have the default rendering of all its properites and links as it is when no projection is applied to the parent.
The other child should be mapped to a simple string array containing only the id or some specific field.
Code and example JSONs
#Entity
public class Parent {
#Id
private Long id;
private String parentValue;
#OneToMany(mappedBy = "parent")
private List<Child1> child1;
#OneToMany(mappedBy = "parent")
private List<Child2> child2;
// ... getters and setters
}
#Entity
public class Child1 {
#Id
private Long id;
private String child1Value;
#ManyToOne
Parent parent;
// ... getters and setters
}
#Entity
public class Child2 {
#Id
private Long id;
#ManyToOne
Parent parent;
}
the response when getting the collection resource of parent is this
{
"_embedded": {
"parents": [
{
"parentValue": "Parent1",
"child1": [
{
"child1Value": "Child1",
"_links": {
"parent": {
"href": "http://localhost:8080/parents/1"
}
}
}
],
"child2": [
{
"_links": {
"parent": {
"href": "http://localhost:8080/parents/1"
}
}
}
],
// removed remaining json to shorten the example
}
But what i like to achieve is the following JSON
{
"_embedded": {
"parents": [
{
"parentValue": "Parent1",
"child1": [
{
"child1Value": "Child1",
"_links": {
"parent": {
"href": "http://localhost:8080/parents/1"
}
}
}
],
"child2": [1],
What i tried so far
Added an excerptProjection to the ParentRepository:
#RepositoryRestResource(excerptProjection = ParentRepository.ArrayProjection.class)
public interface ParentRepository extends PagingAndSortingRepository<Parent, Long>{
public interface ArrayProjection {
String getParentValue();
List<Child1> getChild1();
#Value("#{target.child2.![id]}")
List<Long> getChild2();
}
}
Edited: In the first version of the question, the Projection was incorrect regarding the return type of getChild1(), as it should return the complete collection not only one element. Thanks #kevvvvyp for still trying to help.
The result is similar to what i want, but the links on the Child1 property are missing now:
{
"_embedded": {
"parents": [
{
"child2": [
2
],
"child1": {
"child1Value": "Child1"
},
"parentValue": "Parent1",
// removed remaining json to shorten example
Also the approach with the excerptProjection means i'd have to change the projection everytime the entity changes. Which probalby won't happen to much but that means somebody will forget to change it in the future ;-)

I think a projection is the right way to go here, what about...
interface ArrayProjection {
#JsonIgnore
#Value("#{target}")
Parent getParent();
default String getParentValue(){
return this.getParent().getParentValue();
}
default Child1 getChild1(){
//TODO what are we actually trying to return here? Given we join the parent to
// the child on id & id is the primary key on both entities can we ever get more than one child?
return CollectionUtils.firstElement(this.getParent().getChild1());
}
default List<Long> getChild2() {
return this.getParent().getChild2().stream()
.map(Child2::getId)
.collect(Collectors.toList());
}
}
Response...
GET http://localhost:8080/api/parents
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 31 Mar 2021 21:08:54 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"_embedded": {
"parents": [
{
"parentValue": "Parent1",
"child1": {
"child1Value": "Child1"
},
"child2": [
1
],
"_links": {
"self": {
"href": "http://localhost:8080/api/parents/1"
},
"parent": {
"href": "http://localhost:8080/api/parents/1{?projection}",
"templated": true
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/parents"
},
"profile": {
"href": "http://localhost:8080/api/profile/parents"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
Response code: 200; Time: 713ms; Content length: 724 bytes
In reponse to your concern, if we code using default method's we will see a compile error when that entity changes. It might be possible to use a class projection instead.
Also I would consider if we really want to return a projection by default... it might confuse a client who then tries to create/update a parent (this is perhaps why you've kept the id's hidden by default?).
Sample - https://github.com/kevvvvyp/sdr-sample
Update
Another (more complex) way to acheive this could be to make use of Jackson's SimpleBeanPropertyFilter, you could customise the JSON response based on some value (the entity class, a value in a particular field, an annotation etc).

Related

Jackson module to handle abstract aggregate root and its subclasses in Spring Data REST

I have Spring Data REST based application with repository
public interface CriterionRepository extends JpaRepository<Criterion, Long> {
}
whereas Criterion is base class:
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Criterion extends AbstractEntity {}
and NameCriterion is its subclass
#Entity
public class NameCriterion extends Criterion {
private final String name;
}
Spring Data REST exports the repository as REST resource and one can access it at http://localhost:8080/api/criteria/
Exported resource looks as follows:
{
"_embedded": {
"nameCriteria": [{
"_links": {
"self": {
"href": "http://localhost:8080/api/nameCriterion/1"
},
"nameCriterion": {
"href": "http://localhost:8080/api/nameCriterion/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/criteria"
},
"profile": {
"href": "http://localhost:8080/api/profile/criteria"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
When I try to follow self link, there is no mapping for http://localhost:8080/api/nameCriterion/1
I can follow http://localhost:8080/api/criteria/1 though and I get response without name field from NameCriterion
{
"_links": {
"self": {
"href": "http://localhost:8080/api/nameCriterion/1"
},
"nameCriterion": {
"href": "http://localhost:8080/api/nameCriterion/1"
}
}
}
My assumption is it is a problem with Jackson mapper defined in REST exporter which is not tweaked correctly to handle abstract class Criterion used in JpaRepository as aggregate root.
What Jackson customization should I apply to make it working properly?
In other words, what Jackson module should I create?
There is no need to create a Jackson module. To use a single table for inherited entities we can use #RestResource annotation to mark them as the same resources:
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#Table(name = "criteria")
public abstract class Criterion extends AbstractEntity {
}
#RestResource(rel = "criteria", path = "criteria")
#Entity
public class NameCriterion extends Criterion {
private String name;
}
#RestResource(rel = "criteria", path = "criteria")
#Entity
public class TitleCriterion extends Criterion {
private String title;
}
#RepositoryRestResource(path = "criteria", collectionResourceRel = "criteria", itemResourceRel = "criterion")
public interface CriterionRepository extends JpaRepository<Criterion, Long> {
}
So it becomes possible to obtain all the resources (NameCriterion and TitleCriterion) in one output:
GET http://localhost:8080/api/criteria
{
"_embedded": {
"criteria": [
{
"name": "name1",
"_links": {
"self": {
"href": "http://localhost:8080/api/criteria/1"
},
"nameCriterion": {
"href": "http://localhost:8080/api/criteria/1"
}
}
},
{
"title": "title1",
"_links": {
"self": {
"href": "http://localhost:8080/api/criteria/2"
},
"titleCriterion": {
"href": "http://localhost:8080/api/criteria/2"
}
}
}
]
}
}
GET http://localhost:8080/api/criteria/1
{
"name": "name1",
"_links": {
"self": {
"href": "http://localhost:8080/api/criteria/1"
},
"nameCriterion": {
"href": "http://localhost:8080/api/criteria/1"
}
}
}
GET http://localhost:8080/api/criteria/2
{
"title": "title1",
"_links": {
"self": {
"href": "http://localhost:8080/api/criteria/2"
},
"titleCriterion": {
"href": "http://localhost:8080/api/criteria/2"
}
}
}
Working example.

How can I return child objects in a many-to-many relationship with Dropwizard/Hibernate/Swagger without recursion?

I'm using Dropwizard and Swagger to create a REST API to manage access to various objects. Two of these objects are involved in a many-to-many relationship, for example:
public class Puppy implements Serializable {
private Long id;
private String name;
#ManyToMany(targetEntity = Trick.class)
#JoinTable(
name="puppies_tricks",
joinColumns=#JoinColumn(name="puppy_id"),
inverseJoinColumns=#JoinColumn(name="trick_id"))
private List<Trick> tricks;
#JsonProperty("tricks")
public List<Trick> getTricks() { return this.tricks; }
...
}
public class Trick implements Serializable {
private Long id;
private String name;
#ManyToMany(targetEntity = Puppy.class)
#JoinTable(
name="puppies_tricks",
joinColumns=#JoinColumn(name="trick_id"),
inverseJoinColumns=#JoinColumn(name="puppy_id"))
private List<Puppy> puppies;
#JsonProperty("puppies")
public List<Puppy> getPuppies() { return this.puppies; }
...
}
And suppose the actual data looks something like:
# Tricks:
[{ id: 1, name: 'Roll over' },
{ id: 2, name: 'Play dead' },
{ id: 3, name: 'Steal second' }]
# Puppies:
[{ id: 1, name: 'Flopsy' },
{ id: 2, name: 'Mopsy' },
{ id: 3, name: 'Cottontail' }]
# Puppies_Tricks
[{ puppy_id: 1, trick_id: 1 },
{ puppy_id: 1, trick_id: 2 },
{ puppy_id: 2, trick_id: 2 },
{ puppy_id: 2, trick_id: 3 }]
So when I GET /puppy/1 I want to get a structure like:
{
"id": 1,
"name": "Flopsy",
"tricks": [
{ "id": 1, "name": "Roll over"},
{ "id": 2, "name": "Play dead" } ]
}
but what I'm actually getting is a nested array:
{
"id": 1,
"name": "Flopsy",
"tricks": [
{ "id": 1, "name": "Roll over", "puppies": [ { "id": 1, "name": "Flopsy" }, ...},
{ "id": 2, "name": "Play dead", "puppies": [...] } ]
}
Is there some way to tell Jackson/Dropwizard to stop descending through tricks to when getting puppies, and vice versa?
Dropwizard 1.0.6, Java 1.8.
Jackson has support of object-graph serialization through the #JsonManagedReference #JsonBackReference annotations. See the documentation here. You can find a simple example here.
You should really read more carefully the documentation of the tools you're using.
There is already an answer that summarizes what approaches are available.
#JsonIgnore
#JsonProperty("puppies")
public List<Puppy> getPuppies() { return this.puppies; }
will stop infinite loop
My solution here was to create a separate PuppyCore class that implements serialization but isn't persisted. This includes basic Puppy info (id, name). Similarly a TrickCore has id and name only.
Then Puppy includes Set tricks, with this getter:
public Set<TrickCore> getTricks();
which returns a list of TrickCore objects – these don't have the Puppy list, and so halts the recursion. A similar pattern on Trick returns Set<PuppyCore> for getPuppies().

How to specify which fields to return with #query anotation in Spring Data?

When I run the following query using the #query anotation in Spring Boot, it returns the correct result:
SELECT p FROM Collection p WHERE LOWER(p.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))
{
"_embedded": {
"collections": [
{
"place": "Blessington",
"description": "Collection of old shoes for recycling",
"_links": {
"self": {
"href": "http://localhost:8080/collections/1"
},
"collection": {
"href": "http://localhost:8080/collections/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/collections/search/findByDescription?searchTerm=shoe"
}
}
}
When I try to specify the fields to return:
SELECT p.description FROM Collection p WHERE LOWER(p.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))
I get the following error:
{
"cause": null,
"message": "PersistentEntity must not be null!"
}
How do I specify which fields to return with #query annotation in Spring Data?
Yes it seems the question that Manish posted the link to has the answer.
Answer: You can't.
Spring data will return the whole entity, not individual fields. You can't make it do that. If you want to do that you have to use projections. See linked post.
Thanks #Manish
The suggested link does not fully cover all possibilities.
From the Hopper release of Spring Data it is possible to directly return Projections directly from Query methods:
https://spring.io/blog/2016/05/03/what-s-new-in-spring-data-hopper#projections-on-repository-query-methods
So you can therefore do:
public interface ThingRepository extends JpaRepository<Thing, Long>{
#Query("select t from Thing t .....")
public List<ThingProjection> findBySomeCriteria(...);
}

Generate links for collection resources for a specific single resource

I wrote a custom controller to handle a GET http://localhost:54000/api/v1/portfolios/{id}/evaluate request.
#RequestMapping(value = "/portfolios/{id}/evaluate", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> evaluate(#PathVariable Long id) {
Portfolio portfolio = portfolioService.evaluate(id);
if (portfolio == null) {
return ResponseEntity.notFound().build();
}
Resource<Portfolio> resource = new Resource<>(portfolio);
resource.add(entityLinks.linkForSingleResource(Portfolio.class, id).withSelfRel());
return ResponseEntity.ok(resource);
}
The current response is
{
"summary" : {
"count" : 24.166666666666668,
"yield" : 0.14921630094043895,
"minBankroll" : -6.090909090909091,
"sharpeRatio" : 0.7120933654645042,
"worstReturn" : -2.4545454545454533,
"losingSeason" : 3,
"return" : 3.6060606060606077
},
"_links" : {
"self" : {
"href" : "http://localhost:54000/api/v1/portfolios/4"
}
}
}
but I would like to add collection resources (summaries and systems) linked to that portfolio:
{
"summary": {
"count": 24.166666666666668,
"yield": 0.14921630094043895,
"minBankroll": -6.090909090909091,
"sharpeRatio": 0.7120933654645042,
"worstReturn": -2.4545454545454533,
"losingSeason": 3,
"return": 3.6060606060606077
},
"_links": {
"self": {
"href": "http://localhost:54000/api/v1/portfolios/4"
},
"portfolio": {
"href": "http://localhost:54000/api/v1/portfolios/4"
},
"summaries": {
"href": "http://localhost:54000/api/v1/portfolios/4/summaries"
},
"systems": {
"href": "http://localhost:54000/api/v1/portfolios/4/systems"
}
}
}
I did not find a way to generate those links with the RepositoryEntityLinks entityLinks object
You can always do something like this:
entityLinks.linkForSingleResource(Portfolio.class, id).slash("systems").withRel("systems");
And if your systems endpoint is implemented in a custom controller method you can use the ControllerLinkBuilder to generate a link to your controller method. Lets say you implemented the getSystems method with id parameter in MyControllerClass - then you can generate the link like this (linkTo and methodOn are static methods in ControllerLinkBuilder):
linkTo(methodOn(MyControllerClass.class).getSystems(id)).withRel("systems");

Jackson Polymorphism and #JsonTypeInfo usage

Based on the following JSON:
{"items": [
{
"post_id": 17168289,
"count": 190
}
]}
And:
{"items": [
{
"tag_id": 17168289,
"count": 190
}
]}
I want to create a Tag object and a Post object that both extend an Item object.
How can I use the #JsonTypeInfo annotation to pick up the name of the *_id field so Jackson creates the correct object?
Here's where I am so far:
#JsonSubTypes({
#JsonSubTypes.Type(value=Post.class, name="post_id")
#JsonSubTypes.Type(value=Tag.class, name="tag_id")
})
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
public class Item {
public int count;
}