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
@OrderColumn
to 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
SponsorNameEntity
to get thename_index
column value and use it to update the corresponding rows in the audit table.