Kapan menggunakan EntityManager.find () vs EntityManager.getReference () dengan JPA

103

Saya telah menemukan situasi (yang menurut saya aneh tetapi mungkin cukup normal) di mana saya menggunakan EntityManager.getReference (LObj.getClass (), LObj.getId ()) untuk mendapatkan entitas database dan kemudian meneruskan objek yang dikembalikan ke dipertahankan di tabel lain.

Jadi pada dasarnya alirannya seperti ini:

class TFacade {

  createT (FObj, AObj) {
    T TObj = T baru ();
    TObj.setF (FObj);
    TObj.setA (AObj);
    ...
    EntityManager.persist (TObj);
    ...
    L LObj = A. getL ();
    FObj.setL (LObj);
    FFacade.editF (FObj);
  }
}

@ TransactionAttributeType.REQUIRES_NEW
class FFacade {

  editF (FObj) {
    L LObj = FObj.getL ();
    LObj = EntityManager.getReference (LObj.getClass (), LObj.getId ());
    ...
    EntityManager.merge (FObj);
    ...
    FLHFacade.create (FObj, LObj);
  }
}

@ TransactionAttributeType.REQUIRED
class FLHFacade {

  createFLH (FObj, LObj) {
    FLH FLHObj = FLH baru ();
    FLHObj.setF (FObj);
    FLHObj.setL (LObj);
    ....
    EntityManager.persist (FLHObj);
    ...
  }
}

Saya mendapatkan pengecualian berikut "java.lang.IllegalArgumentException: Entitas tidak diketahui: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Setelah memeriksanya beberapa saat, saya akhirnya menemukan bahwa itu karena saya menggunakan metode EntityManager.getReference () sehingga saya mendapatkan pengecualian di atas karena metode tersebut mengembalikan proxy.

Hal ini membuat saya bertanya-tanya, kapan sebaiknya menggunakan metode EntityManager.getReference () daripada metode EntityManager.find () ?

EntityManager.getReference () melontarkan EntityNotFoundException jika tidak dapat menemukan entitas yang sedang dicari yang sangat nyaman untuk dirinya sendiri. Metode EntityManager.find () hanya mengembalikan null jika tidak dapat menemukan entitas.

Berkenaan dengan batasan transaksi, bagi saya sepertinya Anda perlu menggunakan metode find () sebelum meneruskan entitas yang baru ditemukan ke transaksi baru. Jika Anda menggunakan metode getReference () maka Anda mungkin akan berakhir dalam situasi yang mirip dengan saya dengan pengecualian di atas.

SibzTer
sumber
Lupa menyebutkan, saya menggunakan Hibernate sebagai penyedia JPA.
SibzTer

Jawaban:

152

Saya biasanya menggunakan metode getReference ketika saya tidak perlu mengakses status database (maksud saya metode pengambil). Hanya untuk mengubah keadaan (maksud saya metode penyetel). Seperti yang Anda ketahui, getReference mengembalikan objek proxy yang menggunakan fitur canggih yang disebut pemeriksaan kotor otomatis. Misalkan berikut ini

public class Person {

    private String name;
    private Integer age;

}


public class PersonServiceImpl implements PersonService {

    public void changeAge(Integer personId, Integer newAge) {
        Person person = em.getReference(Person.class, personId);

        // person is a proxy
        person.setAge(newAge);
    }

}

Jika saya memanggil metode find , penyedia JPA, di belakang layar, akan memanggil

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Jika saya memanggil metode getReference , penyedia JPA, di belakang layar, akan memanggil

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Dan kamu tahu kenapa ???

Saat Anda memanggil getReference, Anda akan mendapatkan objek proxy. Sesuatu seperti ini (penyedia JPA menangani penerapan proxy ini)

public class PersonProxy {

    // JPA provider sets up this field when you call getReference
    private Integer personId;

    private String query = "UPDATE PERSON SET ";

    private boolean stateChanged = false;

    public void setAge(Integer newAge) {
        stateChanged = true;

        query += query + "AGE = " + newAge;
    }

}

Jadi sebelum melakukan transaksi, penyedia JPA akan melihat bendera stateChanged untuk memperbarui ATAU BUKAN entitas orang. Jika tidak ada baris yang diperbarui setelah pernyataan update, penyedia JPA akan menampilkan EntityNotFoundException sesuai dengan spesifikasi JPA.

