Inheritance types in JPA

In the previous article about JPA we discovered how to create Java objects inheritance tree directly from a single database table with discriminator annotations. In this occasion we discovered also another interesting annotation, used to define inheritance type - @Inheritance.

This article covers 3 types of inheritance: SINGLE_TABLE, TABLE_PER_CLASS and JOINED. All of them are described in the first part of the article. The second part is about to show in which practical situations we can configure them and in which way.

@Inheritance in JPA

Because relational database doesn't have inheritance concept implemented natively, @Inheritance annotation is used to reflect inheritance between Java objects to database tables. This annotation provides 3 inheritance types, called strategies:

  1. SINGLE_TABLE: in this strategy all classes from hierarchy are stored in a single database table. Typically, it's the strategy which we'll use usually with @DiscriminatorColumn and @DiscriminatorValue annotations.

    As this strategy is based on discriminator values, sometimes it can be difficult to achieve SINGLE_TABLE inheritance for the data which discriminator value is composed by multiple columns or by some more complex rule. Hibernate solves this problem with annotation @DiscriminatorFormula. Inside it we can specify SQL expression which will classify given row to appropriate Java object.
  2. JOINED: this type of inheritance gets both inheritance types, at database and Java level, closer. In other words, entities specified in the application will use separate database tables. All specific database tables will be joined with parent table by relation based on foreign keys.

    Some of JOINED inheritance implementations can need to specify discriminator annotations.

    Depending on database size, this strategy can slow down some queries execution. For example, it can occur in the case of multiple joins, not only between parent and children tables of JOINED inheritance, but also with another useful tables at given moment.
  3. TABLE_PER_CLASS: unlike two previous strategies, this one doesn't have any relation between database and Java objects inheritance. To simplify, you can create an inheritance on Java objects when there are no inheritance in database. Thanks to this strategy you can have two similar tables in the database, not associated each other, and create one base parent class with two children classes in Java lavel.

Example of JOINED inheritance

Following examples should be more explicit than definitions from previous part. SINGLE_TABLE sample was described in the article consacred to single table inheritance with discriminator in JPA. So, we'll begin here by JOINED example:

show mapping for JOINED inheritance
// Promotion.java
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "type")
@Table(name = "promotion")
public abstract class Promotion {

  protected Long id;
  protected String name;
  protected double amount;

  @Id
  @GeneratedValue(strategy = IDENTITY)
  @Column(name = "id")
  public Long getId() {
    return this.id;
  }

  @Column(name = "name", length =  100)
  public String getName() {
    return this.name;
  }

  @Column(name = "amount", length = 7, precision = 2)
  public double getAmount() {
    return this.amount;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setAmount(double amount) {
    this.amount = amount;
  }

}

// PromoGiftVoucher.java
@Entity
@DiscriminatorValue(value = "GIFT_VOUCHER")
@Table(name = "gift_voucher")
public class PromoGiftVoucher extends Promotion {

  @Override
  public String toString() {
      return MoreObjects.toStringHelper(this).add("name", this.name).add("amount", this.amount)
        .toString();
  }

}

// PromoCode.java
@Entity
@DiscriminatorValue(value = "PROMO_CODE")
@Table(name = "promo_code")
public class PromoCode extends Promotion {

  protected PromoCodeType promoCodeType;
  protected String code;

  @Enumerated(value = EnumType.STRING)
  @Column(name = "type")
  public PromoCodeType getPromoCodeType() {
    return this.promoCodeType;
  }

  @Column(name = "code")
  public String getCode() {
    return this.code;
  }

  public void setPromoCodeType(PromoCodeType promoCodeType) {
    this.promoCodeType = promoCodeType;
  }

  public void setCode(String code) {
    this.code = code;
  }

