2015-04-13 5 views
44

Ich habe eine Beziehung zwischen drei Modellobjekt in meinem Projekt (Modell und Repository-Schnipsel am Ende des Beitrags.Wie funktioniert die FetchMode Arbeit in Spring Data JPA

Wenn ich rufe PlaceRepository.findById es tut Feuer drei Auswahlabfragen :

("SQL")

  1. SELECT * FROM place p where id = arg
  2. SELECT * FROM user u where u.id = place.user.id
  3. SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id

Das ist eher ungewöhnliches Verhalten (für mich). Soweit ich nach dem Lesen der Hibernate-Dokumentation feststellen kann, sollte es immer JOIN-Abfragen verwenden. Es gibt keinen Unterschied in den Abfragen, wenn FetchType.LAZY zu FetchType.EAGER in der Place Klasse geändert wird (Abfrage mit zusätzlichen SELECT), das gleiche für die City Klasse, wenn FetchType.LAZY geändert zu FetchType.EAGER (Abfrage mit JOIN).

Wenn ich CityRepository.findById Unterdrückung von Bränden zwei wählt:

  1. SELECT * FROM city c where id = arg
  2. SELECT * FROM state s where id = city.state.id

Mein Ziel ein, die sam Verhalten in allen Situationen zu haben ist (entweder immer JOIN oder SELECT, JOIN bevorzugt aber).

Modelldefinitionen:

Ort:

@Entity 
@Table(name = "place") 
public class Place extends Identified { 

    @Fetch(FetchMode.JOIN) 
    @ManyToOne(fetch = FetchType.LAZY) 
    @JoinColumn(name = "id_user_author") 
    private User author; 

    @Fetch(FetchMode.JOIN) 
    @ManyToOne(fetch = FetchType.LAZY) 
    @JoinColumn(name = "area_city_id") 
    private City city; 
    //getters and setters 
} 

Stadt:

@Entity 
@Table(name = "area_city") 
public class City extends Identified { 

    @Fetch(FetchMode.JOIN) 
    @ManyToOne(fetch = FetchType.LAZY) 
    @JoinColumn(name = "area_woj_id") 
    private State state; 
    //getters and setters 
} 

Repositorys:

PlaceRepository

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom { 
    Place findById(int id); 
} 

UserRepository:

public interface UserRepository extends JpaRepository<User, Long> { 
     List<User> findAll(); 
    User findById(int id); 
} 

CityRepository:

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {  
    City findById(int id); 
} 
+0

Hava ein Blick auf 5 Möglichkeiten faul relationsships zu initialisieren: http://www.thoughts-on-java.org/5-ways-to-initialize-lazy-relations-and-when-to-use -them/ – GKislin

Antwort

59

Ich denke, dass Spring Data die FetchMode ignoriert. Ich benutze immer die @NamedEntityGraph und @EntityGraph Anmerkungen, wenn sie mit Spring Data

@Entity 
@NamedEntityGraph(name = "GroupInfo.detail", 
    attributeNodes = @NamedAttributeNode("members")) 
public class GroupInfo { 

    // default fetch mode is lazy. 
    @ManyToMany 
    List<GroupMember> members = new ArrayList<GroupMember>(); 

    … 
} 

@Repository 
public interface GroupRepository extends CrudRepository<GroupInfo, String> { 

    @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD) 
    GroupInfo getByGroupName(String name); 

} 

Überprüfen Sie die Dokumentation here

+0

Ich scheint nicht für mich zu arbeiten. Ich meine, es funktioniert, aber ... Wenn ich Repository mit '@EntityGraph' annotieren, funktioniert es nicht (normalerweise). Beispiel: 'Place findById (int id); funktioniert aber' List findAll(); 'endet mit der Exception' org.springframework.data.mapping.PropertyReferenceException: Es wurde keine Eigenschaft für Typ Place! 'Gefunden. Es funktioniert, wenn ich '@Query manuell hinzufüge (" wähle p von Stelle p ")'. Scheint jedoch wie eine Umgehung. – SirKometa

+0

Vielleicht funktioniert dosent auf findAll(), da es eine existierende Methode von der JpaRepository-Schnittstelle ist, während Ihre andere Methode "findById" eine benutzerdefinierte Abfrage-Methode ist, die zur Laufzeit generiert wird. – wesker317

+0

Ich habe beschlossen, dies als die richtige Antwort zu markieren, da es das Beste ist. Es ist jedoch nicht perfekt. Es funktioniert in den meisten Szenarien, aber bisher habe ich Bugs in Spring-Data-JPA mit komplexeren EntityGraphs bemerkt. Danke :) – SirKometa

27

Zunächst einmal arbeiten, @Fetch(FetchMode.JOIN) und @ManyToOne(fetch = FetchType.LAZY) sind antagonistisch, ein eifriger fetching anweist, während der andere was darauf hindeutet, ein LAZY holen.

Eager fetching ist rarely a good choice und für ein vorhersagbares Verhalten, sind Sie besser dran, die Abfrage-Zeit JOIN FETCH Direktive:

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom { 

    @Query(value = "SELECT p FROM Place p LEFT JOIN FETCH p.author LEFT JOIN FETCH p.city c LEFT JOIN FETCH c.state where p.id = :id") 
    Place findById(@Param("id") int id); 
} 

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom { 
    @Query(value = "SELECT c FROM City c LEFT JOIN FETCH c.state where c.id = :id") 
    City findById(@Param("id") int id); 
} 
+1

IST es möglich, dasselbe Ergebnis mit Kriterien-API und Federdatenspezifikationen zu erzielen? – svlada

+2

Nicht der Abrufteil, für den JPA-Abrufprofile erforderlich sind. –

+0

Vlad Mihalcea, können Sie den Link mit einem Beispiel teilen, wie dies mithilfe von Spring Data JPA-Kriterien (Spezifikation) funktioniert? Bitte –

2

FetchType.LAZY“ wird nur Feuer für Primärtabelle. Wenn Sie in Ihrem Code eine andere Methode aufrufen, die eine Abhängigkeit von der übergeordneten Tabelle hat, wird die Abfrage ausgelöst, um diese Tabelleninformationen zu erhalten. (FIRES MULTIPLE SELECT)

"FetchType.EAGER" wird Verknüpfung aller Tabelle einschließlich relevante übergeordnete Tabellen direkt erstellen. (VERWENDET JOIN)

Wann verwendet: Sie zwangsweise notwendig Angenommen abhängig Elterntabelle Angaben stammen zu verwenden, dann FetchType.EAGER wählen. Wenn Sie nur Informationen für bestimmte Datensätze benötigen, verwenden Sie FetchType.LAZY.

Denken Sie daran, FetchType.LAZY benötigt eine aktive db-Sitzungsfactory an der Stelle in Ihrem Code, an der Sie die Elterntabelleninformationen abrufen möchten.

z. für LAZY:

.. Place fetched from db from your dao loayer 
.. only place table information retrieved 
.. some code 
.. getCity() method called... Here db request will be fired to get city table info 

Additional reference

+0

Interessanterweise hat diese Antwort mich auf den richtigen Weg gebracht, 'NamedEntityGraph' zu verwenden, da ich einen nicht hydratisierten Objektgraph wollte. –

12

Frühlings-JPA erstellt die Abfrage des Entität-Manager, und Hibernate ignoriert den Modus holen, wenn die Abfrage von der Entität Manager gebaut wurde.

Das Folgende ist die Arbeit um, dass ich verwendet:

  1. Implement a custom repository witch inherits from SimpleJpaRepository

  2. Aufschalten der Methode getQuery(Specification<T> spec, Sort sort):

    @Override 
    protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) { 
        CriteriaBuilder builder = entityManager.getCriteriaBuilder(); 
        CriteriaQuery<T> query = builder.createQuery(getDomainClass()); 
    
        Root<T> root = applySpecificationToCriteria(spec, query); 
        query.select(root); 
    
        applyFetchMode(root); 
    
        if (sort != null) { 
         query.orderBy(toOrders(sort, root, builder)); 
        } 
    
        return applyRepositoryMethodMetadata(entityManager.createQuery(query)); 
    } 
    

    In der Mitte des Verfahrens, fügen applyFetchMode(root);, um den Abrufmodus anzuwenden, damit Hibernate die Abfrage mit dem richtigen Join erstellt.

    (Leider müssen wir das ganze Verfahren und damit verbundene private Methoden von der Basisklasse kopieren, da es kein anderer Erweiterungspunkt war.)

  3. Implement applyFetchMode:

    private void applyFetchMode(Root<T> root) { 
        for (Field field : getDomainClass().getDeclaredFields()) { 
    
         Fetch fetch = field.getAnnotation(Fetch.class); 
    
         if (fetch != null && fetch.value() == FetchMode.JOIN) { 
          root.fetch(field.getName(), JoinType.LEFT); 
         } 
        } 
    } 
    
+0

Leider funktioniert dies nicht für Abfragen, die mit dem Namen der Repository-Methode generiert wurden. –

2

Ich arbeitete an dream83619 Antwort, um es verschachtelten Hibernate @Fetch Annotationen behandeln zu lassen. Ich habe eine rekursive Methode verwendet, um Annotationen in verschachtelten assoziierten Klassen zu finden.

So you have to implement custom repository und überschreiben getQuery(spec, domainClass, sort) Methode. Leider müssen Sie auch alle referenzierten privaten Methoden kopieren :(.

Hier ist der Code, kopierte private Methoden werden weggelassen.
EDIT: Hinzugefügte verbleibende private Methoden.

@NoRepositoryBean 
public class EntityGraphRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> { 

    private final EntityManager em; 
    protected JpaEntityInformation<T, ?> entityInformation; 

    public EntityGraphRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { 
     super(entityInformation, entityManager); 
     this.em = entityManager; 
     this.entityInformation = entityInformation; 
    } 

    @Override 
    protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) { 
     CriteriaBuilder builder = em.getCriteriaBuilder(); 
     CriteriaQuery<S> query = builder.createQuery(domainClass); 

     Root<S> root = applySpecificationToCriteria(spec, domainClass, query); 

     query.select(root); 
     applyFetchMode(root); 

     if (sort != null) { 
      query.orderBy(toOrders(sort, root, builder)); 
     } 

     return applyRepositoryMethodMetadata(em.createQuery(query)); 
    } 

    private Map<String, Join<?, ?>> joinCache; 

    private void applyFetchMode(Root<? extends T> root) { 
     joinCache = new HashMap<>(); 
     applyFetchMode(root, getDomainClass(), ""); 
    } 

    private void applyFetchMode(FetchParent<?, ?> root, Class<?> clazz, String path) { 
     for (Field field : clazz.getDeclaredFields()) { 
      Fetch fetch = field.getAnnotation(Fetch.class); 

      if (fetch != null && fetch.value() == FetchMode.JOIN) { 
       FetchParent<?, ?> descent = root.fetch(field.getName(), JoinType.LEFT); 
       String fieldPath = path + "." + field.getName(); 
       joinCache.put(path, (Join) descent); 

       applyFetchMode(descent, field.getType(), fieldPath); 
      } 
     } 
    } 

    /** 
    * Applies the given {@link Specification} to the given {@link CriteriaQuery}. 
    * 
    * @param spec can be {@literal null}. 
    * @param domainClass must not be {@literal null}. 
    * @param query must not be {@literal null}. 
    * @return 
    */ 
    private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass, 
     CriteriaQuery<S> query) { 

     Assert.notNull(query); 
     Assert.notNull(domainClass); 
     Root<U> root = query.from(domainClass); 

     if (spec == null) { 
      return root; 
     } 

     CriteriaBuilder builder = em.getCriteriaBuilder(); 
     Predicate predicate = spec.toPredicate(root, query, builder); 

     if (predicate != null) { 
      query.where(predicate); 
     } 

     return root; 
    } 

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) { 
     if (getRepositoryMethodMetadata() == null) { 
      return query; 
     } 

     LockModeType type = getRepositoryMethodMetadata().getLockModeType(); 
     TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type); 

     applyQueryHints(toReturn); 

     return toReturn; 
    } 

    private void applyQueryHints(Query query) { 
     for (Map.Entry<String, Object> hint : getQueryHints().entrySet()) { 
      query.setHint(hint.getKey(), hint.getValue()); 
     } 
    } 

    public Class<T> getEntityType() { 
     return entityInformation.getJavaType(); 
    } 

    public EntityManager getEm() { 
     return em; 
    } 
} 
+0

Ich versuche Ihre Lösung, aber ich habe eine private Metadaten-Variable in einer der Methoden zu kopieren, die Probleme bereitet. Können Sie den endgültigen Code teilen? – Homer1980ar

+0

Gemeinsamer Rest des Codes –

0

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
von diesem Link:

wenn Sie JPA verwenden oben auf Hibernate, gibt es keine Möglichkeit, die FetchMode von Hibernate JOINHowever verwendet zu setzen, wenn Sie JPA verwenden oben auf Hibernate, gibt es keine Möglichkeit, den von Hibernate verwendeten FetchMode auf JOIN zu setzen.

Die Spring Data JPA-Bibliothek enthält eine API für domänenbasierte Designspezifikationen, mit der Sie das Verhalten der generierten Abfrage steuern können.

final long userId = 1; 

final Specification<User> spec = new Specification<User>() { 
    @Override 
    public Predicate toPredicate(final Root<User> root, final 
    CriteriaQuery<?> query, final CriteriaBuilder cb) { 
    query.distinct(true); 
    root.fetch("permissions", JoinType.LEFT); 
    return cb.equal(root.get("id"), userId); 
} 
}; 

List<User> users = userRepository.findAll(spec);