1

Hibernate 4.3.11

I have an issue saving the following object graph in hibernate. The Employer is being saved using the merge() method.

Employer 
   |_ List<EmployerProducts> employerProductsList; 
         |_ List<EmployerProductsPlan> employerProductsPlan;

The Employer & EmployerProducts have a auto generated pk. The EmployerProductsPlan is a composite key consisting of the EmployerProducts id and a String with the plan code.

The error occurs when there is a transient object in the EmployerProducts list that cascades to List<EmployerProductsPlan>. The 1st error that I encountered which I have been trying to get past was an internal hibernate NPE. This post here perfectly describes the issue that I am having which causes the null pointer Hibernate NullPointer on INSERTED id when persisting three levels using @Embeddable and cascade

The OP left a comment specifying what they did to resolve, but I end up with a different error when changing to the suggested mapping. After changing the mapping, I am now getting

org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.webexchange.model.EmployerProductsPlan#com.webexchange.model.EmployerProductsPlanId@c733f9bd]

Due to other library dependencies, I cannot upgrade above 4.3.x at this time. This project is using spring-boot-starter-data-jpa 1.3.3. No other work is being performed on the session other than calling merge() and passing the employer object.

Below is the mappings for each class:

Employer

@Entity
@Table(name = "employer")
@lombok.Getter
@lombok.Setter
@lombok.EqualsAndHashCode(of = {"employerNo"})
public class Employer implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "EMPLOYER_NO", unique = true, nullable = false)
    private Long employerNo;

     .....


    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employer", orphanRemoval = true)
    private List<EmployerProducts> employerProductsList = new ArrayList<>(0);
}

EmployerProducts

@Entity
@Table(name = "employer_products")
@Accessors(chain = true) // has to come before @Getter and @Setter
@lombok.Getter
@lombok.Setter
@lombok.EqualsAndHashCode(of = {"employerProductsNo"})

public class EmployerProducts implements Serializable {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "employer_products_no", unique = true, nullable = false)
    private Long employerProductsNo;

    @ManyToOne
    @JoinColumn(name = "employer_no", nullable = false)
    private Employer employer;

    ......

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "employerProducts", orphanRemoval = true)
    private List<EmployerProductsPlan> employerProductsPlanList = new ArrayList<>(0);
}

EmployerProductsPlan

@Accessors(chain = true) // has to come before @Getter and @Setter
@lombok.Getter
@lombok.Setter
@lombok.EqualsAndHashCode(of = {"id"})
@Entity
@Table(name="employer_products_plan")
public class EmployerProductsPlan implements Serializable {

    @EmbeddedId
    @AttributeOverrides({ @AttributeOverride(name = "plan", column = @Column(name = "epp_plan", nullable = false)),
            @AttributeOverride(name = "employerProductsNo", column = @Column(name = "employer_products_no", nullable = false)) })
    private EmployerProductsPlanId id;

    @ManyToOne
    @JoinColumn(name = "employer_products_no")
    @MapsId("employerProductsNo")
    private EmployerProducts employerProducts;

}

I am populating the employerProducts above with the same instance of the EmployerProducts object that is being saved. It is transient and has no id populated as it does not existing in the db yet.

EmployerProductsPlanId

@Accessors(chain = true) // has to come before @Getter and @Setter
@lombok.Getter
@lombok.Setter
@lombok.EqualsAndHashCode(of = {"plan", "employerProductsNo"})
@Embeddable
public class EmployerProductsPlanId implements Serializable {

    private String plan;

    private Long employerProductsNo;

   // This was my previous mapping that was causing the internal NPE in hibernate
   /* @ManyToOne
    @JoinColumn(name = "employer_products_no")
    private EmployerProducts employerProducts;*/
}

UPDATE: Showing struts controller and dao. The Employer object is never loaded from the db prior to the save. Struts is creating this entire object graph from the Http request parameters.

Struts 2.5 controller

@lombok.Getter
@lombok.Setter
public class EditEmployers extends ActionHelper implements Preparable {

    @Autowired
    @lombok.Getter(AccessLevel.NONE)
    @lombok.Setter(AccessLevel.NONE)
    private IEmployerDao employerDao;

    private Employer entity;

    ....