salam,

Arthur Ronald
sumber
4
Saya menggunakan EclipseLink 2.5.0 dan kueri yang disebutkan di atas tidak benar. Itu selalu mengeluarkan SELECTsebelumnya UPDATE, tidak peduli yang mana dari find()/ getReference()saya gunakan. Yang lebih buruk, SELECTmelintasi hubungan NON-LAZY (mengeluarkan yang baru SELECTS), meskipun saya hanya ingin memperbarui satu bidang dalam satu entitas.
Dejan Milosevic
1
@Arthur Ronald apa yang terjadi jika ada penjelasan Versi dalam entitas yang disebut oleh getReference?
David Hofmann
Saya memiliki masalah yang sama dengan @DejanMilosevic: ketika menghapus entitas yang diperoleh melalui getReference (), SELECT dikeluarkan pada entitas itu dan melintasi semua hubungan LAZY dari entitas itu, sehingga mengeluarkan banyak SELECTS (dengan EclipseLink 2.5.0).
Stéphane Appercel
27

Seperti yang saya jelaskan di artikel ini , dengan asumsi Anda memiliki Postentitas induk dan anak PostCommentseperti yang diilustrasikan dalam diagram berikut:

masukkan deskripsi gambar di sini

Jika Anda menelepon findketika Anda mencoba untuk mengatur @ManyToOne postasosiasi:

PostComment comment = new PostComment();
comment.setReview("Just awesome!");

Post post = entityManager.find(Post.class, 1L);
comment.setPost(post);

entityManager.persist(comment);

Hibernate akan menjalankan pernyataan berikut:

SELECT p.id AS id1_0_0_,
       p.title AS title2_0_0_
FROM   post p
WHERE p.id = 1

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Just awesome!', 1)

Query SELECT tidak berguna kali ini karena kita tidak membutuhkan entitas Post untuk diambil. Kami hanya ingin mengatur kolom Kunci Asing post_id yang mendasarinya.

Sekarang, jika Anda menggunakan getReference:

PostComment comment = new PostComment();
comment.setReview("Just awesome!");

Post post = entityManager.getReference(Post.class, 1L);
comment.setPost(post);

entityManager.persist(comment);

Kali ini, Hibernate hanya akan mengeluarkan pernyataan INSERT:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Just awesome!', 1)

Berbeda finddengangetReference hanya mengembalikan proxy entitas yang hanya memiliki set pengenal. Jika Anda mengakses Proxy, pernyataan SQL terkait akan dipicu selama EntityManager masih terbuka.

Namun, dalam kasus ini, kami tidak perlu mengakses proxy entitas. Kami hanya ingin menyebarkan Kunci Asing ke rekaman tabel yang mendasari sehingga memuat Proxy sudah cukup untuk kasus penggunaan ini.

Saat memuat Proxy, Anda perlu menyadari bahwa LazyInitializationException dapat muncul jika Anda mencoba mengakses referensi Proxy setelah EntityManager ditutup. Untuk detail lebih lanjut tentang penanganan LazyInitializationException, lihat artikel ini .

Vlad Mihalcea
sumber
1
Terima kasih Vlad karena telah memberi tahu kami tentang hal ini! Namun menurut javadoc, hal ini tampaknya mengganggu: "Waktu proses penyedia persistensi diizinkan untuk menampilkan EntityNotFoundException saat getReference dipanggil". Ini tidak mungkin tanpa SELECT (setidaknya untuk memeriksa keberadaan baris), bukan? Jadi akhirnya SELECT tergantung pada implementasinya.
adrhc
3
Untuk kasus penggunaan yang Anda jelaskan, Hibernate menawarkan hibernate.jpa.compliance.proxyproperti konfigurasi , sehingga Anda dapat memilih kepatuhan JPA atau kinerja akses data yang lebih baik.
Vlad Mihalcea
@VladMihalcea mengapa getReferencediperlukan sama sekali jika itu cukup untuk menetapkan contoh model baru dengan set PK. Apa yang saya lewatkan?
rilaby
Ini hanya didukung di Hibernarea tidak akan memungkinkan Anda untuk memuat asosiasi jika dilalui.
Vlad Mihalcea
8

