Cuándo usar EntityManager.find () vs EntityManager.getReference () con JPA

103

Me he encontrado con una situación (que creo que es extraña pero posiblemente bastante normal) en la que uso EntityManager.getReference (LObj.getClass (), LObj.getId ()) para obtener una entidad de base de datos y luego pasar el objeto devuelto a persistir en otra mesa.

Entonces, básicamente, el flujo fue así:

class TFacade {

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

Recibía la siguiente excepción "java.lang.IllegalArgumentException: Entidad desconocida: com.my.persistence.L $$ EnhancerByCGLIB $$ 3e7987d0"

Después de mirarlo por un tiempo, finalmente descubrí que era porque estaba usando el método EntityManager.getReference () que estaba obteniendo la excepción anterior ya que el método estaba devolviendo un proxy.

Esto me hace preguntarme, ¿ cuándo es aconsejable utilizar el método EntityManager.getReference () en lugar del método EntityManager.find () ?

EntityManager.getReference () arroja una EntityNotFoundException si no puede encontrar la entidad que se busca, lo cual es muy conveniente en sí mismo. El método EntityManager.find () simplemente devuelve null si no puede encontrar la entidad.

Con respecto a los límites de la transacción, me parece que necesitaría usar el método find () antes de pasar la entidad recién encontrada a una nueva transacción. Si usa el método getReference (), probablemente terminaría en una situación similar a la mía con la excepción anterior.

SibzTer
fuente
Olvidé mencionar que estoy usando Hibernate como proveedor de JPA.
SibzTer

Respuestas:

152

Normalmente uso el método getReference cuando no necesito acceder al estado de la base de datos (me refiero al método getter). Solo para cambiar de estado (me refiero al método del setter). Como debe saber, getReference devuelve un objeto proxy que usa una característica poderosa llamada verificación automática sucia. Suponga lo siguiente

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

}

Si llamo al método de búsqueda , el proveedor de JPA, detrás de escena, llamará

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Si llamo al método getReference , el proveedor de JPA, detrás de escena, llamará

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Y usted sabe por qué ???

Cuando llame a getReference, obtendrá un objeto proxy. Algo como este (el proveedor de JPA se encarga 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;
    }

}

Entonces, antes de confirmar la transacción, el proveedor de JPA verá el indicador stateChanged para actualizar O NO la entidad de la persona. Si no se actualiza ninguna fila después de la declaración de actualización, el proveedor de JPA lanzará EntityNotFoundException de acuerdo con la especificación de JPA.

Saludos,

Arthur Ronald
fuente
4
Estoy usando EclipseLink 2.5.0 y las consultas indicadas anteriormente no son correctas. Siempre emite un SELECTantes UPDATE, sin importar cuál de find()/ getReference()yo use. Lo que es peor, SELECTatraviesa relaciones NON-LAZY (emitiendo nuevas SELECTS), aunque solo quiero actualizar un solo campo en una entidad.
Dejan Milosevic
1
@Arthur Ronald, ¿qué sucede si hay una anotación de versión en la entidad llamada por getReference?
David Hofmann
Tengo el mismo problema que @DejanMilosevic: al eliminar una entidad obtenida a través de getReference (), se emite un SELECT en esa entidad y atraviesa todas las relaciones LAZY de esa entidad, emitiendo así muchos SELECTS (con EclipseLink 2.5.0).
Stéphane Appercel
27

Como expliqué en este artículo , asumiendo que tiene una Postentidad padre y un hijo PostCommentcomo se ilustra en el siguiente diagrama:

ingrese la descripción de la imagen aquí

Si llama findcuando intenta establecer la @ManyToOne postasociación:

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

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

entityManager.persist(comment);

Hibernate ejecutará las siguientes declaraciones:

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)

La consulta SELECT es inútil esta vez porque no necesitamos que se obtenga la entidad Post. Solo queremos establecer la columna de clave externa post_id subyacente.

Ahora, si usa getReferenceen su lugar:

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

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

entityManager.persist(comment);

Esta vez, Hibernate emitirá solo la declaración INSERT:

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

A diferencia de find, el getReferenceúnico devuelve un proxy de entidad que solo tiene el identificador establecido. Si accede al Proxy, la instrucción SQL asociada se activará siempre que EntityManager esté todavía abierto.

