/spring-data-jpa-entity-graph

Spring Data JPA extension allowing full dynamic usage of EntityGraph on repositories

Primary LanguageJavaMIT LicenseMIT

Build Status Maven Central

Spring Data JPA EntityGraph

Spring Data JPA only supports EntityGraph through annotations.
Thus, for a repository method, you must select at most one EntityGraph before compilation.
This prevents you from choosing the best EntityGraph considering the runtime context 💔

Spring Data JPA EntityGraph allows you to choose EntityGraph at runtime! This choice is elegantly made by passing EntityGraph, as an argument, to any Spring Data JPA repository method 😍

Quick start

  1. Select the correct version from the compatibility matrix

  2. In addition to Spring Data JPA, add Spring Data JPA EntityGraph dependency :

    <dependency>
      <groupId>com.cosium.spring.data</groupId>
      <artifactId>spring-data-jpa-entity-graph</artifactId>
      <version>${spring-data-jpa-entity-graph.version}</version>
    </dependency>
  3. In your Spring configuration, set the repository factory bean class to EntityGraphJpaRepositoryFactoryBean :

    @Configuration
    @EnableJpaRepositories(repositoryFactoryBeanClass = EntityGraphJpaRepositoryFactoryBean.class)
    public class DataRepositoryConfiguration {
        //...
    }
  4. If you want to use type safe EntityGraphs , add those dependencies:

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-jpamodelgen</artifactId>
      <version>${hibernate.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.cosium.spring.data</groupId>
      <artifactId>spring-data-jpa-entity-graph-generator</artifactId>
      <version>${spring-data-jpa-entity-graph.version}</version>
      <scope>provided</scope>
    </dependency>

Usage

On custom repository methods

If you want to define a custom repository method accepting an EntityGraph, just do it™.

For example, given an entity having attribute named label of type String, you could declare and use a repository like this:

interface MyRepository extends Repository<MyEntity, Long> {
	Optional<MyEntity> findByLabel(String label, EntityGraph entityGraph);
}
myRepository.findByLabel("foo", NamedEntityGraph.loading("bar"));

On pre-defined repository methods

Spring Data JPA EntityGraph provides repository interfaces extending Spring Data JPA. For each Spring Data JPA pre-defined method, the provided interfaces overload the method with EntityGraph as an additional argument.

For example, Spring Data JPA CrudRepository defines method Optional<T> findById(ID id). This method is overloaded by Spring Data JPA EntityGraph EntityGraphCrudRepository as Optional<T> findById(ID id, EntityGraph entityGraph).

To be able to use these overloaded methods, you must extend one of Spring Data JPA EntityGraph provided repository interfaces.

The following matrix describes the mapping between Spring Data JPA and Spring Data JPA EntityGraph :

Spring Data JPA Spring Data JPA EntityGraph
JpaRepository EntityGraphJpaRepository
JpaSpecificationExecutor EntityGraphJpaSpecificationExecutor
QuerydslPredicateExecutor EntityGraphQuerydslPredicateExecutor
CrudRepository EntityGraphCrudRepository
PagingAndSortingRepository EntityGraphPagingAndSortingRepository
QueryByExampleExecutor EntityGraphQueryByExampleExecutor

For example, if you wanted to use Optional<T> findById(ID id, EntityGraph entityGraph), you could declare and use your repository like this:

interface MyRepository extends EntityGraphCrudRepository<MyEntity, Long> {
}
myRepository.findById(1L, NamedEntityGraph.loading("foo"));

EntityGraph provided implementations

DynamicEntityGraph

DynamicEntityGraph class allows you to create on-the-fly EntityGraph by defining their attribute paths. This is similar to Spring's ad-hoc attribute paths.

For example, let's consider the following entities :

@Entity
class Maker {
   //...
   @OneToOne(fetch = FetchType.LAZY)
   private Address address;
   //...
}
@Entity
class Product {
    @Id
    private long id = 0;
    private String name;
    @ManyToOne(fetch = FetchType.LAZY)
    private Brand brand;
    @ManyToOne(fetch = FetchType.LAZY)
    private Maker maker;
    //...
}	

You could declare the following repository :

public interface MyRepository extends Repository<Product, Long> {
    List<Product> findByName(String name, EntityGraph entityGraph);
}

Then perform the findByName using ad-hoc product(brand, maker(address)) EntityGraph :

myRepository.findById(1L, DynamicEntityGraph.loading().addPath("brand").addPath("maker", "address").build());

NamedEntityGraph

NamedEntityGraph class allows you to reference an EntityGraph by its name. Such EntityGraph must have beforehand been registered through EntityManager#createEntityGraph(String graphName) or declared using @NamedEntityGraph annotation.

For example, let's consider the following entity :

@NamedEntityGraphs(value = {
    @NamedEntityGraph(name = "productBrand", attributeNodes = {
        @NamedAttributeNode("brand")
    })
})
@Entity
public class Product {
    @Id
    private long id = 0;
    private String name;
    @ManyToOne(fetch = FetchType.LAZY)
    private Brand brand;
    //...
}	

You could declare the following repository :

public interface MyRepository extends Repository<Product, Long> {
    List<Product> findByName(String name, EntityGraph entityGraph);
}

Then perform the findByName query using productBrand named EntityGraph like this :

myRepository.findByName("foo", NamedEntityGraph.loading("productBrand"));

Type safe EntityGraph

Composing entity graphs by hand can be tedious and error-prone. Wouldn't it be great to benefit from autocompletion and strong type checking while composing your entity graph?

Spring Data JPA EntityGraph Generator has you covered.

This annotation processor makes use of the JPA metamodel information (part of JPA specification) generated by the tool of your choice (e.g. hibernate-jpamodelgen) to generate EntityGraph composers allowing you to safely and easily compose EntityGraph at runtime.

You first need to add a JPA metamodel information generator. hibernate-jpamodelgen is great and should be compatible with any JPA ORM:

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-jpamodelgen</artifactId>
  <version>${hibernate.version}</version>
  <scope>provided</scope>
</dependency>

Then add the entity graph generator annotation processor dependency:

<dependency>
  <groupId>com.cosium.spring.data</groupId>
  <artifactId>spring-data-jpa-entity-graph-generator</artifactId>
  <version>${spring-data-jpa-entity-graph.version}</version>
  <scope>provided</scope>
</dependency>

After compiling your project, you should find XEntityGraph classes where X is the name of your Entity.

For example, let's consider the following entity :

@Entity
public class Product {
    @Id
    private long id = 0;
    private String name;
    @ManyToOne(fetch = FetchType.LAZY)
    private Brand brand;
    //...
}	

You could declare the following repository :

public interface MyRepository extends Repository<Product, Long> {
    List<Product> findByName(String name, EntityGraph entityGraph);
}

spring-data-jpa-entity-graph-generator will detect Product and generate in return ProductEntityGraph class. You could then perform the findByName query using product(brand, maker(address)) EntityGraph like this :

productRepository.findById(1L, ProductEntityGraph
                               .____()
                               .brand()
                               .____
                               .maker()
                               .address()
                               .____
                               .____());

Repository default EntityGraph

You can declare at most one default EntityGraph per repository by overriding EntityGraphRepository#defaultEntityGraph method.

Calling any repository query method - custom or pre-defined - without EntityGraph or with an EntityGraph#NOOP equivalent will lead to the default EntityGraph usage. Otherwise, the EntityGraph passed as query method argument will always have priority.

You could declare a repository as follows :

interface MyRepository extends EntityGraphCrudRepository<MyEntity, Long> {
  @Override
  default Optional<EntityGraph> defaultEntityGraph() {
    return NamedEntityGraph.loading("foo").execute(Optional::of);
  } 
  
  List<MyEntity> findByName(String name);
  List<MyEntity> findByName(String name, EntityGraph entityGraph);
}

The following snippets will lead to the default EntityGraph usage:

myRepository.findById(1L);
myRepository.findById(1L, EntityGraph.NOOP);
myRepository.findByName("bar");

The following snippets will ignore the default EntityGraph and instead use the EntityGraph passed as argument:

myRepository.findById(1L, NamedEntityGraph.loading("alice"));
myRepository.findByName("bar", NamedEntityGraph.loading("barry"));

Chaining EntityGraph definition with query execution

If you prefer fluent apis, you can use any instance of EntityGraph like this:

List<Product> products = ProductEntityGraph
                          .____()
                          .brand()
                          .____
                          .maker()
                          .address()
                          .____
                          .____()
                          .execute(entityGraph -> myRepository.findByLabel("foo", entityGraph));

EntityGraph Semantics

JPA 2.1 defines 2 semantics:

Spring Data JPA EntityGraph uses Load Graph Semantics as the default semantic. This means if you don't define a semantic, EntityGraph implementations will be built using Load Graph Semantics.

Each provided EntityGraph implementation provides an easy way to select the Graph Semantics.

Demo

You can play with https://github.com/Cosium/spring-data-jpa-entity-graph-sample to see the extension in action in a simple Spring Application.

Compatibility matrix

Spring Data JPA version Spring Data JPA EntityGraph version
2.7.x Maven Central 2.7.x
2.6.x Maven Central 2.6.x
2.5.x Maven Central 2.5.x
2.4.x Maven Central 2.4.x
2.3.x Maven Central 2.3.x
2.2.x Maven Central 2.2.x
2.1.x Maven Central 2.1.x
2.0.x Maven Central 2.0.x
1.11.x Maven Central 1.11.x
1.10.x Maven Central 1.10.x

For example, if you were using spring-data-jpa 2.2.x in your project, you would need to select any spring-data-jpa-entity-graph 2.2.x. Thus spring-data-jpa-entity-graph 2.2.8 would be eligible.

"Making JPA Great Again" talk

This talk was given at Paris JUG in January 2019.

The slides are in english.
The video is in French:
Alt text

Genesis

This project was created following spring-projects/spring-data-jpa#1120 discussion.