0

following situation. I have three classes (CV=Curriculum Vitae, ProfessionalExperience and Industry). My REST Controller receives a PUT request for updating an existing CV. JSON looks something like this:

{
"id": 102,
"currentSalery":100,
"desiredSalery":120,
"professionalExperiences": [
    { "jobTitle" : "Technical Project Lead", "company" : "Example Company 1", 
        "monthStart" : 10, "yearStart" : 2008, "monthEnd" : 11, "yearEnd" : 2017, "industry" : {"id" : 1001, "name" : "IT"}
    },
    { "jobTitle" : "Software Consultant", "company" : "Example Company 2", 
        "monthStart" : 11, "yearStart" : 2017, "industry" : {"name" : "Sales"}
    }
]}

The relationships between CV and ProfessionalExperience is 1:n. The relationship between ProfessionalExperience and Industry is n:1. Note that the JSON can have already existing Industry Objects as references (here IT) or new ones (here Sales).

Here (I hope) all important parts of the ProfessionalExperience and Industry class

ProfessionalExperience:

@Entity
public class ProfessionalExperience  implements Serializable {

@Id
@SequenceGenerator(name="prof_seq", initialValue=1, allocationSize=100)
@GeneratedValue(strategy= GenerationType.SEQUENCE, generator="prof_seq")
private Long id;

@ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
@JoinColumn(name = "industry_id")
private Industry industry;

@JsonIgnore
@ManyToOne
@JoinColumn(name = "cv_id")
private CV cv;

public Industry getIndustry() {
    return industry;
}

/**
 * Set new industry. The method keeps relationships consistency
 * * this professionalExperience is removed from the previous industry
 * * this professionalExperience is added to next industry
 *
 * @param industry
 */
public void setIndustry(Industry industry) {
    //prevent endless loop
    if (sameAsFormer(industry))
        return ;
    //set new industry
    Industry oldIndustry = this.industry;
    this.industry = industry;
    //remove from the industry
    if (oldIndustry!=null)
        oldIndustry.removeProfessionalExperience(this);
    //set myself into industry
    if (industry!=null)
        industry.addProfessionalExperience(this);
}


private boolean sameAsFormer(Industry newIndustry) {
    return industry==null? newIndustry == null : industry.equals(newIndustry);
}



}

I implemented the setter as mentioned in JPA/Hibernate: detached entity passed to persist but with no success.

Industry:

@Entity
public class Industry  implements Serializable {

@Id
@SequenceGenerator(name="industry_seq", initialValue=1, allocationSize=100)
@GeneratedValue(strategy= GenerationType.SEQUENCE, generator="industry_seq")
private Long id;

 // REMOVED cascade = CascadeType.ALL for testing
@JsonIgnore
@OneToMany(mappedBy = "industry")
private List<ProfessionalExperience> professionalExperiences = new ArrayList<>();

/**
 * Returns a collection with owned professional experiences. The
 * returned collection is a defensive copy.
 *
 * @return a collection with owned professional experiences
 */
public List<ProfessionalExperience> getProfessionalExperiences() {
    return new ArrayList<ProfessionalExperience>(professionalExperiences);
}

/**
 * Add new professionalExperience to the industry. The method keeps
 * relationships consistency:
 */
public void addProfessionalExperience(ProfessionalExperience professionalExperience) {
    // prevent endless loop
    if(professionalExperiences.contains(professionalExperience))
        return;

    professionalExperiences.add(professionalExperience);
    professionalExperience.setIndustry(this);
}

/**
 * Removes the professionalExperience from the industry. The method keeps
 * relationships consistency:
 */
public void removeProfessionalExperience (ProfessionalExperience professionalExperience) {
    // prevent endless loop
    if(!professionalExperiences.contains(professionalExperience))
        return;

    professionalExperiences.remove(professionalExperience);
    professionalExperience.setIndustry(null);
}   }

I played around with different combinations of CascadeTypes on ProfessionalExperience an Industry side, but never got the right combination. If i remove the CascadeType from Industry I just get

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: x.y.z.Industry

With CascadeType.ALL on both sides it depends on the Industry objects I want to save. If there are all new (or no duplicates), then it is working. But if two ProfessionalExperiences are referencing the same Industry object i get this:

Caused by: java.lang.IllegalStateException: Multiple representations of the same entity [x.y.z.Industry#303] are being merged

Can someone help me out? Thans in advance!

Bobbycar
  • 3
  • 2

2 Answers2

0

You have made the relationships a little complicated :) Although relationships will depend on your use cases but still i recommend to have relationships like this:

  • CV to Professional Experience: 1-to-n bidirectional (which you already have)
  • Professional Experience to Industry: n-to-1 (you mentioned that you have this relationship but your code shows that you have 1-to-n bidirectional)

Also make industry, meta data i.e. it will have it's own lifecycle (you need to save it before using it)

Also if you need all the Professional Experiences by Industry then you can get them by firing additional query, something like findAllByIndustry(Industry industry).

P.S. enable the secondary cache. it will share the performance burden with database.

naren
  • 301
  • 4
  • 8
  • Thanks for your answer. I think in the end the Industries will be a prefilled set of values, so I can avoid to save new values at CV creation. – Bobbycar May 30 '18 at 09:59
  • But I don't understand, why I have a 1-to-n bidirectional relationship between ProfessionalExperience and Industry in my code. Maybe you can explain this a little bit more. In ProfessionalExperience the industry is annotated with ManyToOne. – Bobbycar May 30 '18 at 10:02
  • you have the following code: `@JsonIgnore @OneToMany(mappedBy = "industry") private List professionalExperiences = new ArrayList<>();` and within industry you have **mappedBy** which makes this relationship bidirectional. As you need industry to be a prefilled set of values, this is an additional reason you should make industry meta data. Fetch the industries -> show in the drop down where user can select one of them. If user wants to create a new one from the same form, then you can create the industry first and then save that in the Professional Experience object. – naren May 30 '18 at 10:21
0

Finally fixed it. I made two errors

  1. I completely removed the CascadaType from ProfessionalExperience (just left ManyToOne) and added CascadeType.ALL to Industry again
  2. My autogenerated equals Method in ProfessionalExperience was incorrect:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ProfessionalExperience)) return false;
    
        ProfessionalExperience that = (ProfessionalExperience) o;
    
        return id != null ? id.equals(that.id) : that.id == null;
    }
    

This returns true, if we compare two unsaved ProfessionalExperiences, so my contains in addProfessionalExperience was not working as expected. I just changed last line to return id != null ? id.equals(that.id) : that.id != null which workes fine.

This finally leaves me with the limitation, that I cannot assign unsaved industries, but Narender Singh already mentioned how to handle this.

Bobbycar
  • 3
  • 2