Hibernate Envers, @OneToMany List, and @OrderColumn

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 names = new ArrayList<>();

}
[/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 names = new ArrayList<>();

}
[/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

  1. Add the column specified by @OrderColumn to the appropriate audit table. In our example, this column goes into the audit table for SponsorNameEntity.
  2. 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 the name_index column value and use it to update the corresponding rows in the audit table.