JPA와 함께 EntityManager.find () 대 EntityManager.getReference ()를 사용하는 경우


103

EntityManager.getReference (LObj.getClass (), LObj.getId ())를 사용하여 데이터베이스 엔터티를 가져온 다음 반환 된 개체를 전달하는 상황 (이상하다고 생각하지만 아마도 매우 정상적 일 수 있음)을 발견했습니다. 다른 테이블에 유지됩니다.

따라서 기본적으로 흐름은 다음과 같습니다.

class TFacade {

  createT (FObj, AObj) {
    T TObj = 새로운 T ();
    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 ();
    FLHObj.setF (FObj);
    FLHObj.setL (LObj);
    ....
    EntityManager.persist (FLHObj);
    ...
  }
}

"java.lang.IllegalArgumentException : 알 수없는 엔티티 : com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"예외가 발생했습니다.

잠시 살펴본 후 마침내 EntityManager.getReference () 메서드를 사용했기 때문에 메서드가 프록시를 반환 할 때 위의 예외가 발생한다는 것을 알게되었습니다.

이것은 언제 EntityManager.find () 메서드 대신 EntityManager.getReference () 메서드를 사용하는 것이 좋을까요?

EntityManager.getReference ()는 자체적으로 매우 편리한 검색중인 엔티티를 찾을 수없는 경우 EntityNotFoundException을 발생시킵니다. EntityManager.find () 메서드는 엔티티를 찾을 수없는 경우 null을 반환합니다.

트랜잭션 경계와 관련하여 새로 발견 된 엔터티를 새 트랜잭션에 전달하기 전에 find () 메서드를 사용해야하는 것처럼 들립니다. getReference () 메서드를 사용하면 위의 예외를 제외하고는 내 상황과 비슷한 상황에 처하게 될 것입니다.


언급하는 것을 잊었습니다. 저는 Hibernate를 JPA 공급자로 사용하고 있습니다.
SibzTer

답변:


152

일반적으로 데이터베이스 상태에 액세스 할 필요가 없을 때 getReference 메서드를 사용합니다 (getter 메서드를 의미합니다). 상태를 변경하기 위해 (내 말은 setter 메서드를 의미합니다). 아시다시피 getReference는 자동 더티 검사라는 강력한 기능을 사용하는 프록시 객체를 반환합니다. 다음을 가정하십시오.

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

}

find 메소드를 호출하면 JPA 제공자가 백그라운드에서

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

getReference 메서드를 호출하면 JPA 공급자가 백그라운드에서

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

그리고 당신은 이유를 압니다 ???

getReference를 호출하면 프록시 객체를 얻게됩니다. 이와 같은 것 (JPA 공급자가이 프록시 구현을 처리 함)

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

}

따라서 트랜잭션 커밋 전에 JPA 공급자는 사람 엔티티를 업데이트하거나 업데이트하기 위해 stateChanged 플래그를 보게됩니다. 업데이트 문 후에 행이 업데이트되지 않으면 JPA 공급자는 JPA 사양에 따라 EntityNotFoundException을 throw합니다.

문안 인사,


4
EclipseLink 2.5.0을 사용하고 있으며 위에서 언급 한 쿼리가 올바르지 않습니다. / 내가 사용 하는 것에 상관없이 항상 SELECTbefore를 발행합니다 . 더 나쁜 것은 한 엔터티에서 단일 필드를 업데이트하고 싶지만 NON-LAZY 관계 (new 발행 )를 순회 한다는 것입니다. UPDATEfind()getReference()SELECTSELECTS
Dejan Milosevic

1
@Arthur Ronald getReference가 호출 한 엔티티에 Version 주석이 있으면 어떻게됩니까?
데이비드 호프만

@DejanMilosevic과 동일한 문제가 있습니다. getReference ()를 통해 얻은 엔티티를 제거하면 해당 엔티티에서 SELECT가 발행되고 해당 엔티티의 모든 LAZY 관계를 통과하므로 많은 SELECTS (EclipseLink 2.5.0 사용)를 발행합니다.
Stéphane Appercel

27

이 문서 에서 설명했듯이 다음 다이어그램에 설명 된대로 부모 Post엔터티와 자식 이 있다고 가정합니다 PostComment.

여기에 이미지 설명 입력

연결 find을 설정하려고 할 때 전화하는 경우 @ManyToOne post:

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

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

entityManager.persist(comment);

Hibernate는 다음 문을 실행합니다.

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)

이번에는 Post 엔터티를 가져올 필요가 없기 때문에 SELECT 쿼리는 쓸모가 없습니다. 기본 post_id 외래 키 열만 설정하려고합니다.

이제 getReference대신 사용 하는 경우 :

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

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

entityManager.persist(comment);

이번에는 Hibernate가 INSERT 문만 발행합니다.

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

