When you have a bi-directional relationship between entities, you get the good old Jackson infinite recursion. JHipster uses @JSonIgnore to avoid this. But this can be improved with @JsonIdentityInfo
Let's say you have a bidirectional OneToMany relationship between SponsorAgreement and SponsorAgreementState. JHipster generates the following code:
@Entity
public class SponsorAgreement implements Serializable {
@OneToMany(mappedBy = "sponsorAgreement")
@JsonIgnore
private Set<SponsorAgreementState> states = new HashSet<>();
The problem with this `@JsonIgnore`, is that if you explicitelly create a query that returns all the states for a sponsor agreement with a join (`@Query("select sponsor_agreement from SponsorAgreement sponsor_agreement left join fetch sponsor_agreement.states")`) it will always be ignored.
Getting rid of `@JsonIgnore` is doable (but not trivial). If you [read this blog](http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion), there are several solutions:
* `@JsonBackReference` and `@JsonManagedReference` but this implies too much changes in JHipster
* `@JsonIdentityInfo` works pretty well... but slight changes need to be done in the client UI (the components listing all the entities).
##### **Suggest a Fix**
The best way is to use `@JsonIdentityInfo`, but it needs some fixes. If you change the entities by adding `@JsonIdentityInfo` we get the following (notice that `@JSonIgnore` is gone):
@Entity
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class SponsorAgreement implements Serializable {
@OneToMany(mappedBy = "sponsorAgreement")
private Set<SponsorAgreementState> states = new HashSet<>();
@Entity
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class SponsorAgreementState implements Serializable {
With this code, all the CRUD operations keep on working, which is nice. The only problem, is when displaying the list of `SponsorAgreementState` (in component `sponsor-agreement-state.component.html`). When you do a GET to get all the states, the produced JSon will look like this:
[ {
"id" : 1001,
"status" : "CREATED",
"statusDate" : "2017-12-01T09:00:00Z",
"sponsorAgreement" : {
"id" : 951,
"vatRate" : 20.0,
"subtotal" : 20.0,
"total" : 20.0,
"states" : null
}
}, {
"id" : 1002,
"status" : "SENT",
"statusDate" : "2017-12-22T11:02:00Z",
"sponsorAgreement" : 951
}, {
"id" : 1003,
"status" : "ABANDONED",
"statusDate" : "2017-12-16T14:15:00Z",
"sponsorAgreement" : 951
} ]
As you can see, the first state is still linked to the sponsor agreement, but not the other two who only get an id to the sponsor agreement:
* 1001: sponsorAgreement is filled, so the sponsorAgreementState.sponsorAgreement?.id is displayed
* 1002 and 1003: Jackson broke the recursion, and just displays the id (`"sponsorAgreement" : 951`)
So there is a bit of work to be done in list components (`sponsor-agreement-state.component.html`), because you could either display `sponsorAgreementState.sponsorAgreement?.id` or just `sponsorAgreementState.sponsorAgreement`. But that's all.
WDYT of such improvement of adding `@JsonIdentityInfo` on all entities (or DTOs) but also changing the behaviour of the the list compoents ?
##### **Related issues**
* https://github.com/jhipster/generator-jhipster/issues/1569
* https://github.com/jhipster/generator-jhipster/issues/5855
* https://stackoverflow.com/questions/28111493/jhipster-one-to-many-relationship-cant-see-the-collection-on-client-side
##### **JHipster Version(s)**
[email protected] /Users/agoncal/Documents/Code/Temp/jhipster/bidirectjson
└── [email protected]
##### **JHipster configuration, a `.yo-rc.json` file generated in the root folder**
{
"generator-jhipster": {
"promptValues": {
"packageName": "org.agoncal.bidirectjson"
},
"jhipsterVersion": "4.11.1",
"baseName": "bidirectjson",
"packageName": "org.agoncal.bidirectjson",
"packageFolder": "org/agoncal/bidirectjson",
"serverPort": "8080",
"authenticationType": "jwt",
"hibernateCache": "ehcache",
"clusteredHttpSession": false,
"websocket": false,
"databaseType": "sql",
"devDatabaseType": "h2Memory",
"prodDatabaseType": "postgresql",
"searchEngine": false,
"messageBroker": false,
"serviceDiscoveryType": false,
"buildTool": "maven",
"enableSocialSignIn": false,
"enableSwaggerCodegen": false,
"jwtSecretKey": "replaced-by-jhipster-info",
"clientFramework": "angularX",
"useSass": false,
"clientPackageManager": "yarn",
"applicationType": "monolith",
"testFrameworks": [],
"jhiPrefix": "jhi",
"enableTranslation": false
}
}
entityName.json files generated in the .jhipster directory
JDL entity definitions
entity SponsorAgreement (sponsor_agreement) {
vatRate Float required,
subtotal Float required,
total Float required
}
entity SponsorAgreementState (sponsor_agreement_state) {
status SponsorAgreementStatus required,
statusDate Instant required
}
enum SponsorAgreementStatus {
CREATED,
SENT,
ABANDONED,
SIGNED
}
relationship OneToMany {
SponsorAgreement{states} to SponsorAgreementState{sponsorAgreement required}
}
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
git version 2.15.0
node: v8.9.0
npm: 5.5.1
bower: 1.8.0
gulp:
[12:40:09] CLI version 1.3.0
yeoman: 2.0.0
yarn: 1.3.2
Docker version 17.09.0-ce, build afdb6d4
docker-compose version 1.16.1, build 6d1ac21
What about this in SponsorAgreementState ?
@Entity
public class SponsorAgreementState implements Serializable {
@ManyToOne
@JsonIgnoreProperties("states") // instead of @JsonIgnore in SponsorAgreement
private SponsorAgreement sponsorAgreement;
when SponsorAgreementState is serialized (as a root), its sponsorAgreement is serialized as well but without its SponsorAgreementState states.
when SponsorAgreement is serialized (as a root), its SponsorAgreementState states are serialized and then go to 1.
@ctamisier Yes, that's a much easier and a much better solution !
Because JHipster only has bi-directional OneToMany, this approach is a must have.
I would rather get rid of the entity JSON serialization option - I mean, go only with DTOs. It's much simpler to generate case specific DTO - for example SponsorAgreementDetailsDTO which contains SponsorAgreementStateDTO, and SponsorAgreementStateDetailsDTO which contains SponsorAgreementDTO.
Basically a separate 'DetailDTO' and a 'only-simple-attributes-DTO', where the DetailsDTO contains the dtos of the releated entities.
I would use DTOs as well. (I suffered too much not using DTOs when the data model gets bigger and bigger and need to add serialization attributes everywhere).
If you have a JDL for this model you can see how it is generated using DTOs and mapstruct.
There won't be infinite loop because the @ManyToOne relation is defined by a Long id in the DTO (handled properly in mapstruct mapping definition)
@ctamisier @gzsombor yes, I agree, DTOs get rid of the infinite recursion... but then, it doesn't answer my needs: I just get the list of IDs of the childs (instead I want the list of childs).
I'm just saying that in my usecase (eg. having the entire Invoice with all its InvoiceLines), out of the box bidirectional OneToMany mapping is not doable with Entities, and not enough with DTOs. It's fine if you know it. One way to solve some of these problems depending on your usecase is having unidirectional OneToMany (which would work perfectly with Entities, and with a bit of work with DTOs).
Adding @JsonIgnoreProperties on the child would solve the problem for Entities.
I just have a look on this. The template impact is simple but the problem is on the model side (.jhipster/entity.json files).
The SponsorAgreementState doesn't contains the otherEntityRelationshipName
{
"relationshipType": "many-to-one",
"relationshipValidateRules": "required",
"relationshipName": "sponsorAgreement",
"otherEntityName": "sponsorAgreement",
"otherEntityField": "id"
}
At the opposite, the other entity contains
{
"relationshipType": "one-to-many",
"relationshipName": "states",
"otherEntityName": "sponsorAgreementState",
"otherEntityRelationshipName": "sponsorAgreement"
}
So I can add it in the model, but I think it must be validated by the core team.
I can do a small PR to show it. Because it's not big work in fact
it seems working but the behaviour is now different with ES.
ES continue to return null on sub list due to relation. And now hibernate return an empty list but in my test case states list is empty....
Is it what is expected with JsonIgnoreProperties ? Maybe I must keep the JsonIgnore on the One to Many side , no ? Because i think it load the the sub list with a findAll
@agoncal @ctamisier any return on this ?
Most helpful comment
What about this in SponsorAgreementState ?
when SponsorAgreementState is serialized (as a root), its sponsorAgreement is serialized as well but without its SponsorAgreementState states.
when SponsorAgreement is serialized (as a root), its SponsorAgreementState states are serialized and then go to 1.