FetchMode Spring Data JPA'da nasıl çalışır?


95

Projemdeki üç model nesne arasında bir ilişkim var (yazının sonunda model ve depo snippet'leri.

Onu aradığımda PlaceRepository.findByIdüç seçme sorgusu ateşliyor:

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

Bu oldukça sıra dışı bir davranış (benim için). Hazırda bekletme belgelerini okuduktan sonra anlayabildiğim kadarıyla her zaman JOIN sorgularını kullanmalıdır. Sorgularda fark yoktur FetchType.LAZYdeğiştirildi FetchType.EAGERiçinde Placesınıf (ek SELECT ile sorguya), aynı Cityzaman sınıfa FetchType.LAZYdeğiştirildi FetchType.EAGER(JOIN ile sorguya).

CityRepository.findByIdBastırma yangınları kullandığımda iki seçim:

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

Amacım her durumda aynı davranışa sahip olmaktır (her zaman KATIL veya SEÇ, yine de KATIL tercih edilir).

Model tanımları:

Yer:

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

Kent:

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

Depolar:

Yer Deposu

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

Şehir Havuzu:

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

Hava 5 yollarını bir göz tembel relationsships başlatmak için: thoughts-on-java.org/...
Grigory Kislin

Yanıtlar:


114

Spring Data'nın FetchMode'u yok saydığını düşünüyorum. Spring Data ile çalışırken her zaman @NamedEntityGraphve @EntityGraphek açıklamalarını kullanırım

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

}

Belgeleri buradan kontrol edin


1
Benim için çalışmıyor gibiyim. Demek istediğim işe yarıyor ama ... Depoya '@EntityGraph' ile açıklama eklediğimde, kendi kendine çalışmıyor (genellikle). Örneğin: `Place findById (int id);` çalışır ancak List<Place> findAll();Exception ile sonuçlanır org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Manuel olarak eklediğimde çalışıyor @Query("select p from Place p"). Yine de geçici çözüm gibi görünüyor.
SirKometa

Belki de, JpaRepository arabiriminden mevcut bir yöntem olduğu için findAll () üzerinde çalışabilir, diğer yönteminiz "findById" ise çalışma zamanında oluşturulan özel bir sorgu yöntemidir.
wesker317

En iyisi olduğu için bunu doğru cevap olarak işaretlemeye karar verdim. Yine de mükemmel değil. Çoğu senaryoda çalışır, ancak şimdiye kadar daha karmaşık EntityGraphs ile spring-data-jpa'da hatalar fark ettim. Teşekkürler :)
SirKometa

2
@EntityGrapho belirtilebilir olamaz çünkü gerçek senaryolarda neredeyse ununsable ne tür Fetchkullandığımız istiyoruz ( JOIN, SUBSELECT, SELECT, BATCH). Bu @OneToManyilişki ile birlikte ve sorgu kullansak bile Hibernate Fetch tüm tabloyu hafızaya alır MaxResults.
Ondrej Bozek

1
Teşekkürler, JPQL sorgularının, seçilen getirme politikasıyla varsayılan getirme stratejisini geçersiz kılabileceğini söylemek istedim .
adrhc

53

Her şeyden önce, @Fetch(FetchMode.JOIN)ve @ManyToOne(fetch = FetchType.LAZY)düşmanca, biri bir EAGER'ı getirme talimatı verirken, diğeri tembel bir getirme öneriyor.

İstekli getirme nadiren iyi bir seçimdir ve öngörülebilir bir davranış için, sorgu zamanı JOIN FETCHyönergesini kullanmanız daha iyi olur :

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

3
Criteria API ve Spring Data Spesifikasyonları ile aynı sonucu elde etmenin bir yolu var mı?
svlada

2
JPA getirme profilleri gerektiren getirme bölümü değil.
Vlad Mihalcea

Vlad Mihalcea, Spring Data JPA kriterlerini (şartname) kullanarak bunun nasıl yapılacağını bir örnekle paylaşabilir misiniz? Lütfen
Yan Khonski

Böyle bir örneğim yok, ancak Spring Data JPA eğitimlerinde kesinlikle bir tane bulabilirsiniz.
Vlad Mihalcea

Sorgu zamanını kullanıyorsanız ..... varlık üzerinde @OneToMany ... vb. tanımlamanız gerekecek mi?
Eric Huang

19

Spring-jpa, varlık yöneticisini kullanarak sorguyu oluşturur ve Hazırda Beklet, sorgu varlık yöneticisi tarafından oluşturulmuşsa getirme modunu yok sayar.

Aşağıdakiler, kullandığım geçici çalışma:

  1. SimpleJpaRepository'den devralan özel bir depo uygulayın

  2. Yöntemi geçersiz kılın 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));
    }
    

    Yöntemin ortasında, applyFetchMode(root);Hazırda Bekletme'nin sorguyu doğru birleştirmeyle oluşturmasını sağlamak için getirme modunu uygulamak için ekleyin .

    (Maalesef tüm yöntemi ve ilgili özel yöntemleri temel sınıftan kopyalamamız gerekiyor çünkü başka bir uzantı noktası yoktu.)

  3. Uygulama 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);
            }
        }
    }
    

Ne yazık ki bu, depo yöntemi adı kullanılarak oluşturulan sorgular için çalışmaz.
Ondrej Bozek

lütfen tüm ithalat beyanlarını ekleyebilir misiniz? teşekkür ederim.
granadaCoder

3

" FetchType.LAZY" yalnızca birincil tablo için tetiklenir. Kodunuzda üst tablo bağımlılığı olan başka bir yöntemi çağırırsanız, o tablo bilgisini almak için sorguyu ateşleyecektir. (YANGINLAR ÇOKLU SEÇİM)

" FetchType.EAGER", doğrudan ilgili üst tablolar dahil tüm tabloların birleşimini oluşturacaktır. (KULLANIMI JOIN)

Ne Zaman Kullanılmalı: Varsayalım ki, bağımlı ana tablo bilgisini zorunlu olarak kullanmanız ve ardından seçim yapmanız gerekiyor FetchType.EAGER. Yalnızca belirli kayıtlar için bilgiye ihtiyacınız varsa kullanın FetchType.LAZY.

Unutmayın, FetchType.LAZYüst tablo bilgilerini almayı seçerseniz, kodunuzdaki yerde aktif bir db oturum fabrikasına ihtiyaç duyar.

Örneğin 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

Ek referans


İlginç bir şekilde, NamedEntityGraphhidratlanmamış bir nesne grafiği istediğim için bu yanıt beni doğru yola götürdü.
JJ Zabkar

bu cevap daha fazla oyu hak ediyor. Kısa ve öz ve neden pek çok "sihirli bir şekilde tetiklenen" sorgu gördüğümü anlamama çok yardımcı oldu ... çok teşekkürler!
Clint Eastwood

3

Getirme modu yalnızca nesneyi id ile yani kullanılarak seçildiğinde çalışacaktır entityManager.find(). Spring Data her zaman bir sorgu oluşturacağından, getirme modu yapılandırmasının size hiçbir faydası olmayacaktır. Getirme birleşimleriyle özel sorgular kullanabilir veya varlık grafiklerini kullanabilirsiniz.

En iyi performansı istediğinizde, gerçekten ihtiyacınız olan verilerin yalnızca alt kümesini seçmelisiniz. Bunu yapmak için, genellikle gereksiz verilerin alınmasını önlemek için bir DTO yaklaşımı kullanılması önerilir, ancak bu genellikle hataya yatkın bir standart kodla sonuçlanır, çünkü DTO modelinizi bir JPQL aracılığıyla oluşturan özel bir sorgu tanımlamanız gerekir. yapıcı ifadesi.

Spring Data projeksiyonları burada yardımcı olabilir, ancak bir noktada Blaze-Persistence Entity Views gibi bunu oldukça kolaylaştıran ve kılıfında kullanışlı olacak çok daha fazla özelliğe sahip bir çözüme ihtiyacınız olacak! Alıcıların ihtiyacınız olan veri alt kümesini temsil ettiği varlık başına bir DTO arayüzü oluşturursunuz. Sorununuzun çözümü şuna benzeyebilir

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

Sorumluluk reddi, Ben Blaze-Persistence'ın yazarıyım, bu yüzden önyargılı olabilirim.


2

Dream83619 cevabını, yuvalanmış Hazırda Beklet @Fetchek açıklamalarını işlemesi için ayrıntılı olarak ele aldım . İç içe geçmiş ilişkili sınıflarda ek açıklamalar bulmak için özyinelemeli yöntem kullandım.

Bu nedenle, özel depoyu uygulamanız ve getQuery(spec, domainClass, sort)yöntemi geçersiz kılmanız gerekir. Maalesef, başvurulan tüm özel yöntemleri de kopyalamanız gerekiyor :(.

İşte kod, kopyalanan özel yöntemler atlanmıştır.
DÜZENLEME: Kalan özel yöntemler eklendi.

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

Çözümünüzü deniyorum ama kopyalama yöntemlerinden birinde sorun çıkaran özel bir meta veri değişkenim var. Son kodu paylaşabilir misin?
Homer1980ar

yinelemeli Getirme çalışmıyor. OneToMany'ye
sahipsem

henüz iyi test edilmedi, ancak yinelemeli olarak applyFetchMode çağrısı yaparken field.getType () yerine böyle bir şey ((Join) descent) .getJavaType () olmalı
antohoho

2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
bu bağlantıdan:

Hazırda Bekletme'nin üstünde JPA kullanıyorsanız, Hibernate tarafından kullanılan FetchMode'u JOIN olarak ayarlamanın bir yolu yoktur, ancak, Hazırda Bekletme'nin üzerinde JPA kullanıyorsanız, Hibernate tarafından kullanılan FetchMode'u JOIN olarak ayarlamanın bir yolu yoktur.

Spring Data JPA kitaplığı, oluşturulan sorgunun davranışını kontrol etmenize olanak tanıyan bir Etki Alanına Dayalı Tasarım Spesifikasyonları API'si sağlar.

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

2

Vlad Mihalcea'ya göre (bkz. Https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):

JPQL sorguları, varsayılan getirme stratejisini geçersiz kılabilir. İç veya sol birleştirme getirme yönergelerini kullanarak ne getirmek istediğimizi açıkça belirtmezsek, varsayılan seçme getirme ilkesi uygulanır.

Görünüşe göre JPQL sorgusu, beyan edilen getirme stratejinizi geçersiz kılabilir, bu nedenle join fetchbaşvurulan bazı varlıkları hevesle yüklemek için kullanmanız veya EntityManager ile kimliğe göre yüklemeniz gerekir (bu, getirme stratejinize uyacaktır ancak kullanım durumunuz için bir çözüm olmayabilir. ).

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.