Quando usar EntityManager.find () vs EntityManager.getReference () com JPA

103

Eu me deparei com uma situação (que acho estranha, mas possivelmente bastante normal) em que uso o EntityManager.getReference (LObj.getClass (), LObj.getId ()) para obter uma entidade de banco de dados e, em seguida, passar o objeto retornado para ser persistido em outra tabela.

Basicamente, o fluxo era assim:

classe TFacade {

  createT (FObj, AObj) {
    T TObj = novo 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
classe FLHFacade {

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

Eu estava recebendo a seguinte exceção "java.lang.IllegalArgumentException: Unknown entity: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Depois de examinar isso por um tempo, finalmente descobri que era porque eu estava usando o método EntityManager.getReference () que estava recebendo a exceção acima, pois o método estava retornando um proxy.

Isso me faz pensar: quando é aconselhável usar o método EntityManager.getReference () em vez do método EntityManager.find () ?

EntityManager.getReference () lança uma EntityNotFoundException se não puder encontrar a entidade que está sendo pesquisada, o que é muito conveniente por si só. O método EntityManager.find () simplesmente retorna nulo se não puder encontrar a entidade.

Com relação aos limites da transação, parece-me que você precisaria usar o método find () antes de passar a entidade recém-encontrada para uma nova transação. Se você usar o método getReference (), provavelmente acabará em uma situação semelhante à minha, com a exceção acima.

SibzTer
fonte
Esqueci de mencionar que estou usando o Hibernate como provedor JPA.
SibzTer

Respostas:

152

Eu geralmente uso o método getReference quando não preciso acessar o estado do banco de dados (quero dizer, o método getter). Apenas para alterar o estado (quero dizer, o método setter). Como você deve saber, getReference retorna um objeto proxy que usa um recurso poderoso chamado verificação suja automática. Suponha o seguinte

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

}

Se eu chamar o método find , o provedor JPA, nos bastidores, chamará

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Se eu chamar o método getReference , o provedor JPA, nos bastidores, chamará

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

E você sabe por quê ???

Ao chamar getReference, você obterá um objeto proxy. Algo como este (o provedor JPA se encarrega de implementar este proxy)

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

}

Portanto, antes da confirmação da transação, o provedor JPA verá o sinalizador stateChanged para atualizar OU NÃO a entidade pessoa. Se nenhuma linha for atualizada após a instrução de atualização, o provedor JPA lançará EntityNotFoundException de acordo com a especificação JPA.

Saudações,

Arthur Ronald
fonte
4
Estou usando o EclipseLink 2.5.0 e as consultas indicadas acima não estão corretas. Sempre emite um SELECTantes UPDATE, não importa qual de find()/ getReference()eu uso. O que é pior, SELECTatravessa relações NON-LAZY (emissão de novo SELECTS), embora eu queira apenas atualizar um único campo em uma entidade.
Dejan Milosevic
1
@Arthur Ronald o que acontece se houver uma anotação de versão na entidade chamada por getReference?
David Hofmann,
Eu tenho o mesmo problema que @DejanMilosevic: ao remover uma entidade obtida via getReference (), um SELECT é emitido nessa entidade e atravessa todas as relações LAZY daquela entidade, emitindo assim muitos SELECTS (com EclipseLink 2.5.0).
Stéphane Appercel
27

Conforme expliquei neste artigo , supondo que você tenha uma Postentidade pai e uma filha, PostCommentconforme ilustrado no diagrama a seguir:

insira a descrição da imagem aqui

Se você ligar findao tentar definir a @ManyToOne postassociação:

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

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

entityManager.persist(comment);

O Hibernate irá executar as seguintes instruções:

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)

A consulta SELECT é inútil desta vez porque não precisamos que a entidade Post seja buscada. Queremos apenas definir a coluna de chave estrangeira post_id subjacente.

Agora, se você usar em getReferencevez disso:

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

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

entityManager.persist(comment);

Desta vez, o Hibernate emitirá apenas a instrução INSERT:

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

