Quando usar os métodos getOne e findOne, Spring Data JPA

154

Eu tenho um caso de uso em que ele chama o seguinte:

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

Observe o @Transactionalhas Propagation.REQUIRES_NEW e o repositório usa getOne . Quando executo o aplicativo, recebo a seguinte mensagem de erro:

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

Mas se eu mudar o getOne(id)por findOne(id)tudo funciona bem.

BTW, logo antes do caso de uso chamar o método getUserControlById , ele já chamou o método insertUserControl

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

Ambos os métodos são Propagation.REQUIRES_NEW porque estou executando um controle de auditoria simples .

Eu uso o getOnemétodo porque ele é definido na interface JpaRepository e minha interface Repository se estende a partir daí, é claro que estou trabalhando com o JPA.

A interface JpaRepository se estende a partir de CrudRepository . O findOne(id)método é definido em CrudRepository.

Minhas perguntas são:

  1. Por que falhar no getOne(id)método?
  2. Quando devo usar o getOne(id)método?

Estou trabalhando com outros repositórios e todos usam o getOne(id)método e todos funcionam bem, somente quando eu uso o Propagation.REQUIRES_NEW ele falha.

De acordo com a API getOne :

Retorna uma referência para a entidade com o identificador fornecido.

De acordo com a API findOne :

Recupera uma entidade pelo seu ID.

3) Quando devo usar o findOne(id)método?

4) Qual método é recomendado para ser usado?

Desde já, obrigado.

Manuel Jordan
fonte
Você não deve usar o getOne () especialmente para testar a existência de um objeto no banco de dados, porque com o getOne você sempre obtém um objeto! = Null, enquanto o findOne fornece null.
precisa saber é o seguinte

Respostas:

137

TL; DR

T findOne(ID id)(nome na API antiga) / Optional<T> findById(ID id)(nome na nova API) conta com EntityManager.find()a execução de um carregamento ansioso da entidade .

T getOne(ID id)depende EntityManager.getReference()que executa um carregamento lento da entidade . Portanto, para garantir o carregamento efetivo da entidade, é necessário invocar um método.

findOne()/findById()é realmente mais claro e simples de usar do que getOne().
Portanto, mesmo no mais dos casos, favorecem findOne()/findById()mais getOne().


Alteração de API

Pelo menos, a 2.0versão, Spring-Data-Jpamodificada findOne().
Anteriormente, era definido na CrudRepositoryinterface como:

T findOne(ID primaryKey);

Agora, o findOne()método único em que você encontrará CrudRepositoryé aquele definido na QueryByExampleExecutorinterface como:

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

Finalmente SimpleJpaRepository, isso é implementado pela implementação padrão da CrudRepositoryinterface.
Este método é uma consulta por exemplo e você não deseja substituí-lo.

De fato, o método com o mesmo comportamento ainda está lá na nova API, mas o nome do método foi alterado.
Foi renomeado de findOne()para findById()na CrudRepositoryinterface:

Optional<T> findById(ID id); 

Agora ele retorna um Optional. O que não é tão ruim de prevenir NullPointerException.

Portanto, a escolha real é agora entre Optional<T> findById(ID id)e T getOne(ID id).


Dois métodos distintos que se baseiam em dois métodos distintos de recuperação do JPA EntityManager

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

Recupera uma entidade pelo seu ID.

À medida que analisamos a implementação, podemos ver que ela depende EntityManager.find()da recuperação:

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

E aqui em.find()está um EntityManagermétodo declarado como:

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

Seu javadoc declara:

Localizar por chave primária, usando as propriedades especificadas

Portanto, a recuperação de uma entidade carregada parece esperada.

2) Enquanto o T getOne(ID id)javadoc declara (a ênfase é minha):

Retorna uma referência para a entidade com o identificador fornecido.

De fato, a referência terminologia de é realmente válida e a API JPA não especifica nenhum getOne()método.
Portanto, a melhor coisa a fazer para entender o que o wrapper Spring faz é examinar a implementação:

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

Aqui em.getReference() está um EntityManagermétodo declarado como:

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

Felizmente, o EntityManagerjavadoc definiu melhor sua intenção (a ênfase é minha):

Obtenha uma instância, cujo estado pode ser buscado preguiçosamente . 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 EntityNotFoundException quando getReference é chamado.) O aplicativo não deve esperar que o estado da instância esteja disponível no momento do desanexamento , a menos que tenha sido acessado pelo aplicativo enquanto o gerenciador de entidades estava aberto.

Então, invocando getOne() pode retornar uma entidade buscada preguiçosamente.
Aqui, a busca preguiçosa não se refere aos relacionamentos da entidade, mas à própria entidade.

Isso significa que, se invocarmos getOne()e o contexto de persistência for fechado, a entidade poderá nunca ser carregada e, portanto, o resultado será 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 a LazyInitializationExceptionlançada.
Portanto, nesse tipo de situação, o lance EntityNotFoundExceptiondisso é o principal motivo para getOne()lidar com uma instância que não existe no banco de dados, pois uma situação de erro nunca pode ser executada enquanto a entidade não existe.

De qualquer forma, para garantir seu carregamento, você precisa manipular a entidade enquanto a sessão é aberta. Você pode fazer isso invocando qualquer método na entidade.
Ou um uso alternativo melhorfindById(ID id) vez de.


Por que uma API tão obscura?

