Using Hibernate Envers with a @OneToMany association for a List property that uses @OrderColumn requires a little extra glue. Without this extra glue, Envers will throw a NullPointerException in a lazy initializer when you try to load revisions.
Suppose our domain model has a Sponsor entity. A Sponsor can have multiple business names, represented by a Sponsor Name entity. The names are ordered by preference, so we use a @OneToMany with a List. Here’s how we might map the Sponsor entity.
[java]
@Entity
@Audited
@Access(AccessType.FIELD)
public class SponsorEntity {
@OneToMany(mappedBy = “sponsor”, fetch = FetchType.LAZY, orphanRemoval = true)
@OrderColumn(name = “name_index”)
private List
}
[/java]
And here’s the SponsorNameEntity.
[java]
@Entity
@Audited
@Access(AccessType.FIELD)
public class SponsorNameEntity {
@Column(nullable = false)
private String name;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private SponsorEntity sponsor;
}
[/java]
If we use these entities with Hibernate Envers, we’ll get an NPE when we try to load revisions of the SponsorEntity. It happens because the name_index column specified by the @OrderColumn annotation is not included in the audit table for SponsorNameEntity.
We can fix this easily, with an additional field on SponsorNameEntity and an extra annotation on SponsorEntity. The extra field on SponsorNameEntity is used to expose the value of the order column as a field:
[java]
@Entity
@Audited
@Access(AccessType.FIELD)
public class SponsorNameEntity {
// This field is used to capture the value of the column named
// in the @OrderColumn annotation on the referencing entity.
@Column(insertable = false, updatable = false)
private int name_index;
@Column(nullable = false)
private String name;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private SponsorEntity sponsor;
}
[/java]
In SponsorEntity we use an additional annotation to inform Envers about the field that contains the order column value:
[java]
@Entity
@Audited
@Access(AccessType.FIELD)
public class SponsorEntity {
@OneToMany(mappedBy = “sponsor”, fetch = FetchType.LAZY, orphanRemoval = true)
@OrderColumn(name = “name_index”)
@AuditMappedBy(mappedBy = “sponsor”, positionMappedBy = “name_index”)
private List
}
[/java]
The positionMappedBy attribute of the @AuditMappedBy annotation informs Envers that the position of each entry in the list is given by the value of the name_index field that we added to SponsorNameEntity. It seems a little redundant, but we’re required to also specify the value of the mappedBy attribute, which should be the same as the value given in the @OneToMany annotation.
If you discover that you need this fix after you’ve already got some revisions out there in your audit tables, don’t forget to
- Add the column specified by
@OrderColumnto the appropriate audit table. In our example, this column goes into the audit table forSponsorNameEntity. - Initialize the column value in each of the existing rows. You can usually query the table associated with the entity to get the appropriate index value — e.g. in our case we’d query the table for
SponsorNameEntityto get thename_indexcolumn value and use it to update the corresponding rows in the audit table.