Ao contrário find, o getReferenceonly retorna uma entidade Proxy que possui apenas o identificador definido. Se você acessar o Proxy, a instrução SQL associada será disparada enquanto o EntityManager ainda estiver aberto.

No entanto, neste caso, não precisamos acessar o Proxy da entidade. Queremos apenas propagar a chave estrangeira para o registro da tabela subjacente, portanto, carregar um proxy é suficiente para este caso de uso.

Ao carregar um Proxy, você precisa estar ciente de que uma LazyInitializationException pode ser lançada se você tentar acessar a referência de Proxy após o EntityManager ser fechado. Para obter mais detalhes sobre como lidar com o LazyInitializationException, consulte este artigo .

Vlad Mihalcea
fonte
1
Obrigado Vlad por nos deixar saber disso! Mas de acordo com o javadoc, isso parece perturbador: "O tempo de execução do provedor de persistência tem permissão para lançar a EntityNotFoundException quando getReference é chamado". Isso não é possível sem um SELECT (pelo menos para verificar a existência da linha), é? Portanto, um SELECT eventualmente depende da implementação.
adrhc
3
Para o caso de uso que você descreveu, o Hibernate oferece a hibernate.jpa.compliance.proxypropriedade de configuração , para que você possa escolher compatibilidade com JPA ou melhor desempenho de acesso a dados.
Vlad Mihalcea
@VladMihalcea por que getReferenceé necessário se é apenas o suficiente para definir uma nova instância do modelo com o conjunto de PK. o que estou perdendo?
rilaby
Isso só é suportado no Hibernarea e não permitirá que você carregue a associação se for atravessada.
Vlad Mihalcea
8

Como uma referência é 'gerenciada', mas não hidratada, ela também pode permitir que você remova uma entidade por ID, sem precisar carregá-la primeiro na memória.

Como você não pode remover uma entidade não gerenciada, é simplesmente bobagem carregar todos os campos usando find (...) ou createQuery (...), apenas para excluí-la imediatamente.

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

Isso me faz pensar: quando é aconselhável usar o método EntityManager.getReference () em vez do método EntityManager.find ()?

EntityManager.getReference()é realmente um método sujeito a erros e há realmente muito poucos casos em que um código de cliente precisa usá-lo.
Pessoalmente, nunca precisei usá-lo.

EntityManager.getReference () e EntityManager.find (): nenhuma diferença em termos de sobrecarga

Não concordo com a resposta aceita e principalmente:

Se eu chamar o método find , o provedor JPA, nos bastidores, chamará

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Se eu chamar o método getReference , o provedor JPA, nos bastidores, chamará

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Não é o comportamento que obtenho com o Hibernate 5 e o javadoc do getReference()não diz tal coisa:

Obtenha uma instância, cujo estado pode ser buscado lentamente. Se a instância solicitada não existir no banco de dados, a EntityNotFoundException será lançada quando o estado da instância for acessado pela primeira vez. (O tempo de execução do provedor de persistência tem permissão para lançar a EntityNotFoundException quando getReference é chamado.) O aplicativo não deve esperar que o estado da instância estará disponível após a separação, a menos que tenha sido acessado pelo aplicativo enquanto o gerenciador de entidade estava aberto.

EntityManager.getReference() poupa uma consulta para recuperar a entidade em dois casos:

1) se a entidade estiver armazenada no contexto de persistência, esse é o cache de primeiro nível.
E esse comportamento não é específico para EntityManager.getReference(), EntityManager.find() também poupará uma consulta para recuperar a entidade se a entidade estiver armazenada no contexto de persistência.

Você pode verificar o primeiro ponto com qualquer exemplo.
Você também pode contar com a implementação real do Hibernate.
Na verdade, EntityManager.getReference()depende do createProxyIfNecessary()método da org.hibernate.event.internal.DefaultLoadEventListenerclasse para carregar a entidade.
Aqui está sua implementação:

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

