Mapped superclass in JPA

Until now in our examples we were using object inheritance based on entity classes (@Entity). However, we can also construct classes hierarchy without that, by using another JPA's annotation: @MappedSuperclass.

This article shows how to construct class hierarchy without declaring parent class as an entity. At the begin of it, we'll broach theoretical aspect of @MappedSuperclass annotation. After that, we'll see how it works in one simple JUnit test case.

What is @MappedSuperclass ?

In some previous articles we've already constructed classes hierarchies. These hierarchies corresponded to table structures in database. To do that, we used for example @Inheritance and @DiscriminatorColumn annotations. Parent classes constructed in this way have to be defined as entities (@Entity). However, JPA provides an annotation thanks to which we don't need anymore to define a parent class as an entity. This annotation is @MappedSuperclass.

@MappedSuperclass works like an entity class. The difference is it doesn't need to be reflected in database table. In other words, this class behaves like classical abstract class. Additionally to classical abstract class features (inheritance), class annotated with @MappedSuperclass can define JPA mappings for inherited columns or ids. This mapping will be taken in consideration by JPA's provider on the stage of entity manager construction. A very simple mapping can look like:

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

@Entity
public class OrderConfirmed extends Order {
  // ... some properties here, but without the need of
  // declare id strategy, inherited directly from Order
  // class mapping
}

If you launch previous sample, you should be able to manipulate OrderConfirmed object without problems. In the other side, if you remove @MappedSuperclass annotation from Order class, you'll see some of javax.persistence.PersistenceException:

Caused by: javax.persistence.PersistenceException: [PersistenceUnit: sampleUnit] Unable to build Hibernate SessionFactory
	at com.waitingforcode.configuration.AbstractJpaTester.(AbstractJpaTester.java:16)
	... 25 more
Caused by: org.hibernate.AnnotationException: No identifier specified for entity: sample.OrderConfirmed

This exceptions proves well that you can't manage JPA mapping in simple POJO classes, not annotated with @Entity or @MappedSuperclass.

Example of @MappedSuperclass

To see @MappedSuperclass in action, we'll use an example of invoice and letter objects, both based on abstract class representing a document. The mapping looks like:

// Document.java
@MappedSuperclass
public abstract class Document {

  protected Long id;
  protected String address;

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

  @Column(name = "address")
  public String getAddress() {
    return this.address;
  }

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

  public void setAddress(String address) {
    this.address = address;
  }

}

// Invoice.java
@Entity
@Table(name = "invoice")
@AttributeOverride(name = "address", column = @Column(name="invoice_address"))
public class Invoice extends Document {

  private double amount;
  private String companyName;

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

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

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

  public void setCompanyName(String companyName) {
    this.companyName = companyName;
  }

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

}

// Letter.java
@Entity
@Table(name = "letter")
public class Letter extends Document {

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this).add("address", this.address).toString();
  }
}
/**
 * Tests for another inheritance method, {@link javax.persistence.MappedSuperclass}.
 *
 * Expected data before tests:
 * <pre>
 * mysql> select * from letter;
 * +----+-----------------------+
 * | id | address               |
 * +----+-----------------------+
 * |  1 | 39 street, 99999 City |
 * +----+-----------------------+
 * 1 row in set (0.00 sec)
 *
 * mysql> select * from invoice;
 * +----+-----------------------+----------------+--------+
 * | id | invoice_address       | company_name   | amount |
 * +----+-----------------------+----------------+--------+
 * |  1 | 40 street, 00000 City | Customer is me | 399.99 |
 * +----+-----------------------+----------------+--------+
 * 1 row in set (0.00 sec)
 * </pre>
 *
 * @author Bartosz Konieczny
 */
public class MappedSuperclassTest extends AbstractJpaTester {

  @Test
  public void testReadLetter() {
    Query query = entityManager.createQuery("SELECT l FROM Letter l WHERE l.id = :id");
    query.setParameter("id", 1l);
    Letter letter = (Letter) query.getSingleResult();

    assertEquals("Bad Letter object was retrieved from the database", "39 street, 99999 City", 
      letter.getAddress());

    // Select query is very basic
    // select letter0_.id as id1_7_, letter0_.address as address2_7_ from letter letter0_ where letter0_.id=1
  }

  @Test
  public void testReadInvoice() {
    Query query = entityManager.createQuery("SELECT i FROM Invoice i WHERE i.id = :id");
    query.setParameter("id", 1l);
    Invoice invoice = (Invoice) query.getSingleResult();

    assertEquals("Bad Invoice was retrieved from the database (wrong address)", "40 street, 00000 City",
            invoice.getAddress());
    assertEquals("Bad Invoice was retrieved from the database (wrong company)", "Customer is me", 
      invoice.getCompanyName());

    // Select query is very basic
    // select invoice0_.id as id1_6_, invoice0_.invoice_address as invoice_2_6_, invoice0_.amount as amount3_6_,
    // invoice0_.company_name as company_4_6_ from invoice invoice0_ where invoice0_.id=1
  }

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

      Letter letter = new Letter();
      letter.setAddress("Test address");
      entityManager.persist(letter);

      Query query = entityManager.createQuery("SELECT l FROM Letter l WHERE l.id = :id");
      query.setParameter("id", letter.getId());
      Letter dbLetter = (Letter) query.getSingleResult();

      assertEquals("Bad letter was inserted", "Test address", dbLetter.getAddress());

      // Insert query is very basic :
      // insert into letter (address) values ('Test address')
    } finally {
      transaction.rollback();
    }
  }

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

      Invoice invoice = new Invoice();
      invoice.setAmount(39.99d);
      invoice.setCompanyName("Temporary");
      invoice.setAddress("Test address");
      entityManager.persist(invoice);

      Query query = entityManager.createQuery("SELECT i FROM Invoice i WHERE i.id = :id");
      query.setParameter("id", invoice.getId());
      Invoice dbInvoice = (Invoice) query.getSingleResult();

      assertEquals("Bad invoice was inserted (wrong address)", "Test address", 
      dbInvoice.getAddress());
      assertEquals("Bad invoice was inserted (wrong company)", "Temporary", 
      dbInvoice.getCompanyName());

      // Insert query is very basic :
      // insert into invoice (invoice_address, amount, company_name) values ('Test address', 39.99, 'Temporary')
    } finally {
      transaction.rollback();
    }
  }
}

We can see in this article that thanks to class annotated with @MappedSuperclass, we can define some JPA mapping information which will be inherited by children class, implementing or extending @MappedSuperclass. We could achieve the same with abstract entity class, but with one difference - entity class should be defined under database table while mapped superclass has not to be defined in such way. The article also shows that simple POJOs, without any JPA annotation at class level, aren't parsed by JPA provider. So even if they contain some mapping information, the information is not taken in consideration for entity manager construction.


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!