Khi nào sử dụng EntityManager.find () so với EntityManager.getReference () với JPA


103

Tôi đã gặp một tình huống (mà tôi nghĩ là kỳ lạ nhưng có thể khá bình thường) trong đó tôi sử dụng EntityManager.getReference (LObj.getClass (), LObj.getId ()) để lấy một thực thể cơ sở dữ liệu và sau đó chuyển đối tượng trả về được tiếp tục trong một bảng khác.

Vì vậy, về cơ bản dòng chảy như thế này:

lớp TFacade {

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

@ TransactionAttributeType.REQUIRES_NEW
lớp FFacade {

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

@ TransactionAttributeType.REQUIRED
lớp FLHFacade {

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

Tôi nhận được ngoại lệ sau "java.lang.IllegalArgumentException: Unknown entity: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Sau khi xem xét nó một lúc, cuối cùng tôi đã phát hiện ra rằng đó là do tôi đang sử dụng phương thức EntityManager.getReference () mà tôi nhận được ngoại lệ ở trên vì phương thức này đang trả về một proxy.

Điều này khiến tôi tự hỏi, khi nào thì nên sử dụng phương thức EntityManager.getReference () thay vì phương thức EntityManager.find () ?

EntityManager.getReference () ném một EntityNotFoundException nếu nó không thể tìm thấy thực thể đang được tìm kiếm, điều này rất thuận tiện. Phương thức EntityManager.find () chỉ trả về null nếu nó không thể tìm thấy thực thể.

Liên quan đến ranh giới giao dịch, tôi nghe có vẻ như bạn cần sử dụng phương thức find () trước khi chuyển thực thể mới tìm thấy sang một giao dịch mới. Nếu bạn sử dụng phương thức getReference () thì có thể bạn sẽ gặp phải trường hợp tương tự như trường hợp của tôi với ngoại lệ ở trên.


Quên đề cập, tôi đang sử dụng Hibernate làm nhà cung cấp JPA.
SibzTer

Câu trả lời:


152

Tôi thường sử dụng phương thức getReference khi tôi không cần truy cập trạng thái cơ sở dữ liệu (ý tôi là phương thức getter). Chỉ để thay đổi trạng thái (ý tôi là phương pháp setter). Như bạn nên biết, getReference trả về một đối tượng proxy sử dụng một tính năng mạnh mẽ được gọi là kiểm tra bẩn tự động. Giả sử như sau

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

}

Nếu tôi gọi phương thức tìm kiếm , nhà cung cấp JPA, đằng sau hậu trường, sẽ gọi

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Nếu tôi gọi phương thức getReference , nhà cung cấp JPA, đằng sau hậu trường, sẽ gọi

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Và bạn biết tại sao không ???

Khi bạn gọi getReference, bạn sẽ nhận được một đối tượng proxy. Một cái gì đó giống như cái này (nhà cung cấp JPA chăm sóc việc triển khai proxy này)

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

}

Vì vậy, trước khi cam kết giao dịch, nhà cung cấp JPA sẽ nhìn thấy cờ stateChanged để cập nhật thực thể HOẶC KHÔNG phải người. Nếu không có hàng nào được cập nhật sau câu lệnh cập nhật, nhà cung cấp JPA sẽ ném EntityNotFoundException theo đặc tả JPA.

Trân trọng,


4
Tôi đang sử dụng EclipseLink 2.5.0 và các truy vấn được nêu ở trên không đúng. Nó luôn luôn phát hành SELECTtrước UPDATE, bất kể tôi sử dụng cái nào trong số find()/ getReference(). Điều tồi tệ hơn, SELECTđi ngang qua quan hệ KHÔNG HỢP LỆ (phát hành mới SELECTS), mặc dù tôi chỉ muốn cập nhật một trường duy nhất trong một thực thể.
Dejan Milosevic

1
@Arthur Ronald điều gì sẽ xảy ra nếu có chú thích Phiên bản trong thực thể được gọi bởi getReference?
David Hofmann

Tôi gặp vấn đề tương tự với @DejanMilosevic: khi xóa một thực thể thu được qua getReference (), một SELECT được cấp trên thực thể đó và nó truyền qua tất cả các quan hệ LAZY của thực thể đó, do đó tạo ra nhiều SELECTS (với EclipseLink 2.5.0).
Stéphane Appercel

27

Như tôi đã giải thích trong bài viết này , giả sử bạn có một Postthực thể mẹ và một thực thể con PostCommentnhư được minh họa trong sơ đồ sau:

nhập mô tả hình ảnh ở đây

Nếu bạn gọi findkhi cố gắng thiết lập @ManyToOne postliên kết:

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

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

entityManager.persist(comment);

Hibernate sẽ thực thi các câu lệnh sau:

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)