A parte interessante é:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Se não manipularmos efetivamente a entidade, ecoando para a busca preguiçosa do javadoc.
Na verdade, para garantir o carregamento efetivo da entidade, é necessário invocar um método nela.
Então o ganho estaria relacionado a um cenário onde queremos carregar uma entidade sem a necessidade de utilizá-la? No quadro de aplicativos, essa necessidade é realmente incomum e, além disso, o getReference()comportamento também é muito enganoso se você ler a próxima parte.

Por que favorecer EntityManager.find () em vez de EntityManager.getReference ()

Em termos de overhead, getReference()não é melhor do find()que o discutido no ponto anterior.
Então, por que usar um ou outro?

A invocação getReference()pode retornar uma entidade obtida lentamente.
Aqui, a busca lenta não se refere aos relacionamentos da entidade, mas à própria entidade.
Isso significa que, se invocarmos getReference()e o contexto de persistência for fechado, a entidade pode nunca ser carregada e, portanto, o resultado é realmente imprevisível. Por exemplo, se o objeto proxy for serializado, você poderá obter uma nullreferência como resultado serializado ou se um método for chamado no objeto proxy, uma exceção como LazyInitializationExceptioné lançada.

Isso significa que o lançamento EntityNotFoundExceptiondisso é o principal motivo a ser usado getReference()para lidar com uma instância que não existe no banco de dados, pois uma situação de erro pode nunca ser executada enquanto a entidade não existir.

EntityManager.find()não tem a ambição de jogar EntityNotFoundExceptionse a entidade não for encontrada. Seu comportamento é simples e claro. Você nunca terá surpresa, pois ele retorna sempre uma entidade carregada ou null(se a entidade não for encontrada) mas nunca uma entidade sob a forma de um proxy que pode não estar efetivamente carregada.
Portanto, EntityManager.find()deve ser favorecido na maioria dos casos.

davidxxx
fonte
Seu motivo é enganoso quando comparado com a resposta aceita + resposta de Vlad Mihalcea + meu comentário para Vlad Mihalcea (talvez um menos importante neste último +).
adrhc
1
O Pro JPA2 afirma: "Dada a situação muito específica em que getReference () pode ser usado, find () deve ser usado em praticamente todos os casos".
JL_SO
Vote nesta questão porque é um complemento necessário para a resposta aceita e porque meus testes mostraram que, ao definir uma propriedade do proxy da entidade, ela é buscada no banco de dados, ao contrário do que diz a resposta aceita. Apenas o caso declarado por Vlad passou em meus testes.
João Fé
2

Não concordo com a resposta selecionada e, como davidxxx corretamente apontou, getReference não fornece tal comportamento de atualizações dinâmicas sem select. Eu fiz uma pergunta sobre a validade desta resposta, veja aqui - não é possível atualizar sem emitir select on using setter após getReference () do Hibernate JPA .

Sinceramente, não vi ninguém que realmente usou essa funcionalidade. QUALQUER LUGAR. E eu não entendo por que é tão votado.

Agora, em primeiro lugar, não importa o que você chame em um objeto proxy de hibernação, um setter ou um getter, um SQL é disparado e o objeto é carregado.

Mas então pensei, e se o proxy getReference () JPA não fornecer essa funcionalidade. Posso apenas escrever meu próprio proxy.

Agora, todos nós podemos argumentar que as seleções em chaves primárias são tão rápidas quanto uma consulta pode chegar e não é algo que deva ser evitado. Mas para aqueles de nós que não podem lidar com isso por um motivo ou outro, abaixo está uma implementação de tal proxy. Mas antes de ver a implementação, veja seu uso e como é simples de usar.

USO

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

E isso dispararia a seguinte consulta -

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

e mesmo se você quiser inserir, você ainda pode fazer PersistenceService.save (new Order ("a", 2)); e dispararia uma inserção como deveria.

IMPLEMENTAÇÃO

Adicione isso ao seu pom.xml -

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

Faça esta classe para criar proxy dinâmico -

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

Faça uma interface com todos os métodos -

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

Agora, faça um interceptor que permitirá que você implemente esses métodos em seu proxy -

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

E a classe de exceção -

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

Um serviço para salvar usando este proxy -

@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
fonte