|

How to parse a String into an EntityGraph with Hibernate 5.4


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


JPA 2.1 introduced the annotation-based Named Entity Graphs and the Entity Graph API. They provide query-independent ways to define which entity attributes Hibernate shall fetch from the database. And while you can’t omit eagerly fetched associations or attributes with Hibernate, you can specify a query-specific eager loading for a graph of associations.

Sounds great, doesn’t it?

The idea is great, but the definition of the graph feels cumbersome. I will show you an example of it after we took a quick look at the entities that we will use in this article.

The Example

Let’s use a simple example that models orders in an online store with customers, order items, products, and suppliers.

The Order entity will be the root object in all of the following examples. It models a many-to-one association to a Customer and a one-to-many association to OrderItems. Each OrderItem references one Product, which belongs to one Supplier.

Ok, let’s get back to the entity graph definition and compare the API provided by JPA 2.1 with the new one in Hibernate 5.4.

Entity Graphs in JPA and Hibernate <5.4

Here is a quick example of a @NamedEntityGraph that you can use on an Order entity to fetch it together with all associated items. And the referenced subgraph itemsGraph tells Hibernate to load the product that’s referenced by each item.

@NamedEntityGraph(name = "graph.Order.items", 
               attributeNodes = @NamedAttributeNode(value = "items", subgraph = "itemsGraph"), 
               subgraphs = @NamedSubgraph(name = "itemsGraph", attributeNodes = @NamedAttributeNode("product")))

After you’ve defined the graph, you need to instantiate it before you can use it with a call of EntityManager’s find method, a JPQL query or a Criteria Query.

EntityGraph graph = em.getEntityGraph("graph.Order.items");

Order newOrder = em.find(Order.class, 1L, Collections.singletonMap("javax.persistence.fetchgraph", graph));

It doesn’t get any better if you use the Entity Graph API. You then need to instantiate a new EntityGraph object, create a subgraph for it and another subgraph for the subgraph.

EntityGraph<Order> graph = em.createEntityGraph(Order.class);
Subgraph<Object> itemGraph = graph.addSubgraph("items");
itemGraph.addSubgraph("product");

Order newOrder = em.find(Order.class, 1L, Collections.singletonMap("javax.persistence.fetchgraph", graph));

Independent of how you create your EntityGraph, Hibernate extends the executed SQL statement to select the columns of all entities referenced in the graph. So, if your graph initializes the items of an Order and the product of each item, Hibernate has to select all database columns mapped by the Order, OrderItem and Product entity.

14:24:17,342 DEBUG SQL:94 - 
    select
        order0_.id as id1_3_0_,
        order0_.customer_id as customer4_3_0_,
        order0_.orderNumber as orderNum2_3_0_,
        order0_.version as version3_3_0_,
        items1_.order_id as order_id4_1_1_,
        items1_.id as id1_1_1_,
        items1_.id as id1_1_2_,
        items1_.order_id as order_id4_1_2_,
        items1_.product_id as product_5_1_2_,
        items1_.quantity as quantity2_1_2_,
        items1_.version as version3_1_2_,
        product2_.id as id1_2_3_,
        product2_.name as name2_2_3_,
        product2_.price as price3_2_3_,
        product2_.supplier_id as supplier5_2_3_,
        product2_.version as version4_2_3_ 
    from
        purchaseOrder order0_ 
    left outer join
        OrderItem items1_ 
            on order0_.id=items1_.order_id 
    left outer join
        Product product2_ 
            on items1_.product_id=product2_.id 
    where
        order0_.id=?

Entity Graphs with Hibernate 5.4

Hibernate 5.4 brings two improvements to this approach. Hibernate can now parse an Entity Graph from a provided String, and it enables you to merge multiple graphs into one.

Let’s take a look at both of them.

Parse a String into an EntityGraph

Hibernate’s GraphParser class provides a set of static methods to parse a String into a new RootGraph object or to parse it into an existing RootGraph or SubGraph object.

Before we take a look at the format of the parsed String, please keep in mind that Hibernate uses eager fetching for all attributes that don’t map an association. So, it doesn’t make any difference whether or not you include them in the graph.

The format of the String is pretty simple. You start on the root entity and specify a comma-separated list of attributes, which you want to include in the graph. The following code snippet shows a simple EntityGraph that only fetches the items attribute of the Order entity.

RootGraph graph = GraphParser.parse(Order.class, "items", em);

Order newOrder = em.find(Order.class, 1L, Collections.singletonMap("javax.persistence.fetchgraph", graph));

The items attribute maps the one-to-many association to the OrderItem entity. For a lot of use cases, you also might want to define the fetching behavior of an associated entity. You can do that by providing a comma-separated list of attribute names in round brackets. I use this notation in the following example to create an EntityGraph that loads an Order entity with all associated items. For each item, I also want to fetch the referenced product and the supplier of that product.

RootGraph graph = GraphParser.parse(Order.class, "items(product(supplier))", em);

Order newOrder = em.find(Order.class, 1L, Collections.singletonMap("javax.persistence.fetchgraph", graph));

When you execute this short code snippet, you will see that Hibernate uses this EntityGraph in the same way as it uses a graph defined via the @NamedEntityGraph annotation or the Entity Graph API. It extends the SQL statement to select all database columns that are mapped by the entities included in the EntityGraph.

14:26:02,824 DEBUG SQL:94 - 
    select
        order0_.id as id1_3_0_,
        order0_.customer_id as customer4_3_0_,
        order0_.orderNumber as orderNum2_3_0_,
        order0_.version as version3_3_0_,
        items1_.order_id as order_id4_1_1_,
        items1_.id as id1_1_1_,
        items1_.id as id1_1_2_,
        items1_.order_id as order_id4_1_2_,
        items1_.product_id as product_5_1_2_,
        items1_.quantity as quantity2_1_2_,
        items1_.version as version3_1_2_,
        product2_.id as id1_2_3_,
        product2_.name as name2_2_3_,
        product2_.price as price3_2_3_,
        product2_.supplier_id as supplier5_2_3_,
        product2_.version as version4_2_3_,
        supplier3_.id as id1_4_4_,
        supplier3_.name as name2_4_4_ 
    from
        purchaseOrder order0_ 
    left outer join
        OrderItem items1_ 
            on order0_.id=items1_.order_id 
    left outer join
        Product product2_ 
            on items1_.product_id=product2_.id 
    left outer join
        Supplier supplier3_ 
            on product2_.supplier_id=supplier3_.id 
    where
        order0_.id=?

As you have seen in the code snippets, you can easily define an EntityGraph that includes several associations. If you compare that with JPA’s graph definitions, which I showed you at the beginning of this article, you can see that Hibernate’s new String parsing feature makes the graph definition much easier.

Combine multiple graphs into one EntityGraph

Another feature added in Hibernate 5.4 enables you to merge multiple graphs into one. That allows you to define graphs indepently of each other and combine them if needed.

In the example of this article, you could define an EntityGraph that fetches the customer of an Order and combine it with the graph that we used in the previous example. So, Hibernate would fetch the Order entity with the associated Customer, OrderItems and Products.

The only thing you need to do to merge multiple graphs is to call the static merge method on Hibernate’s EntityGraphs class with references to the current Session or EntityManager, the class on which the graph shall be applied and multiple EntityGraph objects.

RootGraph graph1 = GraphParser.parse(Order.class, "items(product(supplier))", em);
RootGraph graph2 = GraphParser.parse(Order.class, "customer", em);
EntityGraph graph = EntityGraphs.merge(em, Order.class, graph1, graph2);

Order newOrder = em.find(Order.class, 1L, Collections.singletonMap("javax.persistence.fetchgraph", graph));

As you can see in the log output, Hibernate merged the two graphs and loaded the Order entity with its Customer, OrderItems, Products, and Suppliers.

13:39:33,975 DEBUG SQL:94 - 
    select
        order0_.id as id1_3_0_,
        order0_.customer_id as customer4_3_0_,
        order0_.orderNumber as orderNum2_3_0_,
        order0_.version as version3_3_0_,
        customer1_.id as id1_0_1_,
        customer1_.name as name2_0_1_,
        items2_.order_id as order_id4_1_2_,
        items2_.id as id1_1_2_,
        items2_.id as id1_1_3_,
        items2_.order_id as order_id4_1_3_,
        items2_.product_id as product_5_1_3_,
        items2_.quantity as quantity2_1_3_,
        items2_.version as version3_1_3_,
        product3_.id as id1_2_4_,
        product3_.name as name2_2_4_,
        product3_.price as price3_2_4_,
        product3_.supplier_id as supplier5_2_4_,
        product3_.version as version4_2_4_,
        supplier4_.id as id1_4_5_,
        supplier4_.name as name2_4_5_ 
    from
        purchaseOrder order0_ 
    left outer join
        Customer customer1_ 
            on order0_.customer_id=customer1_.id 
    left outer join
        OrderItem items2_ 
            on order0_.id=items2_.order_id 
    left outer join
        Product product3_ 
            on items2_.product_id=product3_.id 
    left outer join
        Supplier supplier4_ 
            on product3_.supplier_id=supplier4_.id 
    where
        order0_.id=?

This query also shows a drawback of huge entity graphs. They force Hibernate to select a lot of database columns so that it can instantiate the requested entities. Please make sure that you really need these entities with all their attributes. Otherwise, a simpler projection, e.g., using a DTO, is the better approach.

Conclusion

Entity Graphs are a powerful feature to fetch an entity and initialize the required associations. The only downside of these graphs is that JPA only provides a cumbersome API and a set of annotations to define them.

That changes with Hibernate 5.4. You can now define the complete graph in a simple String. You can then parse the String with the methods of Hibernate’s GraphParser class and use the returned RootGraph in the same way as you use any other EntityGraph.

2 Comments

  1. Thanks for sharing such good post regarding parsing String in EntityGraph with Hibernate.its a very useful content,keep your good work.

  2. “This query also shows a drawback of huge entity graph …”

    That is the reason why JPA/Hibernate works well with strong normalized databases. If so, the overhead from unnecessary attributes is not huge.

    Greetings

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.