Bagaimana cara kerja FetchMode di Spring Data JPA

95

Saya memang memiliki hubungan antara tiga objek model dalam proyek saya (cuplikan model dan repositori di akhir posting.

Ketika saya menyebutnya, PlaceRepository.findByIditu mengaktifkan tiga kueri pemilihan:

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

Itu perilaku yang agak tidak biasa (bagi saya). Sejauh yang saya tahu setelah membaca dokumentasi Hibernate, itu harus selalu menggunakan kueri JOIN. Tidak ada perbedaan dalam kueri saat FetchType.LAZYdiubah ke FetchType.EAGERdi Placekelas (kueri dengan SELECT tambahan), sama untuk Citykelas saat FetchType.LAZYdiubah ke FetchType.EAGER(kueri dengan JOIN).

Saat saya menggunakan CityRepository.findByIdpemadaman api, dua pilihan:

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

Tujuan saya adalah untuk memiliki perilaku yang sama di semua situasi (baik selalu GABUNG atau PILIH, JOIN lebih disukai).

Definisi model:

Tempat:

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

Kota:

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

Repositori:

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
sumber
Hava melihat 5 cara untuk menginisialisasi hubungan malas: pikiran-on-java.org/…
Grigory Kislin

Jawaban:

114

Saya pikir Spring Data mengabaikan FetchMode. Saya selalu menggunakan anotasi @NamedEntityGraphdan @EntityGraphsaat bekerja dengan 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);

}

Cek dokumentasinya disini

wesker317
sumber
1
Sepertinya saya tidak bekerja untuk saya. Maksud saya itu berfungsi tetapi ... Ketika saya menganotasi repositori dengan '@EntityGraph' itu tidak berfungsi sendiri (biasanya). Misalnya: `Place findById (int id);` berfungsi tetapi List<Place> findAll();berakhir dengan Exception org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. Ini berfungsi saat saya menambahkan secara manual @Query("select p from Place p"). Sepertinya ada solusi.
SirKometa
Mungkin itu tidak akan berfungsi pada findAll () karena ini adalah metode yang ada dari antarmuka JpaRepository sementara metode Anda yang lain "findById" adalah metode kueri khusus yang dibuat saat runtime.
wesker317
Saya telah memutuskan untuk menandai ini sebagai jawaban yang tepat karena itu yang terbaik. Itu tidak sempurna. Ini berfungsi di sebagian besar skenario tetapi sejauh ini saya telah melihat bug di spring-data-jpa dengan EntityGraphs yang lebih kompleks. Terima kasih :)
SirKometa
2
@EntityGraphhampir ununsable dalam skenario nyata karena tidak bisa ditentukan seperti apa Fetchyang ingin kita gunakan ( JOIN, SUBSELECT, SELECT, BATCH). Ini dikombinasikan dengan @OneToManyasosiasi dan membuat Hibernate Fetch seluruh tabel ke memori bahkan jika kita menggunakan query MaxResults.
Ondrej Bozek
1
Terima kasih, saya ingin mengatakan bahwa kueri JPQL dapat menggantikan strategi pengambilan default dengan kebijakan pengambilan pilih .
adrhc
53

Pertama-tama, @Fetch(FetchMode.JOIN)dan @ManyToOne(fetch = FetchType.LAZY)bersifat antagonis, yang satu memerintahkan pengambilan EAGER, sementara yang lain menyarankan pengambilan MALAS.

Pengambilan yang bersemangat jarang merupakan pilihan yang baik dan untuk perilaku yang dapat diprediksi, Anda lebih baik menggunakan JOIN FETCHperintah waktu kueri :

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
sumber
3
Adakah cara untuk mencapai hasil yang sama dengan API Kriteria dan Spesifikasi Data Musim Semi?
svlada
2
Bukan bagian pengambilan, yang memerlukan profil pengambilan JPA.
Vlad Mihalcea
Vlad Mihalcea, dapatkah Anda membagikan tautan dengan contoh bagaimana melakukan ini dengan menggunakan kriteria (spesifikasi) JPA Data Musim Semi? Tolong
Yan Khonski
Saya tidak punya contoh seperti itu, tapi Anda pasti bisa menemukannya di tutorial JPA Spring Data.
Vlad Mihalcea
jika menggunakan query-time ..... apakah Anda masih perlu mendefinisikan @OneToMany ... dll pada entitas?
Eric Huang
19

Spring-jpa membuat kueri menggunakan pengelola entitas, dan Hibernasi akan mengabaikan mode pengambilan jika kueri dibuat oleh pengelola entitas.

Berikut ini adalah solusi yang saya gunakan:

  1. Implementasikan repositori kustom yang diwarisi dari SimpleJpaRepository

  2. Ganti metode 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));
    }
    

    Di tengah metode, tambahkan applyFetchMode(root);untuk menerapkan mode pengambilan, untuk membuat Hibernate membuat kueri dengan gabungan yang benar.

    (Sayangnya kami perlu menyalin seluruh metode dan metode privat terkait dari kelas dasar karena tidak ada titik ekstensi lain.)

  3. Terapkan 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);
            }
        }
    }
    
mimpi83619
sumber
Sayangnya ini tidak berfungsi untuk kueri yang dibuat menggunakan nama metode repositori.
Ondrej Bozek
bisakah Anda menambahkan semua pernyataan impor? Terima kasih.
granadaCoder
3

" FetchType.LAZY" hanya akan diaktifkan untuk tabel utama. Jika dalam kode Anda, Anda memanggil metode lain yang memiliki ketergantungan tabel induk maka itu akan mengaktifkan kueri untuk mendapatkan informasi tabel itu. (PILIHAN GANDA KEBAKARAN)

" FetchType.EAGER" akan membuat gabungan dari semua tabel termasuk tabel induk yang relevan secara langsung. (PENGGUNAAN JOIN)

Kapan Menggunakan: Misalkan Anda secara wajib perlu menggunakan informasi tabel induk dependen lalu pilih FetchType.EAGER. Jika Anda hanya membutuhkan informasi untuk catatan tertentu maka gunakan FetchType.LAZY.

Ingat, FetchType.LAZYmembutuhkan pabrik sesi db aktif di tempat di kode Anda di mana jika Anda memilih untuk mengambil informasi tabel induk.

Misalnya untuk 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

Referensi tambahan

Godwin
sumber
Menariknya, jawaban ini membuat saya berada di jalur yang benar untuk menggunakan NamedEntityGraphkarena saya menginginkan grafik objek yang tidak terhidrasi.
JJ Zabkar
jawaban ini membutuhkan lebih banyak suara positif. Ini ringkas dan sangat membantu saya untuk memahami mengapa saya melihat banyak pertanyaan yang "dipicu secara ajaib" ... terima kasih banyak!
Clint Eastwood
3

Mode pengambilan hanya akan bekerja ketika memilih objek dengan id yaitu menggunakan entityManager.find(). Karena Spring Data akan selalu membuat kueri, konfigurasi mode pengambilan tidak akan berguna bagi Anda. Anda dapat menggunakan kueri khusus dengan fetch joins atau menggunakan grafik entitas.

Jika Anda menginginkan kinerja terbaik, Anda harus memilih hanya sebagian dari data yang benar-benar Anda butuhkan. Untuk melakukan ini, secara umum disarankan untuk menggunakan pendekatan DTO untuk menghindari data yang tidak perlu diambil, tetapi biasanya menghasilkan cukup banyak kode boilerplate yang rawan kesalahan, karena Anda perlu menentukan kueri khusus yang membangun model DTO Anda melalui JPQL ekspresi konstruktor.

Proyeksi Spring Data dapat membantu di sini, tetapi pada titik tertentu Anda akan membutuhkan solusi seperti Blaze-Persistence Entity Views yang membuatnya cukup mudah dan memiliki lebih banyak fitur di dalamnya yang akan berguna! Anda cukup membuat antarmuka DTO per entitas di mana getter mewakili subset data yang Anda butuhkan. Solusi untuk masalah Anda bisa terlihat seperti ini

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

Penafian, saya penulis Blaze-Persistence, jadi saya mungkin bias.

Christian Beikov
sumber
2

Saya menguraikan jawaban dream83619 untuk membuatnya menangani @Fetchanotasi Hibernate bersarang . Saya menggunakan metode rekursif untuk menemukan penjelasan di kelas terkait bersarang.

Jadi, Anda harus menerapkan repositori khusus dan getQuery(spec, domainClass, sort)metode penggantian . Sayangnya Anda juga harus menyalin semua metode privat yang direferensikan :(.

Ini kodenya, metode pribadi yang disalin dihilangkan.
EDIT: Menambahkan metode pribadi yang tersisa.

@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
sumber
Saya mencoba solusi Anda tetapi saya memiliki variabel metadata pribadi di salah satu metode untuk menyalin yang menimbulkan masalah. Bisakah Anda membagikan kode terakhir?
Homer1980ar
Pengambilan rekursif tidak berfungsi. Jika saya memiliki OneToMany, ia melewati java.util.List ke iterasi berikutnya
antohoho
belum mengujinya dengan baik, tetapi pikirkan harus seperti ini ((Join) descent) .getJavaType () daripada field.getType () ketika panggilan secara rekursif applyFetchMode
antohoho
2

http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html
dari tautan ini:

Jika Anda menggunakan JPA di atas Hibernate, tidak ada cara untuk menyetel FetchMode yang digunakan oleh Hibernate ke JOIN Namun, jika Anda menggunakan JPA di atas Hibernate, tidak ada cara untuk menyetel FetchMode yang digunakan oleh Hibernate ke JOIN.

Pustaka JPA Spring Data menyediakan API Spesifikasi Desain Berbasis Domain yang memungkinkan Anda mengontrol perilaku kueri yang dihasilkan.

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);
kafka
sumber
2

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

Kueri JPQL dapat menggantikan strategi pengambilan default. Jika kita tidak secara eksplisit mendeklarasikan apa yang ingin kita ambil menggunakan perintah inner atau left join fetch, kebijakan pilih ambil default akan diterapkan.

Tampaknya kueri JPQL mungkin menimpa strategi pengambilan yang Anda nyatakan sehingga Anda harus menggunakan join fetchuntuk memuat dengan bersemangat beberapa entitas yang direferensikan atau hanya memuat dengan id dengan EntityManager (yang akan mematuhi strategi pengambilan Anda tetapi mungkin bukan solusi untuk kasus penggunaan Anda ).

adrhc
sumber