JPA : entity graphs

Possibility to specify fetching of child entities is quite nice JPA feature. However, it was limited to either define the fetching as LAZY or EAGER. Fortunately, only prior to JPA 2.1.

This post describes what changed with the release of JPA 2.1 - more particularly one feature extending LAZY/EAGER behavior called entity graphs. The first part explains what these graphs are and what is the difference between them and 2 mentioned fetching strategies. The second part shows how to define entity graphs in entities. The last part shows some code example with defined entity graphs.

Definition

Entity graphs define fetching plans in more fine way that LAZY/EAGER types do. The entity graphs allow us to specify which specific attributes must be fetched eagerly when child entity is loaded. In a very common example of Order - OrderItem relationship, items fetched for every order could have only name and price attributes loaded. The other information can be considered less important or less frequently used, so not necessarily fetched at query time.

In additional, entity graphs come as another solution to n+1 select anti pattern. JPQL queries generated by them automatically include JOIN clause. Thus, all child entities are loaded at once.

Every entity can have 1 or more entity graphs. For the second case, the graphs should be explicitly named to facilitate the loading the correct composition of entities.

3 types of entity graphs exist:

  1. default - applies to all entities, based on fetch attributes of entity fields.
  2. named - entity graph is identified by a name. When the entity graph is used, its name must be specified.
  3. unnamed - entity graph hasn't any identifier. It can be thought as dynamically created entity graph.

Entity graph creation

As told in previous section, entity graph is one of 3 types, so it's defined in 3 different manners. The default entity graph applies implicitly to all entities, thus it's no reason to describe it. The second, named entity graph, is defined through @NamedEntityGraph (or @NamedEntityGraphs if there are 2 or more entity graphs defined) annotation.

Since we talk about named entity graph, the most important attribute is its name, defined in name attribute. Other important attribute is attributeNodes. Defined with @NamedAttributeNode annotation, it's used to specify fetched attributes. For instance, if our class Product has 1 or more ProductItems and we want to fetch only the name of these items, we should put the name of this field inside @NamedAttributeNode's value field.

Unnamed entity graph is created from EntityManager.createEntityGraph(Class) method. Created object (EntityGraph) allows the definition of the same fields as the annotation.

An interesting attribute of entity graphs is subGraph. Thanks to it we can embed entity graphs.

Entity graph examples

Below some code showing the creation and use of entity graphs. First, tested entities:

@Entity
@Table(name = "factory")
public class Factory { 
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @Column(name = "name")
  private String name;

  @OneToMany(mappedBy = "factory", cascade = CascadeType.ALL)
  private Set<Product> products;
// ...
}

@Entity
@Table(name = "product_component")
@IdClass(ProductComponentId.class)
public class ProductComponent {

  @Id
  private long productId;
  @Id
  private long componentId;
  @Id
  private long factoryId;

  @ManyToOne(fetch = FetchType.LAZY)
  @PrimaryKeyJoinColumn(name = "product_id", referencedColumnName = "id")
  private Product product;

  @ManyToOne(fetch = FetchType.LAZY)
  @PrimaryKeyJoinColumn(name = "component_id", referencedColumnName = "id")
  private Product component;

  @ManyToOne(fetch = FetchType.LAZY)
  @PrimaryKeyJoinColumn(name = "factory_id", referencedColumnName = "id")
  private Factory factory;
    
// ...
}

public class ProductComponentId implements Serializable {

  private long productId;

  private long componentId;
    
// ..
}

@Entity
@Table(name = "product")
@NamedEntityGraphs({
  @NamedEntityGraph(name = "Product.namedComponents", attributeNodes = {
      @NamedAttributeNode(value = "components", subgraph = "onlyComponent")
    }, subgraphs = {@NamedSubgraph(name = "onlyComponent", attributeNodes = {@NamedAttributeNode("component")})}
  ),
  @NamedEntityGraph(name = "Product.factory", attributeNodes = {
      @NamedAttributeNode(value = "factory")
    }
  )
})
public class Product {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @Column(name = "name")
  private String name;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "factory_id")
  private Factory factory;

  @OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
  private Set<ProductComponent> components;
    
// ...
}

And the tests illustrating the use of different types of entity graphs:

private EntityManager entityManager;

private Factory ivoryCoastFactory, sugarFacility, componentFactory1, componentFactory2;

private Product chocolate, cacao, sugar;

private TestAppender testAppender;