Karena referensi 'dikelola', tetapi tidak terhidrasi, referensi juga memungkinkan Anda untuk menghapus entitas berdasarkan ID, tanpa perlu memuatnya ke dalam memori terlebih dahulu.

Karena Anda tidak dapat menghapus entitas yang tidak dikelola, itu konyol untuk memuat semua bidang menggunakan find (...) atau createQuery (...), hanya untuk segera menghapusnya.

MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);
Steven Spungin
sumber
7

Hal ini membuat saya bertanya-tanya, kapan sebaiknya menggunakan metode EntityManager.getReference () daripada metode EntityManager.find ()?

EntityManager.getReference()benar-benar merupakan metode rawan kesalahan dan hanya ada sedikit kasus di mana kode klien perlu menggunakannya.
Secara pribadi, saya tidak pernah perlu menggunakannya.

EntityManager.getReference () dan EntityManager.find (): tidak ada perbedaan dalam hal overhead

Saya tidak setuju dengan jawaban yang diterima dan khususnya:

Jika saya memanggil metode find , penyedia JPA, di belakang layar, akan memanggil

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Jika saya memanggil metode getReference , penyedia JPA, di belakang layar, akan memanggil

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Ini bukan perilaku yang saya dapatkan dengan Hibernate 5 dan javadoc getReference()tidak mengatakan hal seperti itu:

Dapatkan sebuah instance, yang statusnya mungkin diambil dengan malas. Jika instance yang diminta tidak ada dalam database, EntityNotFoundException dilemparkan saat status instance pertama kali diakses. (Waktu proses penyedia persistensi diizinkan untuk menampilkan EntityNotFoundException saat getReference dipanggil.) Aplikasi tidak boleh berharap bahwa status instance akan tersedia saat dilepas, kecuali jika itu diakses oleh aplikasi saat pengelola entitas terbuka.

EntityManager.getReference() menyimpan kueri untuk mengambil entitas dalam dua kasus:

1) jika entitas disimpan dalam konteks Ketekunan, itu adalah cache tingkat pertama.
Dan perilaku ini tidak khusus untuk EntityManager.getReference(), EntityManager.find()juga akan menyimpan kueri untuk mengambil entitas jika entitas disimpan dalam konteks Persistensi.

Anda dapat memeriksa poin pertama dengan contoh apa pun.
Anda juga dapat mengandalkan implementasi Hibernate yang sebenarnya.
Memang, EntityManager.getReference()bergantung pada createProxyIfNecessary()metode org.hibernate.event.internal.DefaultLoadEventListenerkelas untuk memuat entitas.
Berikut implementasinya:

private Object createProxyIfNecessary(
        final LoadEvent event,
        final EntityPersister persister,
        final EntityKey keyToLoad,
        final LoadEventListener.LoadType options,
        final PersistenceContext persistenceContext) {
    Object existing = persistenceContext.getEntity( keyToLoad );
    if ( existing != null ) {
        // return existing object or initialized proxy (unless deleted)
        if ( traceEnabled ) {
            LOG.trace( "Entity found in session cache" );
        }
        if ( options.isCheckDeleted() ) {
            EntityEntry entry = persistenceContext.getEntry( existing );
            Status status = entry.getStatus();
            if ( status == Status.DELETED || status == Status.GONE ) {
                return null;
            }
        }
        return existing;
    }
    if ( traceEnabled ) {
        LOG.trace( "Creating new proxy for entity" );
    }
    // return new uninitialized proxy
    Object proxy = persister.createProxy( event.getEntityId(), event.getSession() );
    persistenceContext.getBatchFetchQueue().addBatchLoadableEntityKey( keyToLoad );
    persistenceContext.addProxy( keyToLoad, proxy );
    return proxy;
}

Bagian yang menarik adalah:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Jika kita tidak secara efektif memanipulasi entitas, menggemakan javadoc yang diambil dengan malas .
Memang, untuk memastikan pemuatan yang efektif dari entitas, penerapan metode diperlukan.
Jadi, keuntungannya akan terkait dengan skenario di mana kita ingin memuat entitas tanpa perlu menggunakannya? Dalam kerangka aplikasi, kebutuhan ini sangat jarang dan selain itu getReference()perilakunya juga sangat menyesatkan jika Anda membaca bagian selanjutnya.

Mengapa lebih menyukai EntityManager.find () daripada EntityManager.getReference ()

