Wie funktioniert der FetchMode in Spring Data JPA?

91

Ich habe eine Beziehung zwischen drei Modellobjekten in meinem Projekt (Modell- und Repository-Snippets am Ende des Beitrags.

Wenn ich anrufe PlaceRepository.findById, werden drei ausgewählte Abfragen ausgelöst:

("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, sollten immer JOIN-Abfragen verwendet werden. Es gibt keinen Unterschied zwischen den Abfragen, wenn FetchType.LAZYsie FetchType.EAGERin der PlaceKlasse geändert werden (Abfrage mit zusätzlichem SELECT). Dies gilt auch für die CityKlasse, wenn sie FetchType.LAZYgeändert wird FetchType.EAGER(Abfrage mit JOIN).

Wenn ich die CityRepository.findByIdUnterdrückung von Bränden verwende, werden zwei Optionen ausgewählt:

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

Mein Ziel ist es, in allen Situationen das gleiche Verhalten zu haben (entweder immer JOIN oder SELECT, JOIN jedoch bevorzugt).

Modelldefinitionen:

Platz:

@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
}

Repositories:

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);
}
SirKometa
quelle
Hava einen Blick auf 5 Möglichkeiten, um faule Beziehungen zu initialisieren: Gedanken-on-java.org/…
Grigory Kislin

Antworten:

107

Ich denke, dass Spring Data den FetchMode ignoriert. Ich verwende immer die Anmerkungen @NamedEntityGraphund, @EntityGraphwenn ich mit Spring Data arbeite

@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 hier

wesker317
quelle
1
Ich scheine nicht für mich zu arbeiten. Ich meine, es funktioniert, aber ... Wenn ich das Repository mit '@EntityGraph' kommentiere, funktioniert es (normalerweise) nicht von selbst. Zum Beispiel: `Place findById (int id);` funktioniert, List<Place> findAll();endet aber mit der Ausnahme org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Es funktioniert, wenn ich manuell hinzufüge @Query("select p from Place p"). Scheint jedoch eine Problemumgehung zu sein.
SirKometa
Möglicherweise funktioniert es nicht mit findAll (), da es sich um eine vorhandene Methode aus der JpaRepository-Schnittstelle handelt, während Ihre andere Methode "findById" eine benutzerdefinierte Abfragemethode ist, die zur Laufzeit generiert wird.
wesker317
Ich habe beschlossen, dies als die richtige Antwort zu markieren, da es die beste ist. Es ist jedoch nicht perfekt. Es funktioniert in den meisten Szenarien, aber bisher habe ich Fehler in spring-data-jpa mit komplexeren EntityGraphs festgestellt. Danke :)
SirKometa
2
@EntityGraphfast ununsable in realen Szenarien ist , da sie angegeben werden können nicht , welche Art von Fetchuns verwenden möchten ( JOIN, SUBSELECT, SELECT, BATCH). Dies in Kombination mit der @OneToManyZuordnung und bewirkt, dass der Ruhezustand die gesamte Tabelle in den Speicher abruft, selbst wenn Abfrage verwendet wird MaxResults.
Ondrej Bozek
1
Vielen Dank, ich wollte sagen, dass JPQL-Abfragen möglicherweise die Standardabrufstrategie mit ausgewählten Abrufrichtlinien überschreiben .
Adrc
49

Erstens @Fetch(FetchMode.JOIN)und @ManyToOne(fetch = FetchType.LAZY)antagonistisch, weist einer einen EAGER-Abruf an, während der andere einen LAZY-Abruf vorschlägt.

Eifriges Abrufen ist selten eine gute Wahl, und für ein vorhersehbares Verhalten ist es besser, die JOIN FETCHDirektive für die Abfragezeit zu verwenden :

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);
}
Vlad Mihalcea
quelle
3
Gibt es eine Möglichkeit, mit Criteria API und Spring Data Specifications dasselbe Ergebnis zu erzielen?
Svlada
2
Nicht der Abrufteil, für den JPA-Abrufprofile erforderlich sind.
Vlad Mihalcea
Vlad Mihalcea, können Sie den Link mit einem Beispiel teilen, wie dies unter Verwendung der Spring Data JPA-Kriterien (Spezifikation) getan wird? Bitte
Yan Khonski
Ich habe kein solches Beispiel, aber Sie können sicherlich eines in Spring Data JPA-Tutorials finden.
Vlad Mihalcea
Wenn Sie die Abfragezeit verwenden, müssen Sie noch @OneToMany ... usw. für die Entität definieren?
Eric Huang
19

Spring-jpa erstellt die Abfrage mithilfe des Entitätsmanagers, und der Ruhezustand ignoriert den Abrufmodus, wenn die Abfrage vom Entitätsmanager erstellt wurde.

Das Folgende ist die Arbeit, die ich verwendet habe:

  1. Implementieren Sie ein benutzerdefiniertes Repository, das von SimpleJpaRepository erbt

  2. Überschreiben Sie die 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));
    }
    

    Fügen Sie in der Mitte der Methode add hinzu applyFetchMode(root);, um den Abrufmodus anzuwenden, damit der Ruhezustand die Abfrage mit dem richtigen Join erstellt.

    (Leider müssen wir die gesamte Methode und die zugehörigen privaten Methoden aus der Basisklasse kopieren, da es keinen anderen Erweiterungspunkt gab.)

  3. Implementieren 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);
            }
        }
    }
    
dream83619
quelle
Leider funktioniert dies nicht für Abfragen, die mit dem Namen der Repository-Methode generiert wurden.
Ondrej Bozek
Könnten Sie bitte alle Importanweisungen hinzufügen? Danke.
GranadaCoder
3

" FetchType.LAZY" wird nur für den Primärtisch ausgelöst. Wenn Sie in Ihrem Code eine andere Methode aufrufen, die eine übergeordnete Tabellenabhängigkeit aufweist, wird eine Abfrage ausgelöst, um diese Tabelleninformationen abzurufen. (FIRES MULTIPLE SELECT)

" FetchType.EAGER" erstellt direkt einen Join aller Tabellen einschließlich der relevanten übergeordneten Tabellen. (USES JOIN)

Verwendungszweck: Angenommen, Sie müssen die abhängige übergeordnete Tabelleninformation zwangsweise verwenden und dann auswählen FetchType.EAGER. Wenn Sie nur Informationen für bestimmte Datensätze benötigen, verwenden Sie FetchType.LAZY.

Denken Sie daran, dass FetchType.LAZYan der Stelle in Ihrem Code eine aktive Datenbank-Session-Factory erforderlich ist, an der Sie Informationen zur übergeordneten Tabelle abrufen können.

ZB 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

Zusätzliche Referenz

Godwin
quelle
Interessanterweise brachte mich diese Antwort auf den richtigen Weg, NamedEntityGraphda ich ein nicht hydratisiertes Objektdiagramm wollte.
JJ Zabkar
Diese Antwort verdient mehr Gegenstimmen. Es ist prägnant und hat mir sehr geholfen zu verstehen, warum ich viele "magisch ausgelöste" Abfragen gesehen habe ... vielen Dank!
Clint Eastwood
3

Der Abrufmodus funktioniert nur, wenn das Objekt anhand seiner ID ausgewählt wird entityManager.find(). Da Spring Data immer eine Abfrage erstellt, hat die Konfiguration des Abrufmodus für Sie keine Verwendung. Sie können entweder dedizierte Abfragen mit Abrufverknüpfungen oder Entitätsdiagramme verwenden.

Wenn Sie die beste Leistung erzielen möchten, sollten Sie nur die Teilmenge der Daten auswählen, die Sie wirklich benötigen. Zu diesem Zweck wird im Allgemeinen empfohlen, einen DTO-Ansatz zu verwenden, um zu vermeiden, dass unnötige Daten abgerufen werden müssen. Dies führt jedoch normalerweise zu einer Menge fehleranfälligen Boilerplate-Codes, da Sie eine dedizierte Abfrage definieren müssen, die Ihr DTO-Modell über JPQL erstellt Konstruktorausdruck.