@Before
public void prepareDataset() {
  String PATTERN = "%m";
  testAppender = new TestAppender();
  testAppender.setLayout(new PatternLayout(PATTERN));
  testAppender.setThreshold(Level.TRACE);
  testAppender.activateOptions();
  Logger.getRootLogger().addAppender(testAppender);

  ivoryCoastFactory = factory("Ivory Coast factory");
  sugarFacility = factory("Sugar factory");
  componentFactory1 = factory("Component factory#1");
  componentFactory2 = factory("Component factory#2");
  chocolate = product("Chocolate", ivoryCoastFactory);
  cacao = product("Cacao", ivoryCoastFactory);
  sugar = product("sugar", sugarFacility);
  ivoryCoastFactory.setProducts(Sets.newHashSet(chocolate, cacao));
  sugarFacility.setProducts(Sets.newHashSet(sugar));

  EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("sampleUnit");

  entityManager = entityManagerFactory.createEntityManager();
  EntityTransaction transaction = entityManager.getTransaction();
  transaction.begin();
  entityManager.persist(chocolate);
  entityManager.persist(cacao);
  entityManager.persist(sugar);
  entityManager.persist(ivoryCoastFactory);
  entityManager.persist(sugarFacility);
  entityManager.persist(componentFactory1);
  entityManager.persist(componentFactory2);
  ProductComponent sugarComponent = component(chocolate, sugar, componentFactory1);
  ProductComponent cacaoComponent = component(chocolate, cacao, componentFactory2);
  entityManager.persist(sugarComponent);
  entityManager.persist(cacaoComponent);
  entityManager.flush();
  entityManager.detach(chocolate);
  entityManager.detach(cacao);
  entityManager.detach(sugar);
  entityManager.detach(ivoryCoastFactory);
  entityManager.detach(sugarFacility);
  entityManager.detach(componentFactory1);
  entityManager.detach(componentFactory2);
  entityManager.detach(sugarComponent);
  entityManager.detach(cacaoComponent);
  transaction.commit();
}

@After
public void clean() {
  entityManager.clear();
}

@Test
public void should_get_only_component_name_with_appropriated_entity_graph() {
  EntityGraph<Product> entityGraph = (EntityGraph<Product>) entityManager.createEntityGraph("Product.namedComponents");
  Map<String, Object> hints = new HashMap<>();
  hints.put("javax.persistence.loadgraph", entityGraph);

  Product product = entityManager.find(Product.class, chocolate.getId(), hints);

  assertThat(product).isNotNull();
  assertThat(product.getComponents()).hasSize(2);
  assertThat(product.getComponents()).extracting("component").extracting("name")
    .containsOnly(sugar.getName(), cacao.getName());
  // factory.isNotNull() is not sufficient to prove query on factory table
  assertThat(product.getFactory().getName()).isNotNull();
  assertThat(testAppender.productSelectQueries).hasSize(1);
  assertThat(testAppender.productJoinQueries).hasSize(1);
  assertThat(testAppender.componentSelectQueries).isEmpty();
  assertThat(testAppender.componentJoinQueries).hasSize(1);
  assertThat(testAppender.factorySelectQueries).hasSize(1);
  assertThat(testAppender.factoryJoinQueries).isEmpty();
}

@Test
public void should_query_factories_twice_because_of_field_not_returned_by_entity_graph() throws InterruptedException {
  EntityGraph<Product> entityGraph = (EntityGraph<Product>) entityManager.createEntityGraph("Product.namedComponents");
  Map<String, Object> hints = new HashMap<>();
  hints.put("javax.persistence.loadgraph", entityGraph);

  Product product = entityManager.find(Product.class, chocolate.getId(), hints);

  assertThat(product).isNotNull();
  assertThat(product.getComponents()).hasSize(2);
  assertThat(product.getComponents()).extracting("component").extracting("name")
    .containsOnly(sugar.getName(), cacao.getName());
  assertThat(product.getComponents()).extracting("factory").extracting("name")
    .containsOnly(componentFactory1.getName(), componentFactory2.getName());
  assertThat(product.getFactory().getName()).isNotNull();
  // factory.isNotNull() is not sufficient to prove query on factory table
  assertThat(product.getFactory().getName()).isNotNull();
  assertThat(testAppender.productSelectQueries).hasSize(1);
  assertThat(testAppender.productJoinQueries).hasSize(1);
  assertThat(testAppender.componentSelectQueries).isEmpty();
  assertThat(testAppender.componentJoinQueries).hasSize(1);
  // 1 query for product.factory.name + 2 queries, each for one component, for product.components.factory.name
  assertThat(testAppender.factorySelectQueries).hasSize(3);
  assertThat(testAppender.factoryJoinQueries).isEmpty();
}


