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;
}
}