The strategy I'm taking to implementing a maker-checker scenario is through using multiple tables. Currently, I'm using Hibernate 4.2 (annotations). The following scenario is what I would like to achieve. However, I'm having problems with the multi-level inheritance.
The basic idea is that there are two tables (pending
and approved
). When an add()
occurs, the entry is inserted into the pending
table. When that entry is approved, it is removed from the pending
table and inserted into the approved
table.
Policy (the policy)
|
+ -- Pending (maker information)
|
+ -- Approved (checker information)
So, class Policy
is the class that defines the necessary fields for a policy. To keep this post shorter, the fields are not be shown.
@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // problem
public abstract class Policy { ... }
The Pending
class is for the newly-added Policy
that is awaiting approval and it has information on the maker/adder.
@Entity
@Table(name = "pending")
public class Pending extends Policy {
@Column(name = "adder", ...)
private String adder;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "time_added", ...)
private Date timeAdded;
}
The Approved
class is for approved entities and it contains additional information on the approver in addition to the information from the Pending
class.
@Entity
@Table(name = "approved")
public class Approved extends Pending {
@Column(name = "approver", ...)
private String approver;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "time_approved", ...)
private Date timeApproved;
}
My first thought was to try TABLE_PER_CLASS
. However, it resulted in the following runtime error: org.hibernate.MappingException: Cannot use identity column key generation with <union-subclass> mapping for: ...
. The solution for this is to modify the base class @GeneratedValue(strategy = GenerationType.IDENTITY)
to @GeneratedValue(strategy = GenerationType.TABLE)
. However, modifying that class is beyond my scope as it is shared across multiple projects.
Just for the heck of it, I tried the other two strategies. Obviously, SINGLE_TABLE
resulted in one table, with an extra column DTYPE
. Not what we wanted. JOINED
resulted in two tables, but the approved
table has a foreign key to the pending
table. Since we wanted to remove an entry from the pending
table and move it to the approved
table, this would not work for us.
Currently, my solution is to as follows, which is basically copy and paste the code from the Pending
class into the Approved
class.
@Entity
@Table(name = "approved")
public class Approved extends Policy {
@Column(name = "adder", ...)
private String adder;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "time_added", ...)
private Date timeAdded;
@Column(name = "approver", ...)
private String approver;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "time_approved", ...)
private Date timeApproved;
}
This solution seems counter-intuitive as it duplicates code. Is there a solution that does not require code duplication and keeps the maker-checker process that way it currently works?
After experimenting with the suggested approach by @kostja, I arrived at the following solution.
The maker class encapsulates information pertaining to the maker, which is also an @Embeddable
class.
@Embedabble
public class Maker {
@Column(name="maker_id", ...)
private String makerId;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="time_added", ...)
private Date timeAdded;
}
Similarly, the checker class also encapsulates information pertaining to the checker, which is also an @Embeddable
class.
@Embedabble
public class Checker {
@Column(name="checker_id", ...)
private String makerId;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="time_approved", ...)
private Date timeApproved;
}
The payload is an @Embeddable
class. By making the payload an @Embeddable
class, the Maker
and Checker
can be reused for multiple payloads.
@Embeddable
public class Payload { ... }
For example, given two different payloads that requires maker/checker. One of the payload requires 2 checker.
@Embeddable
public class PayloadA { ... }
@Embeddable
public class PayloadB { ... }
Then we define the following two tables for PayloadA
.
@Entity
@Table("a_pending")
public class PendingA {
@Embedded
private PayloadA payload;
@Embedded
private Maker maker;
}
@Entity
@Table("a_approved")
public class ApprovedA {
@Embedded
private PayloadA payload;
@Embedded
private Maker maker;
@Embedded
private Checker checker;
}
Similarly, for PayloadB
define two tables. And PayloadB
requires two checkers.
@Entity
@Table("b_pending")
public class PendingB {
@Embedded
private PayloadB payload;
@Embedded
private Maker maker;
}
@Entity
@Table("b_approved")
public class ApprovedB {
@Embedded
private PayloadB payload;
@Embedded
private Maker maker;
@Embedded
@AttributeOverrides(value = {
@AttributeOverride(name="checkerId",column="checker1_id"),
@AttributeOverride(name="timeApproved",column="checker1_time_approved"),
})
private Checker checker1;
@Embedded
@AttributeOverrides(value = {
@AttributeOverride(name="checkerId",column="checker2_id"),
@AttributeOverride(name="timeApproved",column="checker2_time_approved"),
})
private Checker checker2;
}
I hope this solution should be general and flexible enough.