Dari segi overhead, getReference()memang tidak lebih baik dari yang find()dibahas pada poin sebelumnya.
Jadi mengapa menggunakan yang satu atau yang lainnya?

Memanggil getReference()dapat mengembalikan entitas yang diambil dengan lambat.
Di sini, lazy fetching tidak merujuk pada hubungan entitas tetapi entitas itu sendiri.
Artinya jika kita memanggil getReference()dan kemudian konteks Persistence ditutup, entitas mungkin tidak pernah dimuat dan hasilnya benar-benar tidak dapat diprediksi. Misalnya jika objek proxy berseri, Anda bisa mendapatkan nullreferensi sebagai hasil berseri atau jika metode dipanggil pada objek proxy, pengecualian sepertiLazyInitializationException dilempar.

Artinya lemparan EntityNotFoundExceptionitu adalah alasan utama penggunaangetReference() untuk menangani contoh yang tidak ada dalam database karena situasi kesalahan mungkin tidak pernah dilakukan saat entitas tidak ada.

EntityManager.find()tidak memiliki ambisi untuk melempar EntityNotFoundExceptionjika entitas tidak ditemukan. Perilakunya sederhana dan jelas. Anda tidak akan terkejut karena selalu mengembalikan entitas yang dimuat atau null(jika entitas tidak ditemukan) tetapi tidak pernah entitas di bawah bentuk proxy yang mungkin tidak dimuat secara efektif.
Jadi EntityManager.find()harus disukai di sebagian besar kasus.

davidxxx
sumber
Alasan Anda menyesatkan jika dibandingkan dengan tanggapan yang diterima + tanggapan Vlad Mihalcea + komentar saya untuk Vlad Mihalcea (mungkin + yang terakhir ini kurang penting).
adrhc
1
Pro JPA2 menyatakan: "Mengingat situasi yang sangat spesifik di mana getReference () dapat digunakan, find () harus digunakan di hampir semua kasus".
JL_SO
Beri suara positif pada pertanyaan ini karena ini adalah pelengkap yang diperlukan untuk jawaban yang diterima dan karena pengujian saya menunjukkan bahwa, saat menyetel properti proxy entitas, itu diambil dari database, bertentangan dengan jawaban yang diterima. Hanya kasus yang dinyatakan oleh Vlad yang lulus ujian saya.
João Fé
2

Saya tidak setuju dengan jawaban yang dipilih, dan seperti yang ditunjukkan davidxxx dengan benar, getReference tidak menyediakan perilaku pembaruan dinamis tanpa pemilihan. Saya mengajukan pertanyaan tentang validitas jawaban ini, lihat di sini - tidak dapat memperbarui tanpa memilih menggunakan penyetel setelah getReference () dari hibernate JPA .

Sejujurnya saya belum melihat siapa pun yang benar-benar menggunakan fungsi itu. DI MANA SAJA. Dan saya tidak mengerti mengapa itu sangat disukai.

Sekarang pertama-tama, apa pun yang Anda panggil pada objek proxy hibernasi, penyetel atau pengambil, SQL dijalankan dan objek dimuat.

Tapi kemudian saya berpikir, jadi bagaimana jika JPA getReference () proxy tidak menyediakan fungsionalitas itu. Saya bisa menulis proxy saya sendiri.

Sekarang, kita semua dapat berargumen bahwa pemilihan pada kunci primer secepat yang bisa didapat kueri dan itu bukan sesuatu yang harus dihindari. Namun bagi kita yang tidak bisa mengatasinya karena satu dan lain hal, di bawah ini adalah implementasi dari proxy tersebut. Tetapi sebelum saya Anda melihat implementasinya, lihat penggunaannya dan betapa sederhananya penggunaannya.

PEMAKAIAN

Order example = ProxyHandler.getReference(Order.class, 3);
example.setType("ABCD");
example.setCost(10);
PersistenceService.save(example);

Dan ini akan mengaktifkan kueri berikut -

UPDATE Order SET type = 'ABCD' and cost = 10 WHERE id = 3;

dan bahkan jika Anda ingin memasukkan, Anda masih bisa melakukan PersistenceService.save (new Order ("a", 2)); dan itu akan mengaktifkan sisipan sebagaimana mestinya.

PENERAPAN

Tambahkan ini ke pom.xml Anda -

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>

