Hibernate: Legacy XML vs. JPA Annotations

This is an abbreviated chapter from my book Java for the Real World. Want more content like this? Click here to get the book! The code for this chapter is available here.

Object relational mappers attempt to solve these problems by abstracting away all SQL from the developer. Theoretically, given enough information about the objects, it is possible for all SQL to be generated programatically.

Hibernate was one of the first ORM tools for Java. It was not until a few years later when persistence was standardized with the Java Persistence API (JPA). The JPA provides no implementation–rather it allows third-party tools to leverage a common API. And it just so happens that Hibernate is the most commonly used third-party implementation. Having said that, it is still possible to use Hibernate *without* using JPA. The sample code for this chapter includes two Hibernate projects: one that is more modern and leverages the JPA annotations and one that is an older style that uses XML for its configuration. To further complicate matters, you can use JPA interfaces or Hibernate’s native classes to interact with the database. For convenience, the JPA code example also uses JPA interfaces while the XML code example uses Hibernate native objects, but it’s possible to mix and match. You should be familiar with all of these options as they are all still popular.

JPA Annotations

The JPA provides a set of annotations that are used to mark up domain classes to describe how they map to the underlying database. In general, classes are annotated @Entity and @Table(name = "myDatabaseTableName"), while properties are annotated @Column(name = "myColumnName"). Additional annotations are added to “special” columns such as ID columns and foreign key columns. Importantly, foreign keys are not represented as integers but rather as the objects themselves. For example, the OrderLineItem class does not have a purchase_id property, it has an Order property. Here’s the complete annotated properties of that class.

@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "purchase_id")
private Order order;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ingredient_id")
private Ingredient ingredient;

@Column(name = "units")
private Integer units;

Focusing on the two foreign-keyed properties, notice that @JoinColumn is used to signify it is used in a SQL join and that the relationship is described as @ManyToOne since there are many OrderLineItems for each Order. The fetch = FetchType.Lazy instructs Hibernate (or more accurately, any JPA provider you’re using) to not load the actual Order object until it is explicitly asked for. Lazy‘s opposite is FetchType.Eager which would load the Order as soon as the OrderLineItem is returned from the database.

It’s not required, but I chose to create a bi-directional relationship between an order and its line items. Here’s how Order is annotated.

@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderLineItem> orderLineItems;

@Column(name = "create_dttm")
private Timestamp created = Timestamp.valueOf(LocalDateTime.now());

@Column(name = "total_price")
private BigDecimal totalPrice;

The new annotation here is @OneToMany: there is one Order for many OrderLineItems. The mappedBy attribute tells Hibernate that it should use the value order to find a setter, i.e. setOrder when returning OrderLineItems, and the attribute cascade is used to describe the behavior of child objects when an operation occurs on the parent. Using CascadeType.ALL means updates, inserts, and deletes on an Order will cascade down to child OrderLineItems.

As you can imagine, there are many annotations to fit the wide variety of relationships in databases. You might consider browsing the JPA JavaDoc to get a feeling for what is possible.

When using JPA, Hibernate is usually configured using a persistence.xml file or programatically with Java. Neither of these options are proprietary, and therefore could be used with other JPA provider. For more details, see “Bootstrapping” in the official documentation. Finally, if you use Spring Boot, the configuration is mostly taken care of automatically with just a few values set in application.yml / application.properties.

XML Mappings

The older method of mapping objects for Hibernate was via–you guessed it–XML. You will have one MyClassName.hbm.xml file for each entity class in the application and it will need to be on the classpath. Each file lists the properties of the class (e.g. <property ...) and the relationships (e.g. <one-to-many ...). Here’s the mappings for Order and OrderLineItem. If you compare them to the JPA-annotated classes, you should notice similar terminology and structure, although some of the vocabulary does not match.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.letstalkdata.iscream.domain.Order" table="purchase">
        <id name="id" type="int" column="id">
            <generator class="identity"/>
        </id>
        <property name="created" column="create_dttm" type="timestamp"/>
        <property name="totalPrice" column="total_price" type="big_decimal"/>
        <bag name="orderLineItems"
             table="purchase_line_item"
             cascade="all"
             inverse="true">
            <key column="purchase_id" not-null="true" />
            <one-to-many class="com.letstalkdata.iscream.domain.OrderLineItem"/>
        </bag>
    </class>
