Spring Data JPA And NamedEntityGraphs
Asked Answered
H

6

59

currently I am wrestling with being able to fetch only the data I need. The findAll() method needs to fetch data dependant on where its getting called. I do not want to end up writing different methods for each entity graph. Also, I would avoid calling entitymanagers and forming the (repetitive) queries myself. Basicly I want to use the build in findAll method, but with the entity graph of my liking. Any chance?

@Entity
@Table(name="complaints")
@NamedEntityGraphs({
    @NamedEntityGraph(name="allJoinsButMessages", attributeNodes = {
            @NamedAttributeNode("customer"),
            @NamedAttributeNode("handling_employee"),
            @NamedAttributeNode("genre")
    }),
    @NamedEntityGraph(name="allJoins", attributeNodes = {
            @NamedAttributeNode("customer"),
            @NamedAttributeNode("handling_employee"),
            @NamedAttributeNode("genre"),
            @NamedAttributeNode("complaintMessages")
    }),
    @NamedEntityGraph(name="noJoins", attributeNodes = {

    })
})
public class Complaint implements Serializable{
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private long id;

    private Timestamp date;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer")
    private User customer;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "handling_employee")
    private User handling_employee;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="genre")
    private Genre genre;

    private boolean closed;

    @OneToMany(mappedBy = "complaint", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<ComplaintMessage> complaintMessages = new ArrayList<ComplaintMessage>();

//getters and setters
}

And my JPARepository

@Repository
public interface ComplaintRepository extends JpaRepository<Complaint, Long>{

    List<Complaint> findByClosed(boolean closed);

    @EntityGraph(value = "allJoinsButMessages" , type=EntityGraphType.FETCH)
    @Override
    List<Complaint> findAll(Sort sort);
}
Hepta answered 11/8, 2015 at 14:3 Comment(1)
I think the differently named methods solution would be more appropriate.Pile
I
59

We ran into a similar problem and devised several prospective solutions but there doesn't seem to be an elegant solution for what seems to be a common problem.

1) Prefixes. Data jpa affords several prefixes (find, get, ...) for a method name. One possibility is to use different prefixes with different named graphs. This is the least work but hides the meaning of the method from the developer and has a great deal of potential to cause some non-obvious problems with the wrong entities loading.

@Repository
@Transactional
public interface UserRepository extends CrudRepository<User, Integer>, UserRepositoryCustom {
    @EntityGraph(value = "User.membershipYearsAndPreferences", type = EntityGraphType.LOAD)
    User findByUserID(int id);

    @EntityGraph(value = "User.membershipYears", type = EntityGraphType.LOAD)
    User readByUserId(int id);
}

2) CustomRepository. Another possible solutions is to create custom query methods and inject the EntityManager. This solution gives you the cleanest interface to your repository because you can name your methods something meaningful, but it is a significant amount of complexity to add to your code to provide the solution AND you are manually grabbing the entity manager instead of using Spring magic.

interface UserRepositoryCustom {
    public User findUserWithMembershipYearsById(int id);
}

class UserRepositoryImpl implements UserRepositoryCustom {
    @PersistenceContext
    private EntityManager em;
    @Override
    public User findUserWithMembershipYearsById(int id) {
        User result = null;
        List<User> users = em.createQuery("SELECT u FROM users AS u WHERE u.id = :id", User.class)
                .setParameter("id", id)
                .setHint("javax.persistence.fetchgraph", em.getEntityGraph("User.membershipYears"))
                .getResultList();
        if(users.size() >= 0) {
            result = users.get(0);
        }
        return result;
    }
}

@Repository
@Transactional
public interface UserRepository extends CrudRepository<User, Integer>, UserRepositoryCustom {
    @EntityGraph(value = "User.membershipYearsAndPreferences", type = EntityGraphType.LOAD)
    User findByUserID(int id);
}

3) JPQL. Essentially this is just giving up on named entity graphs and using JPQL to handle your joins for you. Non-ideal in my opinion.

@Repository
@Transactional
public interface UserRepository extends CrudRepository<User, Integer>, UserRepositoryCustom {
    @EntityGraph(value = "User.membershipYearsAndPreferences", type = EntityGraphType.LOAD)
    User findByUserID(int id);

    @Query("SELECT u FROM users WHERE u.id=:id JOIN??????????????????????????")
    User findUserWithTags(@Param("id") final int id);
}

We went with option 1 because it is the simplest in implementation but this does mean when we use our repositories we have have to look at the fetch methods to make sure we are using the one with the correct entity graph. Good luck.

Sources:

I don't have enough reputation to post all of my sources. Sorry :(

Inseminate answered 27/10, 2015 at 16:52 Comment(7)
instead of changing the verb of the method one can also add an arbitrary string between the verb and "by" like findEagerById or findLessEagerByIdCapillarity
@JensSchauder is there a similar solution for findAll?Nifty
@JordanMackie It results in somewhat strange names but you can end the name after By which makes it a variant of findAll: findAllBy, findEverythingBy ...Capillarity
for #3, do you have to do the joins? I thought that's what the graph was forMoye
It's weird that I can use findOneWithMembershipByUserId with annotation, but not findAllWithMemberShip.Recipience
Comment by @JensSchauder should be the accepted answer. Also works like findAllWithThisAndThatFieldByOtherFieldIllegible
@JordanMackie, yes you can just write findAllEagerBy, getAllWithAssociationsBy, etc. If you just finish your name with By (like next pattern find\get\read... + All + ... + By) it'll be treated the same as the default method findAll.Mizzenmast
S
23

We had the same issue and built a Spring Data JPA extension to solve it :

https://github.com/Cosium/spring-data-jpa-entity-graph

This extension allows to pass named or dynamically built EntityGraph as an argument of any repository method.

