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);
}
dendimiiii
  • 1,659
  • 3
  • 15
  • 26

6 Answers6

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 :(

Community
  • 1
  • 1
Chris Spencer
  • 614
  • 7
  • 3
  • 13
    instead of changing the verb of the method one can also add an arbitrary string between the verb and "by" like `findEagerById` or `findLessEagerById` – Jens Schauder Jun 21 '18 at 16:43
  • 1
    @JensSchauder is there a similar solution for findAll? – Jordan Mackie Aug 09 '18 at 15:56
  • 4
    @JordanMackie It results in somewhat strange names but you can end the name after `By` which makes it a variant of `findAll`: `findAllBy`, `findEverythingBy` ... – Jens Schauder Aug 10 '18 at 04:11
  • for #3, do you have to do the joins? I thought that's what the graph was for – Matt Broekhuis Jan 22 '19 at 17:02
  • It's weird that I can use `findOneWithMembershipByUserId` with annotation, but not `findAllWithMemberShip`. – LunaticJape Feb 04 '19 at 17:42
  • Comment by @JensSchauder should be the accepted answer. Also works like `findAllWithThisAndThatFieldByOtherField` – Hans Wouters Nov 26 '19 at 11:40
  • 1
    @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`. – Max May 22 '21 at 03:14
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.

Réda Housni Alaoui
  • 1,244
  • 2
  • 15
  • 22
  • I made my answer more accurate. – Réda Housni Alaoui Nov 26 '16 at 19:02
  • 1
    This is very useful. I have an outstanding Spring Data ticket around this issue https://jira.spring.io/browse/DATAJPA-645?filter=-2. Have you considered incorporating to the Spring Data project ? Does it work with QueryDsl Predicate methods? – Alan Hay Feb 01 '17 at 20:15
  • Hello Alan, before creating https://github.com/Cosium/spring-data-jpa-entity-graph, I have asked dynamic entitygraph inclusion in ticket https://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 ;) – Réda Housni Alaoui Feb 02 '17 at 16:00
  • Great. Thanks. Interesting reading the discussion on your ticket! – Alan Hay Feb 02 '17 at 21:06
  • Hey Réda, thank you for a solution, looks very promising, however it does not compatible with Java 12 version – Oleg Baranenko Nov 05 '19 at 19:41
  • Hi @OlegBaranenko , please take a look at https://github.com/Cosium/spring-data-jpa-entity-graph/issues/23 – Réda Housni Alaoui Nov 06 '19 at 15:53
13

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();

   ...

}

Grigory Kislin
  • 16,647
  • 10
  • 125
  • 197
  • 1
    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")` – Oleg Kuts Nov 19 '16 at 18:06
  • @ChrisSpencer's answer seems to suggest combining jpql and entity graphs doesn't work, can you confirm that this works? – Jordan Mackie Aug 09 '18 at 16:00
  • 3
    @JordanMackie I've used it in my project: https://github.com/gkislin/topjava/blob/master/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java – Grigory Kislin Aug 10 '18 at 10:10
  • 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.5` – Blockost Jan 04 '22 at 16:16
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

mowwwalker
  • 16,634
  • 25
  • 104
  • 157
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.

Femi
  • 64,273
  • 8
  • 118
  • 148
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.

smile
  • 498
  • 7
  • 18