Cuando use los métodos getOne y findOne Spring Data JPA

154

Tengo un caso de uso en el que se llama lo siguiente:

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl getUserControlById(Integer id){
    return this.userControlRepository.getOne(id);
}

Observe que @Transactionaltiene Propagation.REQUIRES_NEW y el repositorio usa getOne . Cuando ejecuto la aplicación, recibo el siguiente mensaje de error:

Exception in thread "main" org.hibernate.LazyInitializationException: 
could not initialize proxy - no Session
...

Pero si cambio el getOne(id)por findOne(id)todo funciona bien.

Por cierto, justo antes de que el caso de uso llame al método getUserControlById , ya ha llamado al método insertUserControl

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl insertUserControl(UserControl userControl) {
    return this.userControlRepository.save(userControl);
}

Ambos métodos son Propagation.REQUIRES_NEW porque estoy haciendo un control de auditoría simple .

Uso el getOnemétodo porque está definido en la interfaz JpaRepository y mi interfaz Repository se extiende desde allí, por supuesto, estoy trabajando con JPA.

La interfaz JpaRepository se extiende desde CrudRepository . El findOne(id)método se define en CrudRepository.

Mis preguntas son:

  1. ¿Por qué fallar el getOne(id)método?
  2. ¿Cuándo debo usar el getOne(id)método?

Estoy trabajando con otros repositorios y todos usan el getOne(id)método y todo funciona bien, solo cuando uso Propagation.REQUIRES_NEW falla.

De acuerdo con la API getOne :

Devuelve una referencia a la entidad con el identificador dado.

De acuerdo con la API findOne :

Recupera una entidad por su id.

3) ¿Cuándo debo usar el findOne(id)método?

4) ¿Qué método se recomienda usar?

Gracias por adelantado.

Manuel Jordan
fuente
Especialmente no debes usar getOne () para probar la existencia de un objeto en la base de datos, porque con getOne siempre obtienes un objeto! = Null, mientras que findOne entrega null.
Uwe Allner

Respuestas:

137

TL; DR

T findOne(ID id)(nombre en la antigua API) / Optional<T> findById(ID id)(nombre en la nueva API) se basa en EntityManager.find()que realiza una carga ansiosa de la entidad .

T getOne(ID id)se basa en EntityManager.getReference()que realiza una entidad de carga diferida . Por lo tanto, para garantizar la carga efectiva de la entidad, se requiere invocar un método.

findOne()/findById()es realmente más claro y fácil de usar que getOne().
Así que en el mismo la mayor parte de los casos, favorecen findOne()/findById()más getOne().


Cambio de API

De al menos, la 2.0versión, Spring-Data-Jpamodificada findOne().
Anteriormente, se definía en la CrudRepositoryinterfaz como:

T findOne(ID primaryKey);

Ahora, el único findOne()método que encontrará CrudRepositoryes el que se define en la QueryByExampleExecutorinterfaz como:

<S extends T> Optional<S> findOne(Example<S> example);

Eso se implementa finalmente mediante SimpleJpaRepositoryla implementación predeterminada de la CrudRepositoryinterfaz.
Este método es una búsqueda por ejemplo de búsqueda y no desea hacerlo como reemplazo.

De hecho, el método con el mismo comportamiento todavía está allí en la nueva API pero el nombre del método ha cambiado.
Fue renombrado de findOne()a findById()en la CrudRepositoryinterfaz:

Optional<T> findById(ID id); 

Ahora devuelve un Optional. Lo cual no es tan malo de prevenir NullPointerException.

Entonces, la elección real es ahora entre Optional<T> findById(ID id)y T getOne(ID id).


Dos métodos distintos que se basan en dos métodos distintos de recuperación de JPA EntityManager

1) El Optional<T> findById(ID id)javadoc afirma que:

Recupera una entidad por su id.

A medida que analizamos la implementación, podemos ver que depende de EntityManager.find()la recuperación:

public Optional<T> findById(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);

    Class<T> domainType = getDomainClass();

    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }

    LockModeType type = metadata.getLockModeType();

    Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();

    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

Y aquí em.find()hay un EntityManagermétodo declarado como:

public <T> T find(Class<T> entityClass, Object primaryKey,
                  Map<String, Object> properties);

Sus estados javadoc:

Buscar por clave principal, utilizando las propiedades especificadas

Entonces, recuperar una entidad cargada parece esperado.