Sin embargo, en este caso, no necesitamos acceder a la entidad Proxy. Solo queremos propagar la clave externa al registro de la tabla subyacente, por lo que cargar un proxy es suficiente para este caso de uso.

Al cargar un Proxy, debe tener en cuenta que se puede lanzar una LazyInitializationException si intenta acceder a la referencia de Proxy después de cerrar EntityManager. Para obtener más detalles sobre el manejo de LazyInitializationException, consulte este artículo .

Vlad Mihalcea
fuente
1
¡Gracias Vlad por dejarnos saber esto! Pero según javadoc esto parece perturbador: "El tiempo de ejecución del proveedor de persistencia puede lanzar la EntityNotFoundException cuando se llama a getReference". Esto no es posible sin un SELECT (al menos para verificar la existencia de la fila), ¿verdad? Entonces, un SELECT eventualmente depende de la implementación.
adrhc
3
Para el caso de uso que describió, Hibernate ofrece la hibernate.jpa.compliance.proxypropiedad de configuración , por lo que puede elegir el cumplimiento de JPA o un mejor rendimiento de acceso a datos.
Vlad Mihalcea
@VladMihalcea por qué getReferencees necesario si es suficiente para establecer una nueva instancia de modelo con el conjunto de PK. ¿Qué me estoy perdiendo?
Rilaby
Esto solo es compatible con Hibernarea y no le permitirá cargar la asociación si se atraviesa.
Vlad Mihalcea
8

Debido a que una referencia está 'administrada', pero no hidratada, también puede permitirle eliminar una entidad por ID, sin necesidad de cargarla primero en la memoria.

Como no puede eliminar una entidad no administrada, es simplemente una tontería cargar todos los campos usando find (...) o createQuery (...), solo para eliminarlo inmediatamente.

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

Esto me hace preguntarme, ¿cuándo es aconsejable utilizar el método EntityManager.getReference () en lugar del método EntityManager.find ()?

EntityManager.getReference()es realmente un método propenso a errores y realmente hay muy pocos casos en los que un código de cliente necesite usarlo.
Personalmente, nunca necesité usarlo.

EntityManager.getReference () y EntityManager.find (): no hay diferencia en términos de gastos generales

No estoy de acuerdo con la respuesta aceptada y en particular:

Si llamo al método de búsqueda , el proveedor de JPA, detrás de escena, llamará

SELECT NAME, AGE FROM PERSON WHERE PERSON_ID = ?

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

Si llamo al método getReference , el proveedor de JPA, detrás de escena, llamará

UPDATE PERSON SET AGE = ? WHERE PERSON_ID = ?

No es el comportamiento que obtengo con Hibernate 5 y el javadoc de getReference()no dice tal cosa:

Obtenga una instancia, cuyo estado se puede obtener de forma perezosa. Si la instancia solicitada no existe en la base de datos, se lanza EntityNotFoundException cuando se accede por primera vez al estado de la instancia. (El tiempo de ejecución del proveedor de persistencia puede lanzar EntityNotFoundException cuando se llama a getReference). La aplicación no debe esperar que el estado de la instancia esté disponible al desconectarse, a menos que la aplicación haya accedido a él mientras el administrador de la entidad estaba abierto.

EntityManager.getReference() reserva una consulta para recuperar la entidad en dos casos:

1) si la entidad está almacenada en el contexto de persistencia, esa es la caché de primer nivel.
Y este comportamiento no es específico de EntityManager.getReference(), EntityManager.find()también ahorrará una consulta para recuperar la entidad si la entidad está almacenada en el contexto de persistencia.

Puede comprobar el primer punto con cualquier ejemplo.
También puede confiar en la implementación real de Hibernate.
De hecho, se EntityManager.getReference()basa en el createProxyIfNecessary()método de la org.hibernate.event.internal.DefaultLoadEventListenerclase para cargar la entidad.
Aquí está su implementació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;
}

La parte interesante es:

Object existing = persistenceContext.getEntity( keyToLoad );

