Single table inheritance with discriminator in JPA

on waitingforcode.com

Single table inheritance with discriminator in JPA

Sometimes database data can be simply reflected in Java objects. But in other situations, when for example one database table stores several types of Java objects, it can be more difficult. And for these situations JPA uses discriminators.

In this article we'll begin by try to understand theoretically, when to use discriminator annotations. After that, we'll show a sample use of them in JUnit class. Please note that in this article we'll pass directly to approached problem. All configuration part is omitted and you can find it in project repository in my Github.

What is discriminator in JPA ?

To understand the utility of JPA's discriminators, let's develop the problem presented in the introduction paragraph. Imagine that in your database you have one table storing products sold by your store - coffee and tea in this case. But in your application layer, you want to add specific behavior for coffee and tea. It's can be done in two ways:
- make one global class, Product, define all methods inside, and decorate type-specific methods with if-else clauses, such as:

public class Product {
  
  // ... some properties here
  private ProductType type; 
  
  public void addMilk()  {
    // suppose that we allow to add milk only to a cup of coffee
    if (type == ProductType.COFFEE)
      // add milk
    }
  }
}

- make two separate classes extending one abstract class and adding type-specific methods inside - more proper and less error-prone approach:
// Product.java
public abstract class Product {
  // ... some properties here with protected visibility
}

// ProductCoffee.java
public class ProductCoffee extends Product {
  public void addMilk() {
    // add milk
  }
}

// ProductTea.java
public class ProductTea extends Product {
  public void specificTeaBehavior() {
    // do something
  }
}

As you can see, there are two different approaches. The second one should be preferred but it's realizable only with JPA's discriminators. Discriminator is an inheritance strategy based on data contained in a single database table. To configure this type of inheritance, JPA provides two annotations:
- @DiscriminatorColumn : in SINGLE_TABLE and JOIN inheritance, this annotation indicates a column containing data permitting to distinguish to which Java object corresponds given database row. In the case of our store, as a discriminator column we'll use a column called type. Usually this annotation is implemented by parent abstract class for type-specific objects (Product in our case).
- @DiscriminatorValue : should be applied on type-specific classes. The value taken by this annotation associates given class with appropriated database rows. For example, if we specify this annotation with the value "TEA" for ProductTea class, all database rows which type column is "TEA" will be considered by application as instance of ProductTea.

Example of discrimininator in JPA

To see how does it work, we can analyze some of project classes, as com.waitingforcode.model.Order and its children classes, OrderCreated with OrderConfirmed. As you can see, they're discriminated through state column which can take one of two values: CONFIRMED or CREATED. This simple unit test shows the discriminator feature:

/**
 * Test cases for {@link javax.persistence.DiscriminatorColumn} and {@link javax.persistence.DiscriminatorValue}
 *
 * Expected order table before the tests :
 * <pre>
 * mysql> select * from `order`;
 * +----+------------------+---------+-----------+---------------------+
 * | id | shopping_cart_id | user_id | state     | updated_date        |
 * +----+------------------+---------+-----------+---------------------+
 * |  1 |                1 |       1 | CREATED   | 2014-10-01 19:00:00 |
 * |  2 |                2 |       3 | CONFIRMED | 2014-10-01 19:15:00 |
 * +----+------------------+---------+-----------+---------------------+
 * 2 rows in set (0.00 sec)
 * </pre>
 *
 * <!> To be able to check MySQL queries logs, activate following lines in your configuration:
 * <pre>
 *  general_log_file        = /var/log/mysql/mysql.log
 *  general_log             = 1
 * </pre>
 *
 * @author Bartosz Konieczny
 */
public class DiscriminatorTest extends AbstractJpaTester {

  @Test
  public void testCreatedOrders() {
    Query query = entityManager.createQuery("SELECT oc FROM OrderCreated oc");
    List<OrderCreated> orders = query.getResultList();
    assertEquals("1 created order should be found in database", 1, orders.size());
    assertEquals("According to expected sample data, the first order (id 1) should be 'created' one", 1l,
            orders.get(0).getId().longValue());
    // in /var/log/mysql/mysql.log you should find following query:
    // select ordercreat0_.id as id2_0_, ordercreat0_.user_id as user_id3_0_, ordercreat0_.shopping_cart_id as
    // shopping4_0_ from `order` ordercreat0_ where ordercreat0_.state='CREATED'
  }