    public String save() {

        beforeSave();

        boolean newRecord = getEntity().getEmployerNo() == null || getEntity().getEmployerNo() == 0;
        Employer savedEmployer = newRecord ?
                employerDao.create(getEntity()) :
                employerDao.update(getEntity());

        setEntity(savedEmployer);

        return "success";
    }


    private void beforeSave() {
        Employer emp = getEntity();

        // associate this employer record with any products attached
        for (EmployerProducts employerProduct : emp.getEmployerProductsList()) {
            employerProduct.setEmployer(emp);

            employerProduct.getEmployerProductsPlanList().forEach(x ->
                    x.setEmployerProducts(employerProduct));
        }

        // check to see if branding needs to be NULL.  It will create the object from the select parameter with no id
        //  if a branding record has not been selected
        if (emp.getBranding() != null && emp.getBranding().getBrandingNo() == null) {
            emp.setBranding(null);
        }
    }



}

Employer DAO

@Repository
@Transactional
@Service
@Log4j
public class EmployerDao  extends WebexchangeBaseDao implements IEmployerDao  {

    private Criteria criteria() {
        return getCurrentSession().createCriteria(Employer.class);
    }

    @Override
    @Transactional(readOnly = true)
    public Employer read(Serializable id) {
        return (Employer)getCurrentSession().load(Employer.class, id);
    }

    @Override
    public Employer create(Employer employer) {
        getCurrentSession().persist(employer);

        return employer;
    }

    @Override
    public Employer update(Employer employer) {

        getCurrentSession().merge(employer);

        return employer;
    }


}
Paul Zepernick
  • 1,452
  • 1
  • 11
  • 25
  • show the entire transactional method where you fetch / create and then merge – Maciej Kowalski Feb 04 '19 at 13:28
  • @MaciejKowalski I added in the code for the Struts controller and dao class. The update() method from the dao is what is being fired. – Paul Zepernick Feb 04 '19 at 13:46
  • Questions: when and where `plan` of `EmployeeProductsPlan` is being setup? Where and how their embededId object is being created? – Carlitos Way Feb 04 '19 at 14:05
  • Other suggestion (this is backup plan): why don't you use a genenerated pk for your plan entities and use the productsId and plan string like a unique search key? – Carlitos Way Feb 04 '19 at 14:07
  • @CarlitosWay The plan of the EmployerProductsPlan is being created by Struts. The inputs are mapped to it and Struts is creating the initial object. The beforeSave() method in the controller is populating the employerProduct object from the parent before the merge happens – Paul Zepernick Feb 04 '19 at 14:28
  • The error is only happening when trying to insert a new EmployerProducts and new EmployerProductsPlan. If the EmployerProducts already exists it will create the new EmployerProductsPlan under it just fine. – Paul Zepernick Feb 04 '19 at 14:30

1 Answers1

0

As of right now, my solution is to loop through the EmployerProducts and check for new records. I called a persist on the new ones before calling the merge() on the parent Employer. I also moved the logic I had associating all the keys into the dao instead of having it in my Struts action. Below is what my update() method in the Employer DAO now looks like

public Employer update(Employer employer) {


    // associate this employer record with any products attached
    for (EmployerProducts employerProduct : employer.getEmployerProductsList()) {
        employerProduct.setEmployer(employer);

        if (employerProduct.getEmployerProductsNo() == null) {
            // The cascade down to employerProductsPlanList has issues getting the employerProductsNo
            // automatically if the employerProduct does not exists yet.  Persist the new employer product
            // before we try to insert the new composite key in the plan
            // https://stackoverflow.com/questions/54517061/hibernate-4-3-cascade-merge-through-multiple-lists-with-embeded-id
            List<EmployerProductsPlan> plansToBeSaved = employerProduct.getEmployerProductsPlanList();
            employerProduct.setEmployerProductsPlanList(new ArrayList<>());
            getCurrentSession().persist(employerProduct);

            // add the plans back in
            employerProduct.setEmployerProductsPlanList(plansToBeSaved);
        }

        // associate the plan with the employer product
        employerProduct.getEmployerProductsPlanList().forEach(x ->
                    x.getId().setEmployerProductsNo(employerProduct.getEmployerProductsNo())
        );

    }


    return (Employer)getCurrentSession().merge(employer);
}
Paul Zepernick
  • 1,452
  • 1
  • 11
  • 25