2) Si no manipulamos eficazmente la entidad, haciéndonos eco de la búsqueda perezosa del javadoc.
De hecho, para garantizar la carga efectiva de la entidad, se requiere invocar un método en ella.
Entonces, ¿la ganancia estaría relacionada con un escenario en el que queremos cargar una entidad sin tener la necesidad de usarla? En el marco de las aplicaciones, esta necesidad es realmente poco común y además el getReference()comportamiento también es muy engañoso si lees la siguiente parte.

Por qué favorecer EntityManager.find () sobre EntityManager.getReference ()

En cuanto a gastos generales, getReference()no es mejor que find()lo comentado en el punto anterior.
Entonces, ¿por qué usar uno u otro?

La invocación getReference()puede devolver una entidad obtenida de forma perezosa.
Aquí, la búsqueda diferida no se refiere a las relaciones de la entidad, sino a la entidad misma.
Significa que si invocamos getReference()y luego se cierra el contexto de persistencia, es posible que la entidad nunca se cargue y, por lo tanto, el resultado es realmente impredecible. Por ejemplo, si el objeto proxy está serializado, puede obtener una nullreferencia como resultado serializado o si se invoca un método en el objeto proxy, LazyInitializationExceptionse lanza una excepción como .

Significa que el lanzamiento de EntityNotFoundExceptioneso es la razón principal a utilizar getReference()para manejar una instancia que no existe en la base de datos, ya que es posible que nunca se realice una situación de error mientras la entidad no exista.

EntityManager.find()no tiene la ambición de lanzar EntityNotFoundExceptionsi no se encuentra la entidad. Su comportamiento es simple y claro. Nunca te sorprenderás ya que devuelve siempre una entidad cargada o null(si no se encuentra la entidad) pero nunca una entidad bajo la forma de un proxy que puede no estar efectivamente cargada.
Por tanto, EntityManager.find()debería favorecerse en la mayoría de los casos.

davidxxx
fuente
Su razón es engañosa en comparación con la respuesta aceptada + la respuesta de Vlad Mihalcea + mi comentario a Vlad Mihalcea (tal vez menos importante este último +).
adrhc
1
Pro JPA2 afirma: "Dada la situación muy específica en la que se puede usar getReference (), find () debería usarse en prácticamente todos los casos".
JL_SO
Voto esta pregunta porque es un complemento necesario para la respuesta aceptada y porque mis pruebas mostraron que, al establecer una propiedad del proxy de la entidad, se obtiene de la base de datos, al contrario de lo que dice la respuesta aceptada. Solo el caso declarado por Vlad pasó mis pruebas.
João Fé
2

No estoy de acuerdo con la respuesta seleccionada y, como Davidxxx señaló correctamente, getReference no proporciona tal comportamiento de actualizaciones dinámicas sin select. Hice una pregunta sobre la validez de esta respuesta, vea aquí: no se puede actualizar sin emitir select sobre el uso de setter después de getReference () de hibernate JPA .

Honestamente, no he visto a nadie que haya usado esa funcionalidad. EN CUALQUIER SITIO. Y no entiendo por qué está tan votada.

Ahora, en primer lugar, no importa lo que llame a un objeto proxy de hibernación, un setter o un getter, se dispara un SQL y se carga el objeto.

Pero luego pensé, ¿qué pasa si el proxy JPA getReference () no proporciona esa funcionalidad? Puedo escribir mi propio proxy.

Ahora, todos podemos argumentar que las selecciones en claves primarias son tan rápidas como puede ser una consulta y no es algo que deba evitarse mucho. Pero para aquellos de nosotros que no podemos manejarlo por una razón u otra, a continuación se muestra una implementación de dicho proxy. Pero antes de que vea la implementación, vea su uso y lo simple que es de usar.

USO

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

Y esto dispararía la siguiente consulta:

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

e incluso si desea insertar, aún puede hacer PersistenceService.save (new Order ("a", 2)); y dispararía un inserto como debería.

IMPLEMENTACIÓN

Agregue esto a su pom.xml -

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

Haga esta clase para crear un 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());
    }
}

Crea una interfaz con todos los métodos:

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

Ahora, cree un interceptor que le permitirá implementar estos métodos en su 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!");
}
}

Y la clase de excepción -

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

Un servicio para ahorrar 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
fuente