달리 find의이 getReference유일한 식별자 만 세트가 엔티티 프록시를 반환합니다. 프록시에 액세스하는 경우 EntityManager가 아직 열려있는 한 연관된 ​​SQL 문이 트리거됩니다.

그러나이 경우 엔터티 프록시에 액세스 할 필요가 없습니다. 우리는 외래 키를 기본 테이블 레코드로만 전파하기를 원하므로이 사용 사례에는 프록시를로드하는 것으로 충분합니다.

Proxy를로드 할 때 EntityManager가 닫힌 후 Proxy 참조에 액세스하려고하면 LazyInitializationException이 발생할 수 있음을 알아야합니다. 취급에 대한 자세한 내용은 이 기사를LazyInitializationException 확인 하십시오 .


1
이것을 알려 주셔서 Vlad에게 감사드립니다! 그러나 javadoc에 따르면 이것은 "지속성 공급자 런타임이 getReference가 호출 될 때 EntityNotFoundException을 throw 할 수 있도록 허용됩니다."라는 혼란스러워 보입니다. 이것은 SELECT 없이는 불가능합니다 (적어도 행 존재를 확인하기 위해), 그렇지 않습니까? 따라서 결국 SELECT는 구현에 따라 다릅니다.
adrhc

3
설명한 사용 사례에 대해 Hibernate는 hibernate.jpa.compliance.proxy구성 속성을 제공 하므로 JPA 준수 또는 더 나은 데이터 액세스 성능을 선택할 수 있습니다.
Vlad Mihalcea

@VladMihalcea getReferencePK 세트로 모델의 새 인스턴스를 설정하는 것만으로도 충분하다면 왜 필요합니다. 내가 무엇을 놓치고 있습니까?
rilaby

이것은 Hibernarea에서만 지원되며 순회하는 경우 연관을로드 할 수 없습니다.
Vlad Mihalcea

8

참조는 '관리'되지만 수화되지는 않기 때문에 먼저 메모리에로드 할 필요없이 ID로 엔티티를 제거 할 수 있습니다.

관리되지 않는 엔터티를 제거 할 수 없기 때문에 find (...) 또는 createQuery (...)를 사용하여 모든 필드를로드하는 것은 단지 즉시 삭제하는 것뿐입니다.

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

7

이것은 언제 EntityManager.find () 메서드 대신 EntityManager.getReference () 메서드를 사용하는 것이 좋을까요?

EntityManager.getReference()오류가 발생하기 쉬운 방법이며 클라이언트 코드에서이를 사용해야하는 경우는 거의 없습니다.
개인적으로 나는 그것을 사용할 필요가 없었습니다.

EntityManager.getReference () 및 EntityManager.find () : 오버 헤드 측면에서 차이 없음

나는 받아 들인 대답에 동의하지 않으며 특히 :

find 메소드를 호출하면 JPA 제공자가 백그라운드에서

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

getReference 메서드를 호출하면 JPA 공급자가 백그라운드에서

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Hibernate 5에서 얻는 행동이 아니며 javadoc은 getReference()그런 말 을 하지 않습니다.

상태를 느리게 가져올 수있는 인스턴스를 가져옵니다. 요청 된 인스턴스가 데이터베이스에 없으면 인스턴스 상태에 처음 액세스 할 때 EntityNotFoundException이 발생합니다. (지속성 공급자 런타임은 getReference가 호출 될 때 EntityNotFoundException을 발생시킬 수 있습니다.) 응용 프로그램은 엔티티 관리자가 열려있는 동안 응용 프로그램에서 액세스하지 않는 한 분리시 인스턴스 상태를 사용할 수있을 것이라고 기 대해서는 안됩니다.

EntityManager.getReference() 두 가지 경우에 항목을 검색하기 위해 쿼리를 예비합니다.

1) 엔터티가 Persistence 컨텍스트에 저장된 경우 첫 번째 수준 캐시입니다.
그리고이 문제는 특정하지 않습니다 EntityManager.getReference(), EntityManager.find()기업이 영속 컨텍스트에 저장되어있는 경우도 예비 쿼리는 개체를 검색하는 것입니다.

예를 들어 첫 번째 요점을 확인할 수 있습니다.
실제 Hibernate 구현에 의존 할 수도 있습니다.
실제로 클래스 EntityManager.getReference()createProxyIfNecessary()메서드에 의존 org.hibernate.event.internal.DefaultLoadEventListener하여 엔티티를로드합니다.
구현은 다음과 같습니다.

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

흥미로운 부분은 다음과 같습니다.

Object existing = persistenceContext.getEntity( keyToLoad );

2) 엔터티를 효과적으로 조작하지 않으면 javadoc을 느리게 가져 오는 것과 같습니다.
실제로 엔티티의 효과적인로드를 보장하려면 엔티티에 대한 메소드를 호출해야합니다.
따라서 이득은 사용할 필요없이 엔티티를로드하려는 시나리오와 관련이 있습니까? 응용 프로그램 프레임에서 이러한 요구는 매우 드물며 getReference()다음 부분을 읽으면 동작도 매우 오해의 소지가 있습니다.

EntityManager.getReference ()보다 EntityManager.find ()를 선호하는 이유

오버 헤드 측면 에서 이전 요점에서 설명한 getReference()것보다 낫지 않습니다 find().
그렇다면 왜 둘 중 하나를 사용합니까?

호출하면 getReference()느리게 가져온 항목이 반환 될 수 있습니다.
여기서 지연 가져 오기는 엔티티의 관계가 아니라 엔티티 자체를 참조합니다.
이는 우리가 호출 getReference()한 다음 Persistence 컨텍스트가 닫히면 엔티티가로드되지 않을 수 있으므로 결과를 실제로 예측할 수 없음을 의미합니다. 예를 들어 프록시 객체가 직렬화 된 null경우 직렬화 된 결과로 참조를 가져 오거나 프록시 객체에서 메서드가 호출되면 다음과 같은 예외 LazyInitializationException가 발생합니다.

이는 엔티티가 존재하지 않는 동안 오류 상황이 수행되지 않을 수 있으므로 데이터베이스에 존재하지 않는 인스턴스를 처리하는 EntityNotFoundException데 사용하는 주된 이유 getReference()입니다.

EntityManager.find()EntityNotFoundException개체가 발견되지 않으면 던지려는 야망 이 없습니다. 그 행동은 간단하고 명확합니다. 항상로드 된 엔티티 또는 null(엔터티가 발견되지 않은 경우) 반환 되지만 효과적으로로드되지 않을 수있는 프록시 모양의 엔티티는 반환하지 않으므로 놀라지 않을 것입니다.
따라서 EntityManager.find()대부분의 경우에 선호되어야합니다.


수락 된 응답 + Vlad Mihalcea 응답 + Vlad Mihalcea에 대한 내 의견 (이번에는 덜 중요 할 수도 있음)과 비교할 때 귀하의 이유는 오해의 소지가 있습니다.
adrhc

1
Pro JPA2는 "getReference ()를 사용할 수있는 매우 구체적인 상황을 감안할 때 거의 모든 경우에 find ()를 사용해야합니다"라고 말합니다.
JL_SO

이 질문은 수락 된 답변에 대한 필수 보완 사항이고 내 테스트에 따르면 엔터티 프록시의 속성을 설정할 때 수락 된 답변이 말하는 것과 달리 데이터베이스에서 가져 오는 것으로 나타났기 때문에이 질문에 찬성 투표하십시오. Vlad가 언급 한 케이스 만 내 테스트를 통과했습니다.
João Fé

2

나는 선택한 대답에 동의하지 않으며 davidxxx가 올바르게 지적했듯이 getReference는 선택하지 않고 동적 업데이트 동작을 제공하지 않습니다. 이 답변의 유효성에 관한 질문을했습니다. 여기를 참조하십시오- 최대 절전 모드 JPA의 getReference () 후 setter를 사용하여 select를 발행하지 않고 업데이트 할 수 없습니다 .

솔직히 그 기능을 실제로 사용한 사람은 본 적이 없습니다. 어딘가에. 그리고 나는 그것이 왜 그렇게 upvoted인지 이해하지 못합니다.

우선, 하이버 네이트 프록시 객체, setter 또는 getter에서 무엇을 호출하든 SQL이 실행되고 객체가로드됩니다.

하지만 JPA getReference () 프록시가 해당 기능을 제공하지 않으면 어떻게 될까요? 나만의 프록시를 작성할 수 있습니다.

이제 우리는 기본 키에 대한 선택이 쿼리가 얻을 수있는 것만 큼 빠르며 피해야 할 큰 길이로 이동하는 것이 실제로는 아니라고 주장 할 수 있습니다. 그러나 어떤 이유로 든 처리 할 수없는 사람들을 위해 다음은 그러한 프록시의 구현입니다. 그러나 구현을보기 전에 사용법과 사용이 얼마나 간단한 지 확인하십시오.

용법

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

그리고 이것은 다음 쿼리를 실행합니다.

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

삽입하려는 경우에도 PersistenceService.save (new Order ( "a", 2)); 삽입물을 발사합니다.

이행

이것을 pom.xml에 추가하십시오-

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

이 클래스를 만들어 동적 프록시를 만듭니다.

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

모든 방법으로 인터페이스 만들기-

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

이제 프록시에서 이러한 메서드를 구현할 수있는 인터셉터를 만듭니다.

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

그리고 예외 클래스-

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

이 프록시를 사용하여 저장할 서비스-

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

}
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.