  @Override
  public String toString() {
      return MoreObjects.toStringHelper(this).add("name", this.name).add("amount", this.amount)
        .add("promo code type", this.promoCodeType).add("code", this.code).toString();
  }

}

Below you can find some test cases to show how does JOINED inheritance work:

/**
 * Tests for {@link javax.persistence.Inheritance} annotation of JOINED type.
 *
 * Tables expected before the test:
 * <pre>
 * mysql> select * from promotion;
 * +----+-------------------+--------+--------------+
 * | id | name              | amount | type         |
 * +----+-------------------+--------+--------------+
 * |  1 | Christmas promo   |  50.00 | PROMO_CODE   |
 * |  2 | Christmas voucher |  15.00 | GIFT_VOUCHER |
 * +----+-------------------+--------+--------------+
 * 2 rows in set (0.00 sec)
 *
 * mysql> select * from promo_code;
 * +----+------+--------+
 * | id | type | code   |
 * +----+------+--------+
 * |  1 | POUR | CHRISM |
 * +----+------+--------+
 * 1 row in set (0.00 sec)
 *
 * mysql> select * from gift_voucher;
 * +----+
 * | id |
 * +----+
 * |  2 |
 * +----+
 * 1 row in set (0.00 sec)
 * </pre>
 *
 * @author Bartosz Konieczny
 */
public class InheritanceJoinedTest extends AbstractJpaTester {

  @Test
  public void testVoucherRead() {
    Query query = entityManager.createQuery("SELECT gv FROM PromoGiftVoucher gv WHERE gv.id = :id");
    query.setParameter("id", 2l);
    PromoGiftVoucher voucher = (PromoGiftVoucher) query.getSingleResult();
    assertEquals("Voucher should be with 15€ value", 15.00d, voucher.getAmount(), 0);
    assertEquals("Bad voucher was found", "Christmas voucher", voucher.getName());

    // SELECT query generated for MySQL is following :
    // 57 Query     select promogiftv0_.id as id2_5_, promogiftv0_1_.amount as amount3_5_, promogiftv0_1_.name as name4_5_
    // from gift_voucher promogiftv0_ inner join promotion promogiftv0_1_ on
    // promogiftv0_.id=promogiftv0_1_.id where promogiftv0_.id=2
  }

  @Test
  public void testCodeRead() {
    Query query = entityManager.createQuery("SELECT pc FROM PromoCode pc WHERE pc.id = :id");
    query.setParameter("id", 1l);
    PromoCode promoCode = (PromoCode) query.getSingleResult();
    assertEquals("Bad promo code was found", "CHRISM", promoCode.getCode());
    assertEquals("Bad promo code type", PromoCodeType.POUR, promoCode.getPromoCodeType());

    // SELECT query generated for MySQL is following :
    // 57 Query     select promocode0_.id as id2_5_, promocode0_1_.amount as amount3_5_, promocode0_1_.name as name4_5_,
    // promocode0_.code as code1_4_, promocode0_.type as type2_4_ from promo_code promocode0_
    // inner join promotion promocode0_1_ on promocode0_.id=promocode0_1_.id where promocode0_.id=1
  }

  @Test
  public void testAddVoucher() {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
      transaction.begin();

      PromoGiftVoucher voucher = new PromoGiftVoucher();
      voucher.setAmount(11d);
      voucher.setName("Temporary");
      entityManager.persist(voucher);

      Query query = entityManager.createQuery("SELECT gv FROM PromoGiftVoucher gv WHERE gv.id = :id");
      query.setParameter("id", voucher.getId());
      PromoGiftVoucher dbVoucher = (PromoGiftVoucher) query.getSingleResult();
      assertEquals("Voucher should be with 11€ value", 11.00d, dbVoucher.getAmount(), 0);
      assertEquals("Bad voucher was inserted", "Temporary", dbVoucher.getName());

      // if you analyze MySQL logs, you'll see that it's inserted with 2 queries:
      // 57 Query     insert into promotion (amount, name, type) values (11.0, 'Temporary', 'PROMO_CODE')
      // 57 Query     insert into promo_code (code, type, id) values ('TMPTMP', 'FIXA', 5)
    } finally {
      transaction.rollback();
    }
  }