Truy vấn SELECT lần này vô dụng vì chúng tôi không cần tìm nạp thực thể Post. Chúng tôi chỉ muốn đặt cột Khóa ngoại post_id bên dưới.

Bây giờ, nếu bạn sử dụng getReferencethay thế:

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

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

entityManager.persist(comment);

Lần này, Hibernate sẽ chỉ đưa ra câu lệnh INSERT:

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

Không giống như find, getReferencechỉ trả về một Proxy thực thể chỉ có bộ định danh. Nếu bạn truy cập Proxy, câu lệnh SQL được liên kết sẽ được kích hoạt miễn là EntityManager vẫn mở.

Tuy nhiên, trong trường hợp này, chúng tôi không cần truy cập Proxy thực thể. Chúng tôi chỉ muốn truyền Khoá ngoại cho bản ghi bảng bên dưới để tải một Proxy là đủ cho trường hợp sử dụng này.

Khi tải một Proxy, bạn cần lưu ý rằng một LazyInitializationException có thể được ném ra nếu bạn cố gắng truy cập tham chiếu Proxy sau khi EntityManager bị đóng. Để biết thêm chi tiết về cách xử lý LazyInitializationException, hãy xem bài viết này .


1
Cảm ơn bạn Vlad đã cho chúng tôi biết điều này! Nhưng theo javadoc thì điều này có vẻ đáng lo ngại: "Thời gian chạy của trình cung cấp bền bỉ được phép ném EntityNotFoundException khi getReference được gọi". Điều này là không thể nếu không có SELECT (ít nhất là để kiểm tra sự tồn tại của hàng), phải không? Vì vậy, một SELECT cuối cùng phụ thuộc vào việc triển khai.
adrhc

3
Đối với trường hợp sử dụng mà bạn đã mô tả, Hibernate cung cấp thuộc tính hibernate.jpa.compliance.proxycấu hình , vì vậy bạn có thể chọn tuân thủ JPA hoặc hiệu suất truy cập dữ liệu tốt hơn.
Vlad Mihalcea,

Tại sao @VladMihalcea lại getReferencecần thiết nếu nó chỉ đủ để thiết lập phiên bản mới của mô hình với bộ PK. Tôi đang thiếu gì?
rilaby

Điều này chỉ được hỗ trợ trong Hibernarea sẽ không cho phép bạn tải liên kết nếu đi ngang.
Vlad Mihalcea

8

Bởi vì một tham chiếu được 'quản lý', nhưng không được hydrat hóa, nó cũng có thể cho phép bạn xóa một thực thể theo ID mà không cần tải nó vào bộ nhớ trước.

Vì bạn không thể xóa một thực thể không được quản lý, nên việc tải tất cả các trường bằng find (...) hoặc createQuery (...), chỉ để xóa nó ngay lập tức.

MyLargeObject myObject = em.getReference(MyLargeObject.class, objectId);
em.remove(myObject);

7

Điều này khiến tôi tự hỏi, khi nào thì nên sử dụng phương thức EntityManager.getReference () thay vì phương thức EntityManager.find ()?

EntityManager.getReference()thực sự là một phương pháp dễ xảy ra lỗi và thực sự có rất ít trường hợp mã máy khách cần sử dụng nó.
Cá nhân tôi, tôi không bao giờ cần sử dụng nó.

EntityManager.getReference () và EntityManager.find (): không có sự khác biệt về chi phí

Tôi không đồng ý với câu trả lời được chấp nhận và đặc biệt:

Nếu tôi gọi phương thức tìm kiếm , nhà cung cấp JPA, đằng sau hậu trường, sẽ gọi

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Nếu tôi gọi phương thức getReference , nhà cung cấp JPA, đằng sau hậu trường, sẽ gọi

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Đó không phải là hành vi mà tôi nhận được với Hibernate 5 và javadoc của getReference()không nói những điều như vậy:

Lấy một phiên bản, trạng thái của nó có thể được tìm nạp một cách lười biếng. Nếu cá thể được yêu cầu không tồn tại trong cơ sở dữ liệu, EntityNotFoundException sẽ được ném khi trạng thái cá thể được truy cập lần đầu tiên. (Thời gian chạy của trình cung cấp bền vững được phép ném EntityNotFoundException khi getReference được gọi.) Ứng dụng không nên mong đợi rằng trạng thái cá thể sẽ có sẵn khi tách ra, trừ khi nó được ứng dụng truy cập trong khi trình quản lý thực thể đang mở.

EntityManager.getReference() dành một truy vấn để truy xuất thực thể trong hai trường hợp:

1) nếu thực thể được lưu trữ trong bối cảnh Persistence, thì đó là bộ đệm cấp đầu tiên.
Và hành vi này không dành riêng cho EntityManager.getReference(), EntityManager.find()cũng sẽ dành một truy vấn để truy xuất thực thể nếu thực thể được lưu trữ trong ngữ cảnh Persistence.

Bạn có thể kiểm tra điểm đầu tiên với bất kỳ ví dụ nào.
Bạn cũng có thể dựa vào việc triển khai Hibernate thực tế.
Thật vậy, EntityManager.getReference()dựa vào createProxyIfNecessary()phương thức của org.hibernate.event.internal.DefaultLoadEventListenerlớp để tải thực thể.
Đây là cách triển khai của nó:

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

Phần thú vị là:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Nếu chúng tôi không thao tác hiệu quả thực thể, sẽ vang vọng đến javadoc được tải xuống một cách lười biếng .
Thật vậy, để đảm bảo việc tải thực thể một cách hiệu quả, cần phải gọi một phương thức trên nó.
Vì vậy, lợi ích sẽ liên quan đến một tình huống mà chúng ta muốn tải một thực thể mà không cần phải sử dụng nó? Trong khung các ứng dụng, nhu cầu này thực sự không phổ biến và ngoài ra getReference()hành vi cũng rất dễ gây hiểu lầm nếu bạn đọc phần tiếp theo.

Tại sao lại ưu tiên EntityManager.find () hơn EntityManager.getReference ()

Về chi phí, getReference()không phải là tốt hơn find()như đã thảo luận ở điểm trước.
Vậy tại sao lại sử dụng cái này hay cái kia?

Lời mời getReference()có thể trả về một thực thể được tìm nạp chậm.
Ở đây, tìm nạp lười biếng không đề cập đến các mối quan hệ của thực thể mà là chính thực thể đó.
Có nghĩa là nếu chúng ta gọi getReference()và sau đó ngữ cảnh Persistence bị đóng, thực thể có thể không bao giờ được tải và do đó, kết quả thực sự không thể đoán trước được. Ví dụ: nếu đối tượng proxy được tuần tự hóa, bạn có thể nhận được một nulltham chiếu dưới dạng kết quả được tuần tự hóa hoặc nếu một phương thức được gọi trên đối tượng proxy, một ngoại lệ chẳng hạn LazyInitializationExceptionsẽ được ném ra.