2) Mientras que los estados T getOne(ID id)javadoc (el énfasis es mío):

Devuelve una referencia a la entidad con el identificador dado.

De hecho, la terminología de referencia es realmente la placa y JPA API no especifica ningún getOne()método.
Entonces, lo mejor que puede hacer para comprender lo que hace el contenedor de Spring es investigar la implementación:

@Override
public T getOne(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

Aquí em.getReference()hay un EntityManagermétodo declarado como:

public <T> T getReference(Class<T> entityClass,
                              Object primaryKey);

Y afortunadamente, el EntityManagerjavadoc definió mejor su intención (el énfasis es mío):

Obtenga una instancia, cuyo estado se pueda obtener perezosamente . Si la instancia solicitada no existe en la base de datos, se emite la EntityNotFoundException cuando se accede por primera vez al estado de la instancia . (Se permite que el tiempo de ejecución del proveedor de persistencia arroje la EntityNotFoundException cuando se llama a getReference). La aplicación no debe esperar que el estado de la instancia esté disponible en la separación , a menos que la aplicación haya accedido mientras el administrador de la entidad estaba abierto.

Por lo tanto, invocar getOne()puede devolver una entidad perezosamente obtenida.
Aquí, la recuperación perezosa no se refiere a las relaciones de la entidad, sino a la entidad misma.

Significa que si invocamos getOne()y luego se cierra el contexto de persistencia, la entidad nunca se cargará y, por lo tanto, el resultado es realmente impredecible.
Por ejemplo, si el objeto proxy se serializa, puede obtener una nullreferencia como resultado serializado o si se invoca un método en el objeto proxy, se LazyInitializationExceptionproduce una excepción como la que se produce.
Entonces, en este tipo de situación, el lanzamiento de EntityNotFoundExceptioneso es la razón principal para usar getOne()para manejar una instancia que no existe en la base de datos, ya que una situación de error nunca se puede realizar mientras la entidad no existe.

En cualquier caso, para garantizar su carga, debe manipular la entidad mientras se abre la sesión. Puede hacerlo invocando cualquier método en la entidad.
O un mejor uso alternativo en findById(ID id)lugar de.


¿Por qué una API tan poco clara?

Para finalizar, dos preguntas para los desarrolladores de Spring-Data-JPA:

  • ¿Por qué no tener una documentación más clara getOne()? La carga diferida de la entidad realmente no es un detalle.

  • ¿Por qué necesitas presentar getOne()para envolver EM.getReference()?
    ¿Por qué no simplemente apegarse al método envuelto getReference():? Este método EM es realmente muy particular mientras getOne() transmite un procesamiento tan simple.

davidxxx
fuente
3
Estaba confundido por qué getOne () no arroja la EntityNotFoundException, pero su "EntityNotFoundException se lanza cuando se accede por primera vez al estado de instancia" me explicó el concepto. Gracias
TheCoder
Resumen de esta respuesta: getOne()utiliza la carga diferida y arroja un EntityNotFoundExceptionelemento si no se encuentra ningún elemento. findById()se carga de inmediato y devuelve nulo si no se encuentra. Como hay algunas situaciones impredecibles con getOne (), se recomienda usar findById () en su lugar.
Janac Meena
124

La diferencia básica es que getOnetiene carga lenta y findOneno.

Considere el siguiente ejemplo:

public static String NON_EXISTING_ID = -1;
...
MyEntity getEnt = myEntityRepository.getOne(NON_EXISTING_ID);
MyEntity findEnt = myEntityRepository.findOne(NON_EXISTING_ID);

if(findEnt != null) {
     findEnt.getText(); // findEnt is null - this code is not executed
}

if(getEnt != null) {
     getEnt.getText(); // Throws exception - no data found, BUT getEnt is not null!!!
}
Marek Halmo
fuente
1
¿no se carga lentamente significa que solo se cargará cuando la entidad se vaya a utilizar? así que esperaría que getEnt sea nulo y el código dentro del segundo si no se ejecuta ¿Podría explicarlo? ¡Gracias!
Doug
Si está envuelto dentro de un servicio web CompletableFuture <>, he descubierto que querrá usar findOne () vs. getOne () debido a su implementación diferida.
Fratt
76

1. ¿Por qué falla el método getOne (id)?

Vea esta sección en los documentos . Si anula la transacción ya establecida, puede estar causando el problema. Sin embargo, sin más información, esta es difícil de responder.

2. ¿Cuándo debo usar el método getOne (id)?

Sin profundizar en lo interno de Spring Data JPA, la diferencia parece estar en el mecanismo utilizado para recuperar la entidad.

Si mira JavaDoc por getOne(ID)debajo de Vea también :

See Also:
EntityManager.getReference(Class, Object)

parece que este método simplemente delega a la implementación del administrador de la entidad JPA.

Sin embargo, los documentos para findOne(ID)no mencionan esto.

La pista también está en los nombres de los repositorios. JpaRepositoryes específico de JPA y, por lo tanto, puede delegar llamadas al administrador de la entidad si es necesario. CrudRepositoryEs agnóstico de la tecnología de persistencia utilizada. Mira aquí . Se utiliza como interfaz de marcador para múltiples tecnologías de persistencia como JPA, Neo4J , etc.

Entonces, no hay realmente una 'diferencia' en los dos métodos para sus casos de uso, es solo que findOne(ID)es más genérico que el más especializado getOne(ID). El que use depende de usted y de su proyecto, pero personalmente me apegaría a findOne(ID)él, ya que hace que su código sea menos específico de implementación y abre las puertas para pasar a cosas como MongoDB, etc. en el futuro sin demasiada refactorización :)