Para finalizar, duas perguntas para os desenvolvedores Spring-Data-JPA:

  • por que não ter uma documentação mais clara getOne()? O carregamento lento da entidade não é realmente um detalhe.

  • por que você precisa apresentar getOne()para embrulhar EM.getReference()?
    Por que não seguir simplesmente o método embrulhado getReference():? Esse método EM é realmente muito particular enquanto getOne() transmite um processamento tão simples.

davidxxx
fonte
3
Fiquei confuso por que getOne () não está lançando a EntityNotFoundException, mas sua "a EntityNotFoundException é lançada quando o estado da instância é acessado pela primeira vez" me explicou o conceito. Obrigado
TheCoder
Resumo desta resposta: getOne()usa carregamento lento e lança um EntityNotFoundExceptionse nenhum item for encontrado. findById()carrega imediatamente e retorna nulo se não for encontrado. Como existem situações imprevisíveis com getOne (), é recomendável usar findById ().
Janac Meena 16/04
124

A diferença básica é que getOneé carregado preguiçosamente efindOne não é.

Considere o seguinte exemplo:

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
fonte
1
não carregado preguiçosamente significa que ele será carregado apenas quando a entidade for usada? então eu esperaria que getEnt fosse nulo e o código dentro do segundo, se não for executado, você poderia explicar. Obrigado!
Doug
Se envolvido dentro de um serviço da web CompletableFuture <>, descobri que você desejará usar findOne () vs. getOne () por causa de sua implementação lenta.
Fratt
76

1. Por que o método getOne (id) falha?

Veja esta seção nos documentos . A substituição da transação já em vigor pode estar causando o problema. No entanto, sem mais informações, este é difícil de responder.

2. Quando devo usar o método getOne (id)?

Sem se aprofundar nas informações internas da Spring Data JPA, a diferença parece estar no mecanismo usado para recuperar a entidade.

Se você olhar para o JavaDoc para getOne(ID)sob Consulte também :

See Also:
EntityManager.getReference(Class, Object)

parece que esse método apenas delega para a implementação do gerente de entidade da JPA.

No entanto, os documentos para findOne(ID)não mencionam isso.

A pista também está nos nomes dos repositórios. JpaRepositoryé específico da JPA e, portanto, pode delegar chamadas ao gerente da entidade, se necessário. CrudRepositoryé independente da tecnologia de persistência usada. Olha aqui . É usado como uma interface de marcador para várias tecnologias de persistência como JPA, Neo4J etc.

Portanto, não há realmente uma 'diferença' nos dois métodos para seus casos de uso, é apenas findOne(ID)mais genérico que o mais especializado getOne(ID). Qual você usa depende de você e do seu projeto, mas eu pessoalmente me ateria ao findOne(ID), pois torna seu código menos específico da implementação e abre as portas para passar a coisas como MongoDB etc. no futuro sem muita refatoração :)

Donovan Muller
fonte
Obrigado Donovan, tem sentido a sua resposta.
Manuel Jordan
20
Eu acho que é muito enganador dizer isso there's not really a 'difference' in the two methodsaqui, porque realmente há uma grande diferença em como a entidade é recuperada e o que você deve esperar que o método retorne. A resposta mais adiante por @davidxxx destaca isso muito bem, e acho que todos que usam o Spring Data JPA devem estar cientes disso. Caso contrário, pode causar um pouco de dor de cabeça.
Fridberg
16

Os getOnemétodos retornam apenas a referência do DB (carregamento lento). Então, basicamente, você está fora da transação (o que Transactionalvocê declarou na classe de serviço não é considerado) e o erro ocorre.

Bogdan Mata
fonte
Parece que EntityManager.getReference (Class, Object) retorna "nada", pois estamos em um novo escopo de transação.
Manuel Jordan
2

Eu realmente acho muito difícil pelas respostas acima. Do ponto de vista da depuração, eu quase passei 8 horas para conhecer o erro bobo.

Eu tenho o projeto spring + hibernate + dozer + Mysql. Para ser claro.

Eu tenho entidade de usuário, entidade de livro. Você faz os cálculos do mapeamento.

Os vários livros estavam vinculados a um usuário. Mas em UserServiceImpl eu estava tentando encontrá-lo por 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;
}

O resultado restante é

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

}

O código acima não buscou os livros que são lidos pelo usuário, digamos.

O bookList sempre foi nulo por causa de getOne (ID). Depois de alterar para findOne (ID). O resultado é

{
"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
fonte
-1

Embora spring.jpa.open-view fosse verdadeiro, não tive nenhum problema com o getOne, mas depois de defini-lo como false, obtive LazyInitializationException. Em seguida, o problema foi resolvido substituindo-o por findById.
Embora exista outra solução sem substituir o método getOne, e que seja colocado @Transactional no método que está chamando repository.getOne (id). Dessa maneira, a transação existe e a sessão não será fechada no seu método e, enquanto estiver usando a entidade, não haverá LazyInitializationException.

Farshad Falaki
fonte
-2

Eu tive um problema semelhante ao entender por que o JpaRespository.getOne (id) não funciona e gera um erro.

Fui e mudei para JpaRespository.findById (id), que requer que você retorne um opcional.

Este é provavelmente o meu primeiro comentário no StackOverflow.

akshaymittal143
fonte
Infelizmente, isso não fornece e responde à pergunta, nem melhora as respostas existentes.
JSTL
Entendo, não há problema.
precisa saber é o seguinte