Spring Data-Projektionen können hier helfen, aber irgendwann benötigen Sie eine Lösung wie Blaze-Persistence Entity Views , die dies ziemlich einfach macht und viel mehr Funktionen in der Hülle hat, die sich als nützlich erweisen werden! Sie erstellen lediglich eine DTO-Schnittstelle pro Entität, in der die Getter die Teilmenge der Daten darstellen, die Sie benötigen. Eine Lösung für Ihr Problem könnte so aussehen

@EntityView(Identified.class)
public interface IdentifiedView {
    @IdMapping
    Integer getId();
}

@EntityView(Identified.class)
public interface UserView extends IdentifiedView {
    String getName();
}

@EntityView(Identified.class)
public interface StateView extends IdentifiedView {
    String getName();
}

@EntityView(Place.class)
public interface PlaceView extends IdentifiedView {
    UserView getAuthor();
    CityView getCity();
}

@EntityView(City.class)
public interface CityView extends IdentifiedView {
    StateView getState();
}

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

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

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

Haftungsausschluss, ich bin der Autor von Blaze-Persistence, daher bin ich möglicherweise voreingenommen.

Christian Beikov
quelle
2

Ich habe die Antwort dream83619 ausgearbeitet , damit verschachtelte @FetchAnnotationen im Ruhezustand verarbeitet werden können . Ich habe eine rekursive Methode verwendet, um Anmerkungen in verschachtelten zugeordneten Klassen zu finden.

Sie müssen also ein benutzerdefiniertes Repository implementieren und die getQuery(spec, domainClass, sort)Methode überschreiben . Leider müssen Sie auch alle referenzierten privaten Methoden kopieren :(.

Hier ist der Code, kopierte private Methoden werden weggelassen.
BEARBEITEN: Verbleibende private Methoden hinzugefügt.

@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;
    }
}
Ondrej Bozek
quelle
Ich versuche Ihre Lösung, aber ich habe eine private Metadatenvariable in einer der zu kopierenden Methoden, die Probleme verursacht. Können Sie den endgültigen Code teilen?
Homer1980ar
rekursiver Abruf funktioniert nicht. Wenn ich OneToMany habe, übergibt es java.util.List an die nächste Iteration
Antohoho
Ich habe es noch nicht gut getestet, aber ich denke, es sollte so etwas wie ((Join) Abstieg) .getJavaType () anstelle von field.getType () sein, wenn der Aufruf rekursiv applyFetchMode
antohoho
2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
über diesen Link:

Wenn Sie JPA über dem Ruhezustand verwenden, gibt es keine Möglichkeit, den von Hibernate verwendeten FetchMode auf JOIN zu setzen. Wenn Sie JPA über Hibernate verwenden, können Sie den von Hibernate verwendeten FetchMode jedoch nicht auf JOIN setzen.

Die Spring Data JPA-Bibliothek bietet eine API für domänengesteuerte Entwurfsspezifikationen, 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);
Kafkas
quelle
2

Laut Vlad Mihalcea (siehe https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

JPQL-Abfragen können die Standardabrufstrategie überschreiben. Wenn wir nicht explizit deklarieren, was wir mit den Anweisungen zum Abrufen der inneren oder linken Verknüpfung abrufen möchten, wird die Standardrichtlinie zum Abrufen der Auswahl angewendet.

Es scheint, dass die JPQL-Abfrage Ihre deklarierte Abrufstrategie möglicherweise überschreibt, sodass Sie sie verwenden join fetchmüssen, um eine referenzierte Entität eifrig zu laden, oder einfach mit EntityManager nach ID laden (was Ihrer Abrufstrategie entspricht, aber möglicherweise keine Lösung für Ihren Anwendungsfall darstellt ).

adrhc
quelle