</hibernate-mapping>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.letstalkdata.iscream.domain.OrderLineItem"
           table="purchase_line_item">
        <id name="id" type="int" column="id">
            <generator class="identity"/>
        </id>
        <many-to-one name="order"
                     class="com.letstalkdata.iscream.domain.Order"
                     lazy="proxy"
                     fetch="join">
            <column name="purchase_id" not-null="true" />
        </many-to-one>
        <many-to-one name="ingredient"
                     class="com.letstalkdata.iscream.domain.Ingredient"
                     lazy="proxy"
                     fetch="join">
            <column name="ingredient_id" not-null="true" />
        </many-to-one>
        <property name="units" column="units" type="int"/>
    </class>
</hibernate-mapping>

When using the mappings, you have to register them with Hibernate via configuration. In most instances, Hibernate is configured programatically with Java, a hibernate.properties file, or a hibernate.cfg.xml file. For more details on hibernate configuration see “Legacy Bootstrapping” in the official documentation.

Writing Data

JPA

As you can imagine, the OrderService for JPA is going to look considerably different from what we have seen so far. The JPA object that does the heavy lifting is an EntityManager. One way to obtain an EntityManager is through the EntityManagerFactoryBuilder > EntityManagerFactory > EntityManager. The EntityManager has three methods that loosely map to “insert”, “update”, and “delete”: persist, merge, and remove. (The difference between persist and merge is actually non-trivial and this StackOverflow answer does a good job discussing the differences.)

Here’s where the real power of ORMs is revealed: no SQL was written, but using the annotations, the JPA provider is able to create the proper inserts to save the Order and its child OrderLineItems! This means saving an object is usually just one line of code:

package com.letstalkdata.iscream.service;

import com.letstalkdata.iscream.domain.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.persistence.EntityManager;
import javax.transaction.Transactional;

@Service
public class OrderService {

    private EntityManager entityManager;

    @Autowired
    public OrderService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public void save(Order order) {
        entityManager.persist(order);
    }

}
Native Hibernate

If you are not using JPA, the worker class is a Hibernate SessionFactory which can be used to create Sessions. One Hibernate Session is used per unit-of-work. This is deliberately vague–a unit of work is typically larger than a single database round-trip and may contain a few tightly related operations. For example, a unit of work might be one user request in a web application.

In addition to merge and persist, Hibernate session also has some operations like save, update, saveOrUpdate. Sadly, these are also non-trivial. For a brief discussion, see “Hibernate: save, persist, update, merge, saveOrUpdate” on Baeldung.

The code to save an object using native Hibernate is still relatively short, but remember to (auto) close the Session.

public void save(Order order) {
    try(Session session = sessionFactory.openSession()) {
        Transaction tx = session.beginTransaction();
        session.persist(order);
        tx.commit();
    }
}

Reading Data

Using an ORM, it’s possible to say “Save this object!” because the object has annotations, but saying “Get an object!” is not straight-forward. What filters should be applied? Do you want one object or many? Do you want child objects? Etc. Because of these questions, you need to construct a query.

With Hibernate, you have a few options: use a Criteria object, write Hibernate Query Language (HQL), or write native SQL. (In general, you should avoid native SQL, since that really defeats the purpose of an ORM framework. But there are definitely situations where it cannot be (easily) avoided.) Here’s how the three methods compare:

Criteria:

private List<Ingredient> getIngredients(Ingredient.Type type) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Ingredient> criteriaQuery =
            cb.createQuery(Ingredient.class);
    Root ingredient = criteriaQuery.from(Ingredient.class);
    criteriaQuery.where(cb.equal(ingredient.get("type"), type));
    TypedQuery<Ingredient> query = entityManager.createQuery(criteriaQuery);
    return query.getResultList();
}

HQL:

private List<Ingredient> getIngredients(Ingredient.Type type) {
    String hql = "select i from Ingredient i where type =:type";
    Query query = entityManager.createQuery(hql);
    query.setParameter("type", type);
    @SuppressWarnings("unchecked")
    List<Ingredient> ingredients =
            (List<Ingredient>) query.getResultList();
    return ingredients;
}

Native SQL:

private List<Ingredient> getIngredients(Ingredient.Type type) {
    String sql = "select * from ingredient where ingredient_type = ?";
    Query query = entityManager.createNativeQuery(sql, Ingredient.class);
    query.setParameter(1, type.name());
    @SuppressWarnings("unchecked")
    List<Ingredient> ingredients =
            (List<Ingredient>) query.getResultList();
    return ingredients;
}

If you are using native Hibernate instead of JPA, the code is largely the same except that a Query is an org.hibernate.Query, not a javax.persistence.Query and is created from Session.createQuery().

For a more in-depth comparison and other practical advice about the Java ecosystem, check out my book Java for the Real World.

Click here to get Java for the Real World!

Tagged on: , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *