it-swarm.com.de

Wie funktioniert der FetchMode in Spring Data JPA?

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

Wenn ich PlaceRepository.findById anrufe, werden drei Auswahlabfragen 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öhnlich (für mich). Soweit ich nach dem Lesen der Hibernate-Dokumentation feststellen kann, sollte immer JOIN-Abfragen verwendet werden. Die Abfragen unterscheiden sich nicht, wenn FetchType.LAZY in der Klasse Place (Abfrage mit zusätzlichem SELECT) in FetchType.EAGER geändert wurde. Dies gilt auch für die Klasse City, wenn FetchType.LAZY in FetchType.EAGER (Abfrage mit JOIN) geändert wurde. 

Wenn ich CityRepository.findById unterdrücke, 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 ein sam Verhalten zu haben (entweder immer JOIN oder SELECT, JOIN 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);
}
62
SirKometa

Ich denke, dass Spring Data den FetchMode ignoriert. Bei der Arbeit mit Spring Data verwende ich immer die Annotationen @NamedEntityGraph und @EntityGraph

@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

80
wesker317

Erstens sind @Fetch(FetchMode.JOIN) und @ManyToOne(fetch = FetchType.LAZY) antagonistisch, wobei einer das Abrufen von EAGER befiehlt, während der andere einen LAZY-Abruf vorschlägt.

Eifrer Abruf ist selten eine gute Wahl und für ein vorhersagbares Verhalten ist es besser, die Abfragezeit-JOIN FETCH-Direktive 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);
}
40
Vlad Mihalcea

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

Folgendes ist die Arbeit, die ich verwendet habe:

  1. Implementiere 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 applyFetchMode(root); hinzu, um den Abrufmodus anzuwenden, damit Hibernate 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. Implementiere 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);
            }
        }
    }
    
15
dream83619

"FetchType.LAZY" wird nur für die primäre Tabelle ausgelöst. Wenn Sie in Ihrem Code eine andere Methode aufrufen, die über eine Abhängigkeit von einer übergeordneten Tabelle verfügt, wird die Abfrage ausgelöst, um diese Tabelleninformationen abzurufen. (MEHRFACH WÄHLEN)

"FetchType.EAGER" erstellt direkt den Join aller Tabellen einschließlich relevanter übergeordneter Tabellen. (USES JOIN)

Verwendungszweck: Angenommen, Sie müssen zwingend die abhängigen Informationen der übergeordneten Tabelle verwenden. Wählen Sie dann FetchType.EAGER. Wenn Sie nur Informationen für bestimmte Datensätze benötigen, verwenden Sie FetchType.LAZY.

Denken Sie daran, dass FetchType.LAZY an der Stelle in Ihrem Code eine aktive Datenbanksitzungsfactory benötigt, an der Sie Informationen zur übergeordneten Tabelle abrufen möchten.

Z.B. 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

2
Godwin

Ich habe auf dream83619 answer eingearbeitet, um geschachtelte Hibernate @Fetch-Anmerkungen handhaben zu lassen. Ich habe rekursive Methoden verwendet, um Anmerkungen in verschachtelten verknüpften Klassen zu finden.

Sie müssen also eine benutzerdefinierte 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.
EDIT: Restliche 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;
    }
}
2
Ondrej Bozek

Der Abrufmodus funktioniert nur, wenn das Objekt nach ID ausgewählt wird, d. H. Mit entityManager.find(). Da Spring Data immer eine Abfrage erstellt, hat die Konfiguration des Abrufmodus keine Verwendung für Sie. Sie können entweder dedizierte Abfragen mit Abruf-Joins 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. Um dies zu erreichen, wird im Allgemeinen empfohlen, einen DTO-Ansatz zu verwenden, um das Abrufen unnötiger Daten zu vermeiden. Dies führt jedoch in der Regel zu einer Menge fehleranfälligen Bufferplate-Code, 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 viele weitere Funktionen im Ärmel hat, die sich als nützlich erweisen werden! Sie erstellen einfach eine DTO-Schnittstelle pro Entität, bei 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 könnte ich voreingenommen sein.

1

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

JPQL-Abfragen überschreiben möglicherweise die Standardabrufstrategie. Wenn wir es nicht tun. Erklären Sie explizit, was wir abrufen möchten, indem Sie den inneren oder linken Join verwenden Abrufanweisungen wird die Standard-Abrufrichtlinie angewendet.

Es scheint, dass die JPQL-Abfrage Ihre deklarierte Abrufstrategie überschreiben kann. Daher müssen Sie join fetch verwenden, um einige referenzierte Entitäten eifrig zu laden, oder laden Sie sie einfach mit id in EntityManager Fall).

1
adrhc

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

wenn Sie JPA über Hibernate verwenden, können Sie den von Hibernate verwendeten FetchMode nicht auf JOIN setzen.

Die JPA-Bibliothek von Spring Data bietet eine API für domänengestützte 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);
0
kafkas