@Test
public void should_get_lazy_loaded_factory_within_a_single_query_thanks_to_entity_graph() {
  EntityGraph<Product> entityGraph = 
      (EntityGraph<Product>) entityManager.createEntityGraph("Product.factory");
  Map<String, Object> hints = new HashMap<>();
  hints.put("javax.persistence.loadgraph", entityGraph);

  Product product = entityManager.find(Product.class, chocolate.getId(), hints);

  assertThat(product).isNotNull();
  assertThat(product.getFactory()).isNotNull();
  assertThat(testAppender.productSelectQueries).hasSize(1);
  assertThat(testAppender.productJoinQueries).isEmpty();
  assertThat(testAppender.componentSelectQueries).isEmpty();
  assertThat(testAppender.componentJoinQueries).isEmpty();
  assertThat(testAppender.factorySelectQueries).isEmpty();
  assertThat(testAppender.factoryJoinQueries).hasSize(1);
}

@Test
public void should_load_entity_graph_from_criteria_api() {
  EntityGraph entityGraph = entityManager.getEntityGraph("Product.factory");
  CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
  CriteriaQuery<Product> criteriaQuery = criteriaBuilder.createQuery(Product.class);
  Root<Product> rootProduct = criteriaQuery.from(Product.class);
  criteriaQuery = criteriaQuery.select(rootProduct);

  List<Product> products = entityManager
    .createQuery(criteriaQuery)
    .setHint("javax.persistence.fetchgraph", entityGraph)
    .getResultList();

  assertThat(products).hasSize(3);
  assertThat(testAppender.productSelectQueries).hasSize(1);
  assertThat(testAppender.productJoinQueries).isEmpty();
  assertThat(testAppender.componentSelectQueries).isEmpty();
  assertThat(testAppender.componentJoinQueries).isEmpty();
  assertThat(testAppender.factorySelectQueries).isEmpty();
  assertThat(testAppender.factoryJoinQueries).hasSize(1);
}

@Test
public void should_add_dynamic_fields_and_create_unnamed_entity_graph() {
  // This entity graph is similar to the previous one with the difference that
  // components are joined programatically
  EntityGraph entityGraph = entityManager.createEntityGraph(Product.class);
  entityGraph.addAttributeNodes("components");
  CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
  CriteriaQuery<Product> criteriaQuery = criteriaBuilder.createQuery(Product.class);
  Root<Product> rootProduct = criteriaQuery.from(Product.class);
  criteriaQuery = criteriaQuery.select(rootProduct).distinct(true);

  List<Product> products = entityManager
    .createQuery(criteriaQuery)
    .setHint("javax.persistence.fetchgraph", entityGraph)
    .getResultList();

  assertThat(products).hasSize(3);
  assertThat(testAppender.productSelectQueries).hasSize(1);
  assertThat(testAppender.productJoinQueries).hasSize(1);
  assertThat(testAppender.componentSelectQueries).isEmpty();
  assertThat(testAppender.componentJoinQueries).hasSize(1);
  assertThat(testAppender.factorySelectQueries).isEmpty();
  assertThat(testAppender.factoryJoinQueries).isEmpty();
} 

private static final class TestAppender extends ConsoleAppender {

  private List<String> productSelectQueries = new ArrayList<>();
  private List<String> productJoinQueries = new ArrayList<>();
  private List<String> componentSelectQueries = new ArrayList<>();
  private List<String> componentJoinQueries = new ArrayList<>();
  private List<String> factorySelectQueries = new ArrayList<>();
  private List<String> factoryJoinQueries = new ArrayList<>();

  @Override
  public void append(LoggingEvent event) {
    String logMessage = event.getRenderedMessage();
    if (logMessage.contains("from factory")) {
      factorySelectQueries.add(logMessage);
    }
    if (logMessage.contains("join factory")) {
      factoryJoinQueries.add(logMessage);
    }
    if (logMessage.contains("from product")) {
      productSelectQueries.add(logMessage);
    }
    if(logMessage.contains("join product")) {
      productJoinQueries.add(logMessage);
    }
    if (logMessage.contains("from product_component")) {
      componentSelectQueries.add(logMessage);
    }
    if (logMessage.contains("join product_component")) {
      componentJoinQueries.add(logMessage);
    }
  }
}

This post shows an alternative to static method of fetching management - entity graphs. The first part describes the differences with historical methods, EAGER/LAZY fetching. We could see that entity graphs bring a lot more of flexibility because we can decide to load only a small part of associated entities. The second part describes how entity graphs are created while the last part shows it through some test cases.