Cara mengambil asosiasi FetchType.LAZY dengan JPA dan Hibernate di Spring Controller

146

Saya memiliki kelas Person:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

Dengan hubungan banyak ke banyak yang malas.

Di controller saya saya miliki

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

Dan PersonRepository hanyalah kode ini, ditulis sesuai dengan panduan ini

public interface PersonRepository extends JpaRepository<Person, Long> {
}

Namun, dalam pengontrol ini saya benar-benar membutuhkan data-malas. Bagaimana saya bisa memicu pemuatannya?

Mencoba mengaksesnya akan gagal

gagal malas menginisialisasi kumpulan peran: no.dusken.momus.model.Person.roles, tidak dapat menginisialisasi proxy - tanpa Sesi

atau pengecualian lain tergantung pada apa yang saya coba.

Deskripsi-xml saya , jika diperlukan.

Terima kasih.

Matsemann
sumber
Bisakah Anda menulis metode, yang akan membuat kueri untuk mengambil Personobjek yang diberikan beberapa parameter? Di dalamnya Query, sertakan fetchklausa dan muat Rolesjuga untuk orang tersebut.
SudoRahul

Jawaban:

206

Anda harus membuat panggilan eksplisit pada koleksi malas untuk menginisialisasi (praktik umum adalah memanggil .size()untuk tujuan ini). Di Hibernate ada metode khusus untuk ini ( Hibernate.initialize()), tetapi JPA tidak memiliki yang setara dengan itu. Tentu saja Anda harus memastikan bahwa pemanggilan selesai, ketika sesi masih tersedia, jadi beri catatan metode pengontrol Anda @Transactional. Alternatifnya adalah membuat lapisan Layanan antara antara Controller dan Repository yang dapat mengekspos metode yang menginisialisasi koleksi malas.

Memperbarui:

Harap perhatikan bahwa solusi di atas mudah, tetapi menghasilkan dua pertanyaan berbeda pada basis data (satu untuk pengguna, satu lagi untuk perannya). Jika Anda ingin mencapai kinerja yang lebih baik, tambahkan metode berikut ke antarmuka repositori Spring Data JPA Anda:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

Metode ini akan menggunakan klausa fetch join JPQL untuk memuat asosiasi peran dengan penuh semangat dalam satu perjalanan pulang-pergi ke basis data, dan karenanya akan mengurangi penalti kinerja yang ditimbulkan oleh dua kueri berbeda dalam solusi di atas.

zagyi
sumber
3
Harap dicatat bahwa ini adalah solusi yang mudah, tetapi menghasilkan dua pertanyaan berbeda pada basis data (satu untuk pengguna, satu lagi untuk perannya). Jika Anda ingin mencapai kinerja yang lebih baik, cobalah menulis metode khusus yang dengan bersemangat menjemput pengguna dan peran terkaitnya dalam satu langkah menggunakan JPQL atau API Kriteria seperti yang disarankan orang lain.
zagyi
Saya sekarang meminta contoh untuk jawaban Jose, harus mengakui bahwa saya tidak mengerti sepenuhnya.
Matsemann
Silakan periksa solusi yang mungkin untuk metode kueri yang diinginkan dalam jawaban saya yang diperbarui.
zagyi
7
Hal yang menarik untuk dicatat, jika Anda jointanpa fetch, set akan dikembalikan bersama initialized = false; Oleh karena itu masih mengeluarkan permintaan kedua setelah set diakses. fetchadalah kunci untuk memastikan hubungan benar-benar dimuat dan menghindari permintaan kedua.
FGreg
Tampaknya masalah dengan melakukan keduanya dan mengambil dan bergabung adalah kriteria gabungan bergabung diabaikan dan Anda akhirnya mendapatkan semuanya dalam daftar atau peta. Jika Anda menginginkan semuanya, maka gunakan fetch, jika Anda menginginkan sesuatu yang spesifik, maka join, tetapi, seperti yang dikatakan, join tersebut akan kosong. Ini mengalahkan tujuan menggunakan pemuatan .LAZY.
K.Nicholas
37

Meskipun ini adalah pos lama, harap pertimbangkan untuk menggunakan @NamedEntityGraph (Javax Persistence) dan @EntityGraph (Spring Data JPA). Kombinasi tersebut berfungsi.

Contoh

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

dan kemudian repo musim semi seperti di bawah ini

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}
rakpan
sumber
1
Perhatikan bahwa itu @NamedEntityGraphadalah bagian dari JPA 2.1 API, yang tidak diterapkan di Hibernate sebelum versi 4.3.0.
naXa
2
@EntityGraph(attributePaths = "employeeGroups")dapat digunakan secara langsung dalam Spring Data Repository untuk memberi anotasi metode tanpa perlu @NamedEntityGraphpada @Entity Anda - kode lebih sedikit, mudah dimengerti ketika Anda membuka repo.
Desislav Kamenov
13

Anda memiliki beberapa opsi

  • Tulis metode pada repositori yang mengembalikan entitas yang diinisialisasi seperti yang disarankan RJ.