Nó có nghĩa là việc ném EntityNotFoundExceptionnó là lý do chính để sử dụng getReference()để xử lý một cá thể không tồn tại trong cơ sở dữ liệu vì tình huống lỗi có thể không bao giờ được thực hiện trong khi thực thể không tồn tại.

EntityManager.find()không có tham vọng ném EntityNotFoundExceptionnếu thực thể không được tìm thấy. Hành vi của nó vừa đơn giản vừa rõ ràng. Bạn sẽ không bao giờ ngạc nhiên vì nó luôn trả về một thực thể được tải hoặc null(nếu thực thể không được tìm thấy) nhưng không bao giờ là một thực thể dưới hình dạng của một proxy có thể không được tải một cách hiệu quả.
Vì vậy EntityManager.find()nên được ưa chuộng trong hầu hết các trường hợp.


Lý do của bạn là sai lệch khi so sánh với phản hồi được chấp nhận + phản hồi của Vlad Mihalcea + nhận xét của tôi cho Vlad Mihalcea (có thể ít quan trọng hơn sau cùng này +).
adrhc

1
Pro JPA2 nêu rõ: "Với tình huống cụ thể mà getReference () có thể được sử dụng, find () nên được sử dụng trong hầu hết mọi trường hợp".
JL_SO

Ủng hộ câu hỏi này vì nó là sự bổ sung cần thiết cho câu trả lời được chấp nhận và vì các thử nghiệm của tôi cho thấy rằng, khi đặt thuộc tính của proxy thực thể, nó được tìm nạp từ cơ sở dữ liệu, trái với những gì câu trả lời được chấp nhận cho biết. Chỉ có trường hợp do Vlad nêu đã vượt qua bài kiểm tra của tôi.
João Fé

2

Tôi không đồng ý với câu trả lời đã chọn và như davidxxx đã chỉ ra một cách chính xác, getReference không cung cấp hành vi cập nhật động mà không có lựa chọn. Tôi đã hỏi một câu hỏi liên quan đến tính hợp lệ của câu trả lời này, xem tại đây - không thể cập nhật mà không đưa ra lựa chọn khi sử dụng setter sau getReference () của JPA ngủ đông .

Tôi thực sự chưa thấy bất kỳ ai thực sự sử dụng chức năng đó. BẤT CỨ Ở ĐÂU. Và tôi không hiểu tại sao nó được ủng hộ như vậy.

Trước hết, bất kể bạn gọi gì trên đối tượng proxy ngủ đông, setter hay getter, SQL sẽ được kích hoạt và đối tượng được tải.

Nhưng sau đó tôi nghĩ, vậy điều gì sẽ xảy ra nếu proxy JPA getReference () không cung cấp chức năng đó. Tôi chỉ có thể viết proxy của riêng mình.

Bây giờ, tất cả chúng ta đều có thể tranh luận rằng các lựa chọn trên khóa chính nhanh như một truy vấn có thể nhận được và nó không thực sự là điều gì đó cần phải tránh xa. Nhưng đối với những người trong chúng ta, những người không thể xử lý nó vì lý do này hay lý do khác, dưới đây là cách triển khai một proxy như vậy. Nhưng trước khi tôi nhìn thấy việc thực hiện, hãy xem cách sử dụng và cách sử dụng nó đơn giản.

SỬ DỤNG

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

Và điều này sẽ kích hoạt truy vấn sau:

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

và ngay cả khi bạn muốn chèn, bạn vẫn có thể thực hiện PersistenceService.save (new Order ("a", 2)); và nó sẽ kích hoạt một phụ trang như nó cần.

THỰC HIỆN

Thêm cái này vào pom.xml của bạn -

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

Tạo lớp này để tạo proxy động -

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

Tạo giao diện với tất cả các phương pháp -

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

Bây giờ, hãy tạo một bộ đánh chặn cho phép bạn triển khai các phương pháp này trên proxy của mình -

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

Và lớp ngoại lệ -

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

Một dịch vụ để tiết kiệm bằng cách sử dụng proxy này -

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

}
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.