With this extension, you would have this method immediatly available:

List<Complaint> findAll(Sort sort, EntityGraph entityGraph);

And be able to call it with an EntityGraph selected at runtime.

Shelter answered 25/11, 2016 at 0:25 Comment(6)
I made my answer more accurate.Dolmen
This is very useful. I have an outstanding Spring Data ticket around this issue jira.spring.io/browse/DATAJPA-645?filter=-2. Have you considered incorporating to the Spring Data project ? Does it work with QueryDsl Predicate methods?Toluol
Hello Alan, before creating github.com/Cosium/spring-data-jpa-entity-graph, I have asked dynamic entitygraph inclusion in ticket jira.spring.io/browse/DATAJPA-749. But it was refused. QueryDsl support is included. There are some test cases for that. But I don't use QueryDsl on my projects. If you find bugs don't hesitate to report them on the tracker ;)Dolmen
Great. Thanks. Interesting reading the discussion on your ticket!Toluol
Hey Réda, thank you for a solution, looks very promising, however it does not compatible with Java 12 versionAllegro
Hi @OlegBaranenko , please take a look at github.com/Cosium/spring-data-jpa-entity-graph/issues/23Dolmen
L
14

Use @EntityGraph together with @Query

@Repository
public interface ComplaintRepository extends JpaRepository<Complaint, Long>{

   @EntityGraph(value = "allJoinsButMessages" , type=EntityGraphType.FETCH)
   @Query("SELECT c FROM Complaint ORDER BY ..")
   @Override
   List<Complaint> findAllJoinsButMessages();

   @EntityGraph(value = "allJoins" , type=EntityGraphType.FETCH)
   @Query("SELECT c FROM Complaint ORDER BY ..")
   @Override
   List<Complaint> findAllJoin();

   ...

}

Leggy answered 17/10, 2016 at 10:40 Comment(4)
I like your solution. But I have faced problem using NamedEntityGraphs - I can't return custom object like this @Query("SELECT new DTO(f) from Entity f")Rooney
@ChrisSpencer's answer seems to suggest combining jpql and entity graphs doesn't work, can you confirm that this works?Nifty
@JordanMackie I've used it in my project: github.com/gkislin/topjava/blob/master/src/main/java/ru/…Leggy
It's been a long time but I keep seeing that @EntityGraph does not work with @Query whereas it does work fine when I test it. Not sure where this assumption comes from... Older versions maybe ? I'm using spring-data-jpa 2.5.5Neutralize
U
5

Using the @EntityGraph annotation on a derived query is possible, as I found out from This article. The article has the example:

@Repository
public interface ArticleRepository extends JpaRepository<Article,Long> {
   @EntityGraph(attributePaths = "topics")
   Article findOneWithTopicsById(Long id);
}

But I don't think there's anything special about "with" and you can actually have anything between find and By. I tried these and they work (this code is Kotlin, but the idea is the same):

interface UserRepository : PagingAndSortingRepository<UserModel, Long> {
    @EntityGraph(attributePaths = arrayOf("address"))
    fun findAnythingGoesHereById(id: Long): Optional<UserModel>

    @EntityGraph(attributePaths = arrayOf("address"))
    fun findAllAnythingGoesHereBy(pageable: Pageable): Page<UserModel>
}

The article had mentioned the caveat that you can't create a method similar to findAll which will query all records without having a By condition and uses findAllWithTopicsByIdNotNull() as an example. I found that just including By by itself at the end of the name was sufficient: findAllWithTopicsBy(). A little more terse but maybe a little more confusing to read. Using method names which end with just By without any condition may be in danger of breaking in future versions in Spring since it doesn't seem like an intended use of derived queries name.

It looks like the code for parsing derived query names in Spring is here on github. You can look there in case you're curious about what's possible for derived queries repository method names.

These are the spring docs for derived queries.

This was tested with spring-data-commons-2.2.3.RELEASE

Underlaid answered 18/12, 2019 at 21:25 Comment(0)
C
2

EDIT: this doesn't actually work. Ended up having to go with https://github.com/Cosium/spring-data-jpa-entity-graph. The default method LOOKS correct, but doesn't successfully override the annotations.

Using JPA, what I found works is to use a default method, with a different EntityGraph annotation:

@Repository
public interface ComplaintRepository extends JpaRepository<Complaint, Long>{

    List<Complaint> findByClosed(boolean closed);

    @EntityGraph(attributePaths = {"customer", "genre", "handling_employee" }, type=EntityGraphType.FETCH)
    @Override
    List<Complaint> findAll(Sort sort);

    @EntityGraph(attributePaths = {"customer", "genre", "handling_employee", "messages" }, type=EntityGraphType.FETCH)
    default List<Complaint> queryAll(Sort sort){
      return findAll(sort);
    }
}

You don't have to do any of the re-implementation, and can customize the entity graph using the existing interface.

Cavill answered 5/4, 2021 at 5:37 Comment(0)
D
1

Can you try create EntiyGraph name with child that you will request and give same name to the find all method. Ex:

@EntityGraph(value = "fetch.Profile.Address.record", type = EntityGraphType.LOAD)
 Employee getProfileAddressRecordById(long id);

For your case:

@NamedEntityGraph(name="all.Customer.handling_employee.genre", attributeNodes = {
        @NamedAttributeNode("customer"),
        @NamedAttributeNode("handling_employee"),
        @NamedAttributeNode("genre")
})

method name in repository

@EntityGraph(value = "all.Customer.handling_employee.genre" , type=EntityGraphType.FETCH)
 findAllCustomerHandlingEmployeeGenre

This way you can keep track of different findAll methods.

Deputize answered 9/2, 2017 at 20:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.