Lebih banyak pekerjaan, kinerja terbaik.

  • Gunakan OpenEntityManagerInViewFilter untuk menjaga sesi tetap terbuka untuk seluruh permintaan.

Lebih sedikit pekerjaan, biasanya dapat diterima di lingkungan web.

  • Gunakan kelas penolong untuk menginisialisasi entitas ketika diperlukan.

Lebih sedikit pekerjaan, berguna ketika OEMIV tidak pada pilihan, misalnya dalam aplikasi Swing, tetapi mungkin berguna juga pada implementasi repositori untuk menginisialisasi entitas apa pun dalam satu kesempatan.

Untuk opsi terakhir, saya menulis kelas utilitas, JpaUtils untuk memulai entitas di beberapa deph.

Sebagai contoh:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}
Jose Luis Martin
sumber
Karena semua permintaan saya adalah panggilan REST sederhana tanpa rendering dll, transaksi pada dasarnya adalah seluruh permintaan saya. Terima kasih atas masukan Anda.
Matsemann
Bagaimana saya melakukan yang pertama? Saya tahu cara menulis kueri, tetapi tidak bagaimana melakukan apa yang Anda katakan. Bisakah Anda tunjukkan sebuah contoh? Akan sangat membantu.
Matsemann
Namun, zagyi memberikan contoh dalam jawabannya, terima kasih telah menunjukkan saya ke arah yang benar.
Matsemann
Saya tidak tahu bagaimana kelas Anda akan dipanggil! solusi tidak selesai, buang waktu orang lain
Shady Sherif
Gunakan OpenEntityManagerInViewFilter untuk menjaga sesi terbuka untuk seluruh permintaan.- Gagasan buruk. Saya akan membuat permintaan tambahan untuk mengambil semua koleksi untuk entitas saya.
Yan Khonski
6

Saya pikir Anda perlu OpenSessionInViewFilter untuk menjaga sesi Anda tetap terbuka selama rendering tampilan (tapi itu bukan praktik yang terlalu baik).

Michail Nikolaev
sumber
1
Karena saya tidak menggunakan JSP atau apa pun, hanya membuat REST-api, @Transaksional akan lakukan untuk saya. Tetapi akan bermanfaat di lain waktu. Terima kasih.
Matsemann
@Matsemann Saya tahu ini sudah terlambat sekarang ... tetapi Anda dapat menggunakan OpenSessionInViewFilter bahkan di dalam pengontrol dan juga sesi akan ada hingga respons dikompilasi ...
Vishwas Shashidhar
@ Matemann Terima kasih! Anotasi transaksional membantu saya! fyi: Ini bahkan berfungsi jika Anda hanya memberi catatan pada superclass dari kelas istirahat.
desperateCoder
3

Data Musim Semi JpaRepository

Data Musim Semi JpaRepositorymendefinisikan dua metode berikut:

  • getOne, Yang mengembalikan sebuah proxy yang entitas yang cocok untuk menetapkan @ManyToOneatau @OneToOneasosiasi orangtua ketika bertahan entitas anak .
  • findById, yang mengembalikan entitas POJO setelah menjalankan pernyataan SELECT yang memuat entitas dari tabel terkait

Namun, dalam kasus Anda, Anda tidak menelepon getOneatau findById:

Person person = personRepository.findOne(1L);

Jadi, saya menganggap findOnemetode ini adalah metode yang Anda tetapkan dalam PersonRepository. Namun, findOnemetode ini tidak terlalu berguna dalam kasus Anda. Karena Anda perlu mengambil Personbersama dengan roleskoleksi, lebih baik menggunakan findOneWithRolesmetode sebagai gantinya.

Metode Data Musim Semi Kustom

Anda dapat mendefinisikan PersonRepositoryCustomantarmuka, sebagai berikut:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

Dan definisikan implementasinya seperti ini:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

Itu dia!

Vlad Mihalcea
sumber
Apakah ada alasan mengapa Anda menulis kueri sendiri dan tidak menggunakan solusi seperti EntityGraph dalam jawaban @rakpan? Bukankah ini menghasilkan hasil yang sama?
Jeroen Vandevelde
Overhead untuk menggunakan EntityGraph lebih tinggi dari permintaan JPQL. Pada akhirnya, Anda lebih baik menulis kueri.
Vlad Mihalcea
Bisakah Anda menguraikan tentang overhead (dari mana asalnya, apakah terlihat, ...)? Karena saya tidak mengerti mengapa ada overhead yang lebih tinggi jika keduanya menghasilkan kueri yang sama.
Jeroen Vandevelde
1
Karena paket EntityGraphs tidak di-cache seperti halnya JPQL. Itu bisa menjadi hit kinerja yang signifikan.
Vlad Mihalcea
1
Persis. Saya harus menulis artikel tentang itu ketika saya punya waktu.
Vlad Mihalcea
1

Anda dapat melakukan hal yang sama seperti ini:

@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

Cukup gunakan faqQuestions.getFaqAnswers (). Size () di controller Anda dan Anda akan mendapatkan ukuran jika daftar intialised malas, tanpa mengambil daftar itu sendiri.

neel4soft
sumber