  @Test
  public void testAddCode() {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
      transaction.begin();

      PromoCode code = new PromoCode();
      code.setAmount(11d);
      code.setName("Temporary");
      code.setCode("TMPTMP");
      code.setPromoCodeType(PromoCodeType.FIXA);
      entityManager.persist(code);

      Query query = entityManager.createQuery("SELECT pc FROM PromoCode pc WHERE pc.id = :id");
      query.setParameter("id", code.getId());
      PromoCode dbCode = (PromoCode) query.getSingleResult();
      assertEquals("Bad promo code was inserted", "TMPTMP", dbCode.getCode());
      assertEquals("Bad promo code type", PromoCodeType.FIXA, dbCode.getPromoCodeType());

      // if you analyze MySQL logs, you'll see that it's inserted with 2 queries:
      // 57 Query     insert into promotion (amount, name, type) values (11.0, 'Temporary', 'GIFT_VOUCHER')
      // 57 Query     insert into gift_voucher (id) values (6)
    } finally {
      transaction.rollback();
    } 
  } 
}

Example of TABLE_PER_CLASS inheritance

And now, it's the turn of TABLE_PER_CLASS inheritance:

show mapping for TABLE_PER_CLASS inheritance

Here too, we have some test cases to illustrate the working logic of TABLE_PER_CLASS inheritance:

/**
 * Tests for TABLE_PER_CLASS strategy in {@link javax.persistence.Inheritance} annotation.
 *
 * Expected tables before the test:
 * <pre>
 * mysql> select * from action_backoffice;
 * +----+------------------------------------------------------------------------------+---------------------+
 * | id | action                                                                       | action_date         |
 * +----+------------------------------------------------------------------------------+---------------------+
 * |  1 | Removing product 'Coffee' by 'moderator'                                     | 2014-10-01 10:00:00 |
 * |  2 | Adding product 'Coffee' by 'moderator'                                       | 2014-10-01 10:15:00 |
 * |  3 | Changing the name of product 'Coffee' to 'Coffee from Brazil' by 'moderator' | 2014-10-01 10:30:00 |
 * |  4 | Removing product 'Coffee from Brazil' by 'moderator'                         | 2014-10-01 10:45:00 |
 * +----+------------------------------------------------------------------------------+---------------------+
 * 4 rows in set (0.00 sec)
 *
 * mysql> select * from action_store;
 * +----+----------------------------------------+---------------------+-------------+
 * | id | action                                 | action_date         | user_cookie |
 * +----+----------------------------------------+---------------------+-------------+
 * |  1 | Adding product 'Milk' to shopping cart | 2014-10-01 15:00:00 | 8SDQ903     |
 * |  2 | Introducing promo code 'FAKECO'        | 2014-10-01 15:15:00 | 8SDQ903     |
 * |  3 | Invalidating promo code 'FAKECO'       | 2014-10-01 15:30:00 | 8SDQ903     |
 * |  4 | Validating shopping cart with 39.99€   | 2014-10-01 15:45:00 | 8SDQ903     |
 * +----+----------------------------------------+---------------------+-------------+
 * 4 rows in set (0.00 sec)
 * </pre>
 *
 * @author Bartosz Konieczny
 */
public class InheritanceTablePerClassTest extends AbstractJpaTester {

  @Test
  public void testReadBackoffice() {
    Query query = entityManager.createQuery("SELECT a FROM ActionBackoffice a ORDER BY a.id ASC");
    List<ActionBackoffice> actions = query.getResultList();
    assertEquals("4 actions should be found", 4, actions.size());

    assertEquals("Bad line was found for the 1st result", "Removing product 'Coffee' by 'moderator'",
            actions.get(0).getActionName());
    assertEquals("Bad line was found for the 2nd result", "Adding product 'Coffee' by 'moderator'",
            actions.get(1).getActionName());
    assertEquals("Bad line was found for the 3rd result",
            "Changing the name of product 'Coffee' to 'Coffee from Brazil' by 'moderator'", actions.get(2).getActionName());
    assertEquals("Bad line was found for the 4th result", "Removing product 'Coffee from Brazil' by 'moderator'",
            actions.get(3).getActionName());

    // SELECT should be made with following query :
    // select actionback0_.id as id1_0_, actionback0_.action_date as action_d2_0_, actionback0_.action as action3_0_ from
    // action_backoffice actionback0_ order by actionback0_.id ASC
  }