Donovan Muller
fuente
Gracias Donovan, tiene sentido su respuesta.
Manuel Jordan
20
Creo que es muy engañoso decir eso there's not really a 'difference' in the two methodsaquí, porque realmente hay una gran diferencia en cómo se recupera la entidad y qué debería esperar que regrese el método. La respuesta más abajo de @davidxxx resalta esto muy bien, y creo que todos los que usan Spring Data JPA deberían ser conscientes de esto. De lo contrario, puede causar bastante dolor de cabeza.
fridberg
16

Los getOnemétodos devuelven solo la referencia de DB (carga diferida). Así que básicamente estás fuera de la transacción (elTransactional no se considera que ha sido declarado en la clase de servicio) y se produce el error.

Bogdan Mata
fuente
Parece que EntityManager.getReference (clase, objeto) devuelve "nada" ya que estamos en un nuevo ámbito de transacción.
Manuel Jordan
2

Realmente me parece muy difícil de las respuestas anteriores. Desde la perspectiva de la depuración, casi pasé 8 horas para conocer el error tonto.

Tengo pruebas de primavera + hibernación + dozer + proyecto Mysql. Para ser claro.

Tengo entidad de usuario, entidad de libro. Haces los cálculos de mapeo.

¿Se vincularon los libros múltiples a un usuario? Pero en UserServiceImpl estaba tratando de encontrarlo mediante getOne (userId);

public UserDTO getById(int userId) throws Exception {

    final User user = userDao.getOne(userId);

    if (user == null) {
        throw new ServiceException("User not found", HttpStatus.NOT_FOUND);
    }
    userDto = mapEntityToDto.transformBO(user, UserDTO.class);

    return userDto;
}

El resultado de descanso es

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 1,
        "name": "TEST_ME",
        "bookList": null
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

El código anterior no obtuvo los libros que el usuario lee, digamos.

La lista de libros siempre fue nula debido a getOne (ID). Después de cambiar a findOne (ID). El resultado es

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 0,
        "name": "Annama",
        "bookList": [
            {
                "id": 2,
                "book_no": "The karma of searching",
            }
        ]
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

EngineSense
fuente
-1

Si bien spring.jpa.open-in-view era cierto, no tuve ningún problema con getOne, pero después de establecerlo en falso, obtuve LazyInitializationException. Entonces el problema se resolvió reemplazando con findById.
Aunque hay otra solución sin reemplazar el método getOne, y es poner @Transactional en el método que llama a repository.getOne (id). De esta manera, la transacción existirá y la sesión no se cerrará en su método y, al usar la entidad, no habrá ninguna LazyInitializationException.

Farshad Falaki
fuente
-2

Tuve un problema similar al entender por qué JpaRespository.getOne (id) no funciona y arroja un error.

Fui y cambié a JpaRespository.findById (id) que requiere que devuelvas un Opcional.

Este es probablemente mi primer comentario en StackOverflow.

akshaymittal143
fuente
Desafortunadamente, esto no proporciona y responde a la pregunta, ni mejora las respuestas existentes.
JSTL
Ya veo, no hay problema.
akshaymittal143