I have been trying to debug constraint problems with collections in Hibernate.
The problem happens when you try to add an additional child to an already existing parent. For example:
1. Run a batch job that creates a person with one role.
2. Run a second batch job that adds a second role to the same person.
When Hibernate persists I get this error, even though I can verify that the sponsorid has been set before calling:
entityManager.merge(person);
<ORA-01400: cannot insert NULL into ("PRS_ROLE_RECORDS"."SPONSOR_ID")>
There is a not null constraint on sponsor_id.
I added triggers to the tables that were having constraint problems so that any insert/update/delete could be logged to another table.
When I disabled the not null constraint on sponsor id (and sponsor_t) then the role was successfully added. When I looked at my log, I found the following:
prs_role_records INSERT Record_ID: 48243 Percent Time: 100 RoleId: 1257 Sponsorid: Sponsor Type: Id: 96303 source_sor_id: SRDB affiliation_date: 01-MAY-95 sor_person_id: 33196
prs_role_records UPDATE Record_ID: 48243 Percent Time: 100 RoleId: 1257 Sponsorid: 213 Sponsor Type: 29 Id: 96303 source_sor_id: SRDB affiliation_date: 01-MAY-95 sor_person_id: 33196
What you see is that Hibernate did a two step save. An initial insert with sponsorid null (and sponsor_t) . Followed by an update with sponsorid (and sponsor_t) filled in.
I do not see any reason for this given that the sponsorid (and sponsor_t) was already set before calling merge. I also don't see why it singled out sponsorid (and sponsor_t) when other values seem to be filled in on the insert statement. Could anybody help explain why the merge was done in a two step process and why it did not have the value of sponsor id (and sponsor_t) at the time of the insert?
Thanks for your help!
The code:
Person table:
Code:
@Table(name = "prs_sor_persons")
@org.hibernate.annotations.Table(appliesTo = "prs_sor_persons", indexes = {@Index(name = "SOR_PERSON_SOURCE_AND_ID_INDEX", columnNames = {"source_sor_id", "id"})})
public class JpaSorPersonImpl extends Entity implements SorPerson {
@Id
@Column(name = "record_id")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "prs_sor_persons_seq")
@SequenceGenerator(name = "prs_sor_persons_seq", sequenceName = "prs_sor_persons_seq", initialValue = 1, allocationSize = 50)
private Long recordId;
@Column(name = "id")
@Required(property = "person.sorId")
private String sorId;
@Column(name = "source_sor_id", nullable = false)
@NotNull
@Size(min = 1)
private String sourceSor;
@Column(name = "person_id")
private Long personId;
@Column(name = "date_of_birth", nullable = true)
@Temporal(TemporalType.DATE)
@Required(property = "person.dateOfBirth", message = "dateOfBirthRequiredMsg")
@Past
private Date dateOfBirth;
@Column(name = "gender", length = 1, nullable = true)
@Required(property = "person.gender", message = "{genderRequiredMsg}")
@Size(min = 1, max = 1, message = "genderRequiredMsg")
@Gender
private String gender;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "person", fetch = FetchType.EAGER, targetEntity = JpaSorNameImpl.class)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@Fetch(value = FetchMode.SUBSELECT)
@RequiredSize(property = "person.names")
@Valid
private List<SorName> names = new ArrayList<SorName>();
@Column(name = "ssn", nullable = true, length = 9)
@Required(property = "person.ssn")
private String ssn;
@OneToOne(cascade=CascadeType.ALL, mappedBy="person", fetch = FetchType.EAGER, targetEntity = JpaSorDisclosureSettingsImpl.class)
private JpaSorDisclosureSettingsImpl disclosureSettings;
[b] @OneToMany(cascade = CascadeType.ALL, mappedBy = "person", fetch = FetchType.EAGER, targetEntity = JpaSorRoleImpl.class)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@Fetch(value = FetchMode.SUBSELECT)
@Valid
private List<SorRole> roles = new ArrayList<SorRole>();[/b]
Role Table:
Code:
@javax.persistence.Entity(name = "sorRole")
@Table(name = "prs_role_records", uniqueConstraints = @UniqueConstraint(columnNames = {"source_sor_id", "id"}))
@org.hibernate.annotations.Table(appliesTo = "prs_role_records", indexes = @Index(name = "PRS_ROLE_SOR_PERSON_INDEX", columnNames = "sor_person_id"))
public class JpaSorRoleImpl extends Entity implements SorRole {
@Id
@Column(name = "record_id")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "prs_role_records_seq")
@SequenceGenerator(name = "prs_role_records_seq", sequenceName = "prs_role_records_seq", initialValue = 1, allocationSize = 50)
private Long recordId;
@Column(name = "id")
@NotNull
@Size(min = 1)
private String sorId;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "sorRole", fetch = FetchType.EAGER, targetEntity = JpaSorUrlImpl.class)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@RequiredSize(property = "role.urls")
@Valid
@Fetch(value = FetchMode.SUBSELECT)
private List<Url> urls = new ArrayList<Url>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "sorRole", fetch = FetchType.EAGER, targetEntity = JpaSorEmailAddressImpl.class)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@RequiredSize(property = "role.emailAddresses")
@Valid
@Fetch(value = FetchMode.SUBSELECT)
private List<EmailAddress> emailAddresses = new ArrayList<EmailAddress>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "sorRole", fetch = FetchType.EAGER, targetEntity = JpaSorPhoneImpl.class)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@RequiredSize(property = "role.phones")
@Valid
@Fetch(value = FetchMode.SUBSELECT)
private List<Phone> phones = new ArrayList<Phone>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "sorRole", fetch = FetchType.EAGER, targetEntity = JpaSorAddressImpl.class)
@org.hibernate.annotations.Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@RequiredSize(property = "role.addresses")
@Valid
@Fetch(value = FetchMode.SUBSELECT)
private List<Address> addresses = new ArrayList<Address>();
@Column(name = "source_sor_id", nullable = false)
@NotNull
@Size(min = 1)
private String sourceSorIdentifier;
@ManyToOne(optional = false)
@JoinColumn(name = "sor_person_id", nullable = false)
private JpaSorPersonImpl person;
@Column(name = "percent_time", nullable = false)
@DecimalMin(value = "0")
@DecimalMax(value = "100")
private int percentage;
@ManyToOne(optional = false)
@JoinColumn(name = "person_status_t")
@NotNull
@AllowedTypes(property = "role.personStatus")
private JpaTypeImpl personStatus;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "sorRole", fetch = FetchType.EAGER, targetEntity = JpaSorLeaveImpl.class)
@RequiredSize(property = "role.leaves")
@Valid
@Fetch(value = FetchMode.SUBSELECT)
private List<Leave> leaves = new ArrayList<Leave>();
@ManyToOne(optional = false)
@JoinColumn(name = "role_id")
@NotNull
private JpaRoleInfoImpl roleInfo;
[b] @Column(name="sponsor_id", nullable = false)
@NotNull
private Long sponsorId;
@ManyToOne(optional = false)
@JoinColumn(name="sponsor_t")
@NotNull
private JpaTypeImpl sponsorType;[/b]
@Column(name = "affiliation_date", nullable = false)
@Temporal(TemporalType.DATE)
@NotNull(message = "startDateRequiredMsg")
private Date start;
@Column(name = "termination_date")
@Temporal(TemporalType.DATE)
private Date end;
@ManyToOne()
@JoinColumn(name = "termination_t")
private JpaTypeImpl terminationReason;