  @Test
  public void testReadStore() {
    Query query = entityManager.createQuery("SELECT a FROM ActionStore a ORDER BY a.id ASC");
    List<ActionStore> actions = query.getResultList();
    assertEquals("4 actions should be found", 4, actions.size());

    assertEquals("Bad line was found for the 1st result", "Adding product 'Milk' to shopping cart",
            actions.get(0).getActionName());
    assertEquals("Bad line was found for the 2nd result", "Introducing promo code 'FAKECO'",
            actions.get(1).getActionName());
    assertEquals("Bad line was found for the 3rd result", "Invalidating promo code 'FAKECO'",
            actions.get(2).getActionName());
    assertEquals("Bad line was found for the 4th result", "Validating shopping cart with 39.99€",
            actions.get(3).getActionName());

    // SELECT should be made with following query :
    // select actionstor0_.id as id1_0_, actionstor0_.action_date as action_d2_0_, actionstor0_.action as action3_0_,
    // actionstor0_.user_cookie as user_coo1_3_ from action_store actionstor0_ order by actionstor0_.id ASC
  }

  @Test
  public void testAddBackoffice() {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
      transaction.begin();

      ActionBackoffice action = new ActionBackoffice();
      action.setActionDate(new Date());
      action.setActionName("Test");
      entityManager.persist(action);

      Query query = entityManager.createQuery("SELECT a FROM ActionBackoffice a WHERE a.id = :id");
      query.setParameter("id", action.getId());
      ActionBackoffice dbAction = (ActionBackoffice) query.getSingleResult();

      assertEquals("Bad ActionBackoffice was inserted", "Test", dbAction.getActionName());

      // insert should be made with following queries
      // select sequence_next_hi_value from hibernate_sequences where sequence_name = 'Action' for update
      // update hibernate_sequences set sequence_next_hi_value = 22 where sequence_next_hi_value = 21
      // and sequence_name = 'Action'
      // insert into action_backoffice (action_date, action, id) values ('2014-10-30 12:51:06', 'Test', 21)
    } finally {
      transaction.rollback();
    }
  }

  @Test
  public void testAddStore() {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
      transaction.begin();

      ActionStore action = new ActionStore();
      action.setUserCookie("XXX");
      action.setActionDate(new Date());
      action.setActionName("TestSt");
      entityManager.persist(action);

      Query query = entityManager.createQuery("SELECT a FROM ActionStore a WHERE a.id = :id");
      query.setParameter("id", action.getId());
      ActionStore dbAction = (ActionStore) query.getSingleResult();

      assertEquals("Bad ActionStore was inserted", "TestSt", dbAction.getActionName());

      // insert should be made with following queries
      // select sequence_next_hi_value from hibernate_sequences where sequence_name = 'Action' for update
      // update hibernate_sequences set sequence_next_hi_value = 21 where sequence_next_hi_value = 20
      // and sequence_name = 'Action'
      // insert into action_store (action_date, action, user_cookie, id)
      // values ('2014-10-30 12:51:06', 'TestSt', 'XXX', 20)
    } finally {
      transaction.rollback();
    }
  }
}

This article shows different inheritance types used in JPA. We saw that 2 of them, JOINED and SINGLE_TABLE, represent Java objects inheritance a little bit in the database. The third one, TABLE_PER_CLASS, is completely different because it doesn't need to have inheritance representation in database.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!