javahibernatejpa

JPA (Hibernate-backed) Maker-Checker Multi-Level Inheritance


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?


Solution

  • 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.