Jadikan kelas ini untuk membuat proxy dinamis -

@SuppressWarnings("unchecked")
public class ProxyHandler {

public static <T> T getReference(Class<T> classType, Object id) {
    if (!classType.isAnnotationPresent(Entity.class)) {
        throw new ProxyInstantiationException("This is not an entity!");
    }

    try {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(classType);
        enhancer.setCallback(new ProxyMethodInterceptor(classType, id));
        enhancer.setInterfaces((new Class<?>[]{EnhancedProxy.class}));
        return (T) enhancer.create();
    } catch (Exception e) {
        throw new ProxyInstantiationException("Error creating proxy, cause :" + e.getCause());
    }
}

Buat antarmuka dengan semua metode -

public interface EnhancedProxy {
    public String getJPQLUpdate();
    public HashMap<String, Object> getModifiedFields();
}

Sekarang, buat interseptor yang memungkinkan Anda menerapkan metode ini pada proxy Anda -

import com.anil.app.exception.ProxyInstantiationException;
import javafx.util.Pair;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import javax.persistence.Id;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author Anil Kumar
*/
public class ProxyMethodInterceptor implements MethodInterceptor, EnhancedProxy {

private Object target;
private Object proxy;
private Class classType;
private Pair<String, Object> primaryKey;
private static HashSet<String> enhancedMethods;

ProxyMethodInterceptor(Class classType, Object id) throws IllegalAccessException, InstantiationException {
    this.classType = classType;
    this.target = classType.newInstance();
    this.primaryKey = new Pair<>(getPrimaryKeyField().getName(), id);
}

static {
    enhancedMethods = new HashSet<>();
    for (Method method : EnhancedProxy.class.getDeclaredMethods()) {
        enhancedMethods.add(method.getName());
    }
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    //intercept enhanced methods
    if (enhancedMethods.contains(method.getName())) {
        this.proxy = obj;
        return method.invoke(this, args);
    }
    //else invoke super class method
    else
        return proxy.invokeSuper(obj, args);
}

@Override
public HashMap<String, Object> getModifiedFields() {
    HashMap<String, Object> modifiedFields = new HashMap<>();
    try {
        for (Field field : classType.getDeclaredFields()) {

            field.setAccessible(true);

            Object initialValue = field.get(target);
            Object finalValue = field.get(proxy);

            //put if modified
            if (!Objects.equals(initialValue, finalValue)) {
                modifiedFields.put(field.getName(), finalValue);
            }
        }
    } catch (Exception e) {
        return null;
    }
    return modifiedFields;
}

@Override
public String getJPQLUpdate() {
    HashMap<String, Object> modifiedFields = getModifiedFields();
    if (modifiedFields == null || modifiedFields.isEmpty()) {
        return null;
    }
    StringBuilder fieldsToSet = new StringBuilder();
    for (String field : modifiedFields.keySet()) {
        fieldsToSet.append(field).append(" = :").append(field).append(" and ");
    }
    fieldsToSet.setLength(fieldsToSet.length() - 4);
    return "UPDATE "
            + classType.getSimpleName()
            + " SET "
            + fieldsToSet
            + "WHERE "
            + primaryKey.getKey() + " = " + primaryKey.getValue();
}

private Field getPrimaryKeyField() throws ProxyInstantiationException {
    for (Field field : classType.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.isAnnotationPresent(Id.class))
            return field;
    }
    throw new ProxyInstantiationException("Entity class doesn't have a primary key!");
}
}

Dan kelas pengecualian -

public class ProxyInstantiationException extends RuntimeException {
public ProxyInstantiationException(String message) {
    super(message);
}

Layanan untuk menyimpan menggunakan proxy ini -

@Service
public class PersistenceService {

@PersistenceContext
private EntityManager em;

@Transactional
private void save(Object entity) {
    // update entity for proxies
    if (entity instanceof EnhancedProxy) {
        EnhancedProxy proxy = (EnhancedProxy) entity;
        Query updateQuery = em.createQuery(proxy.getJPQLUpdate());
        for (Entry<String, Object> entry : proxy.getModifiedFields().entrySet()) {
            updateQuery.setParameter(entry.getKey(), entry.getValue());
        }
        updateQuery.executeUpdate();
    // insert otherwise
    } else {
        em.persist(entity);
    }

}
}
Anil Kumar
sumber