  @Test
  public void testConfirmedOrders() {
    Query query = entityManager.createQuery("SELECT oc FROM OrderConfirmed oc");
    List<OrderConfirmed> orders = query.getResultList();
    assertEquals("1 confirmed order should be found in database", 1, orders.size());
    assertEquals("According to expected sample data, the second order (id 2) should be 'created' one", 2l,
            orders.get(0).getId().longValue());
    // in /var/log/mysql/mysql.log you should find following query:
    // select orderconfi0_.id as id2_0_, orderconfi0_.user_id as user_id3_0_, orderconfi0_.shopping_cart_id as
    // shopping4_0_ from `order` orderconfi0_ where orderconfi0_.state='CONFIRMED'
  }

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

      User customer = new User();
      customer.setLogin("Temporary");
      customer.setType(UserType.CUSTOMER);
      entityManager.persist(customer);

      ShoppingCart shoppingCart = new ShoppingCart();
      shoppingCart.setCustomer(customer);
      shoppingCart.setCreationDate(new Date());
      shoppingCart.setLastUpdateDate(new Date());
      shoppingCart.setState(ShoppingCartState.CONFIRMED);
      entityManager.persist(shoppingCart);

      OrderCreated created = new OrderCreated();
      created.setCustomer(customer);
      created.setShoppingCart(shoppingCart);
      entityManager.persist(created);

      OrderConfirmed confirmed = new OrderConfirmed();
      confirmed.setCustomer(customer);
      confirmed.setShoppingCart(shoppingCart);
      entityManager.persist(confirmed);

      Query query = entityManager.createQuery("SELECT oc FROM OrderCreated oc WHERE oc.id = :id");
      query.setParameter("id", created.getId());
      OrderCreated createdFromDb = (OrderCreated) query.getSingleResult();
      assertNotNull("OrderCreated should be correctly saved in persistence storage", createdFromDb);

      query = entityManager.createQuery("SELECT oc FROM OrderConfirmed oc WHERE oc.id = :id");
      query.setParameter("id", confirmed.getId());
      OrderConfirmed confirmedFromDb = (OrderConfirmed) query.getSingleResult();
      assertNotNull("OrderConfirmed should be correctly saved in persistence storage", confirmedFromDb);
    } finally {
      transaction.rollback();
    }
  }
}

Tested objects look like:

// OrderConfirmed.java
@Entity
@DiscriminatorValue("CONFIRMED")
public class OrderConfirmed extends Order {
}

// OrderCreated.java
@Entity
@DiscriminatorValue("CREATED")
public class OrderCreated extends Order {
}

// Order.java
@Entity
@Table(name = "`order`")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "state")
public abstract class Order {

  protected Long id;
  protected ShoppingCart shoppingCart;
  protected User customer;
  protected Date lastUpdated;

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

  @OneToOne
  @JoinColumn(name = "shopping_cart_id")
  public ShoppingCart getShoppingCart() {
    return this.shoppingCart;
  }

  @ManyToOne
  @JoinColumn(name = "user_id")
  public User getCustomer() {
    return  this.customer;
  }

  @Temporal(TemporalType.TIME)
  @Column(name = "updated_date")
  public Date getLastUpdated() {
    return this.lastUpdated;
  }

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

  public void setShoppingCart(ShoppingCart shoppingCart) {
    this.shoppingCart = shoppingCart;
  }

  public void setCustomer(User customer) {
    this.customer = customer;
  }

  public void setLastUpdated(Date lastUpdated) {
    this.lastUpdated = lastUpdated;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this).add("id", this.id).add("customer", this.customer)
      .add("shopping cart", this.shoppingCart).toString();
  }
}

This article shows how to implement single table inheritance in Java objects world. In the other words, with @DiscriminatorColumn and @DiscriminatorValue annotations we can simply divide rows from database's table in two or more different Java objects.

Share on: