Saya memang memiliki hubungan antara tiga objek model dalam proyek saya (cuplikan model dan repositori di akhir posting.
Ketika saya menyebutnya, PlaceRepository.findById
itu mengaktifkan tiga kueri pemilihan:
("sql")
SELECT * FROM place p where id = arg
SELECT * FROM user u where u.id = place.user.id
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.LAZY
diubah ke FetchType.EAGER
di Place
kelas (kueri dengan SELECT tambahan), sama untuk City
kelas saat FetchType.LAZY
diubah ke FetchType.EAGER
(kueri dengan JOIN).
Saat saya menggunakan CityRepository.findById
pemadaman api, dua pilihan:
SELECT * FROM city c where id = arg
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);
}
Jawaban:
Saya pikir Spring Data mengabaikan FetchMode. Saya selalu menggunakan anotasi
@NamedEntityGraph
dan@EntityGraph
saat 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
sumber
List<Place> findAll();
berakhir dengan Exceptionorg.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.@EntityGraph
hampir ununsable dalam skenario nyata karena tidak bisa ditentukan seperti apaFetch
yang ingin kita gunakan (JOIN
,SUBSELECT
,SELECT
,BATCH
). Ini dikombinasikan dengan@OneToMany
asosiasi dan membuat Hibernate Fetch seluruh tabel ke memori bahkan jika kita menggunakan queryMaxResults
.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 FETCH
perintah 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); }
sumber
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:
Implementasikan repositori kustom yang diwarisi dari SimpleJpaRepository
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.)
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); } } }
sumber
"
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. (PENGGUNAANJOIN
)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 gunakanFetchType.LAZY
.Ingat,
FetchType.LAZY
membutuhkan pabrik sesi db aktif di tempat di kode Anda di mana jika Anda memilih untuk mengambil informasi tabel induk.Misalnya untuk
LAZY
:Referensi tambahan
sumber
NamedEntityGraph
karena saya menginginkan grafik objek yang tidak terhidrasi.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.
sumber
Saya menguraikan jawaban dream83619 untuk membuatnya menangani
@Fetch
anotasi 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; } }
sumber
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);
sumber
Menurut Vlad Mihalcea (lihat https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ ):
Tampaknya kueri JPQL mungkin menimpa strategi pengambilan yang Anda nyatakan sehingga Anda harus menggunakan
join fetch
untuk 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 ).sumber