Lors de l'utilisation des méthodes getOne et findOne Spring Data JPA

154

J'ai un cas d'utilisation où il appelle ce qui suit:

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

Observez le @Transactionalhas Propagation.REQUIRES_NEW et le référentiel utilise getOne . Lorsque j'exécute l'application, je reçois le message d'erreur suivant:

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

Mais si je change le getOne(id)par findOne(id)tout va bien de travaux.

BTW, juste avant que le cas d'utilisation n'appelle la méthode getUserControlById , il a déjà appelé la méthode insertUserControl

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

Les deux méthodes sont Propagation.REQUIRES_NEW car je fais un simple contrôle d' audit .

J'utilise la getOneméthode car elle est définie dans l' interface JpaRepository et mon interface Repository s'étend à partir de là, je travaille avec JPA bien sûr.

L' interface JpaRepository s'étend de CrudRepository . La findOne(id)méthode est définie dans CrudRepository.

Mes questions sont:

  1. Pourquoi échouer la getOne(id)méthode?
  2. Quand dois-je utiliser la getOne(id)méthode?

Je travaille avec d'autres référentiels et tous utilisent la getOne(id)méthode et tout fonctionne bien, uniquement lorsque j'utilise Propagation.REQUIRES_NEW, cela échoue.

Selon l' API getOne :

Renvoie une référence à l'entité avec l'identifiant donné.

Selon l' API findOne :

Récupère une entité par son identifiant.

3) Quand dois-je utiliser la findOne(id)méthode?

4) Quelle méthode est recommandée pour être utilisée?

Merci d'avance.

Manuel Jordan
la source
Vous ne devez surtout pas utiliser getOne () pour tester l'existence d'un objet dans la base de données, car avec getOne vous obtenez toujours un objet! = Null, alors que findOne délivre null.
Uwe Allner

Réponses:

137

TL; DR

T findOne(ID id)(nom dans l'ancienne API) / Optional<T> findById(ID id)(nom dans la nouvelle API) s'appuie sur EntityManager.find()qui effectue un chargement impatient d'entité .

T getOne(ID id)s'appuie sur EntityManager.getReference()qui effectue un chargement paresseux d'entité . Donc, pour assurer le chargement efficace de l'entité, il est nécessaire d'invoquer une méthode dessus.

findOne()/findById()est vraiment plus clair et simple à utiliser que getOne().
Ainsi , dans la plupart des cas très, favoriser findOne()/findById()plus getOne().


Changement d'API

Du moins, la 2.0version, Spring-Data-Jpamodifiée findOne().
Auparavant, il était défini dans l' CrudRepositoryinterface comme:

T findOne(ID primaryKey);

Maintenant, la seule findOne()méthode que vous trouverez dans CrudRepositoryest celle définie dans l' QueryByExampleExecutorinterface comme:

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

Cela est finalement implémenté par SimpleJpaRepositoryl'implémentation par défaut de l' CrudRepositoryinterface.
Cette méthode est une recherche par exemple et vous ne voulez pas qu'elle remplace.

En fait, la méthode ayant le même comportement est toujours présente dans la nouvelle API mais le nom de la méthode a changé.
Il a été renommé de findOne()à findById()dans l' CrudRepositoryinterface:

Optional<T> findById(ID id); 

Maintenant, il renvoie un Optional. Ce qui n'est pas si mal à éviter NullPointerException.

Ainsi, le choix réel se situe désormais entre Optional<T> findById(ID id)et T getOne(ID id).


Deux méthodes distinctes qui reposent sur deux méthodes de récupération JPA EntityManager distinctes

1) Le Optional<T> findById(ID id)javadoc déclare qu'il:

Récupère une entité par son identifiant.

En examinant l'implémentation, nous pouvons voir qu'elle s'appuie sur EntityManager.find()la récupération:

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

Et voici em.find()une EntityManagerméthode déclarée comme:

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

Son javadoc déclare:

Rechercher par clé primaire, en utilisant les propriétés spécifiées

Ainsi, la récupération d'une entité chargée semble attendue.

2) Alors que les T getOne(ID id)javadoc états (souligné dans l' original ):

Renvoie une référence à l'entité avec l'identifiant donné.

En fait, la terminologie de référence est vraiment board et l'API JPA ne spécifie aucune getOne()méthode.
Donc, la meilleure chose à faire pour comprendre ce que fait le wrapper Spring est d'examiner l'implémentation:

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

Voici em.getReference()une EntityManagerméthode déclarée comme:

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

Et heureusement, le EntityManagerjavadoc a mieux défini son intention (c'est moi qui souligne):

Obtenez une instance dont l'état peut être récupéré paresseusement . Si l'instance demandée n'existe pas dans la base de données, l'exception EntityNotFoundException est levée lors du premier accès à l'état de l'instance . (Le moteur d'exécution du fournisseur de persistance est autorisé à lever l'exception EntityNotFoundException lorsque getReference est appelé.) L'application ne doit pas s'attendre à ce que l'état de l'instance soit disponible lors du détachement , à moins qu'il n'ait été accédé par l'application alors que le gestionnaire d'entités était ouvert.

Ainsi, l'invocation getOne()peut retourner une entité extraite paresseusement.
Ici, la récupération paresseuse ne fait pas référence aux relations de l'entité mais à l'entité elle-même.

Cela signifie que si nous appelons getOne()et que le contexte de persistance est fermé, l'entité peut ne jamais être chargée et le résultat est donc vraiment imprévisible.
Par exemple, si l'objet proxy est sérialisé, vous pouvez obtenir une nullréférence en tant que résultat sérialisé ou si une méthode est appelée sur l'objet proxy, une exception telle que LazyInitializationExceptionlevée.
Donc, dans ce genre de situation, le rejet de EntityNotFoundExceptioncela est la principale raison à utiliser getOne()pour gérer une instance qui n'existe pas dans la base de données car une situation d'erreur peut ne jamais être effectuée alors que l'entité n'existe pas.

Dans tous les cas, pour assurer son chargement, vous devez manipuler l'entité pendant que la session est ouverte. Vous pouvez le faire en appelant n'importe quelle méthode sur l'entité.
Ou une meilleure utilisation alternative findById(ID id)au lieu de.


Pourquoi une API si peu claire?

Pour finir, deux questions pour les développeurs Spring-Data-JPA:

  • pourquoi ne pas avoir une documentation plus claire pour getOne()? Le chargement paresseux d'entité n'est vraiment pas un détail.

  • pourquoi avez-vous besoin de présenter getOne()pour envelopper EM.getReference()?
    Pourquoi ne pas simplement coller à la méthode enveloppée: getReference()? Cette méthode EM est vraiment très particulière tout en getOne() véhiculant un traitement si simple.

davidxxx
la source
3
Je ne savais pas pourquoi getOne () ne lançait pas l'exception EntityNotFoundException, mais votre "EntityNotFoundException est lancée lorsque l'état de l'instance est accédé pour la première fois" m'a expliqué le concept. Merci
TheCoder
Résumé de cette réponse: getOne()utilise le chargement paresseux et lance un EntityNotFoundExceptionsi aucun élément n'est trouvé. findById()se charge tout de suite, et renvoie null s'il n'est pas trouvé. Puisqu'il y a des situations imprévisibles avec getOne (), il est recommandé d'utiliser findById () à la place.
Janac Meena le
124

La différence fondamentale est qu'elle getOneest chargée paresseusement et findOnene l'est pas.

Prenons l'exemple suivant:

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
la source
1
n'est pas chargé paresseusement signifie qu'il ne sera chargé que lorsque l'entité sera utilisée? donc je m'attendrais à ce que getEnt soit nul et que le code à l'intérieur du second ne soit pas exécuté. Pourriez-vous s'il vous plaît expliquer. Merci!
Doug
Si enveloppé dans un service Web CompletableFuture <>, j'ai trouvé que vous voudrez utiliser findOne () contre getOne () en raison de son implémentation paresseuse.
Fratt
76

1. Pourquoi la méthode getOne (id) échoue-t-elle?

Consultez cette section dans la documentation . Le remplacement de la transaction déjà en place peut être à l'origine du problème. Cependant, sans plus d'informations, il est difficile de répondre.

2. Quand dois-je utiliser la méthode getOne (id)?

Sans creuser dans les éléments internes de Spring Data JPA, la différence semble résider dans le mécanisme utilisé pour récupérer l'entité.

Si vous regardez le JavaDoc pour getOne(ID)sous Voir aussi :

See Also:
EntityManager.getReference(Class, Object)

il semble que cette méthode délègue simplement à l'implémentation du gestionnaire d'entité JPA.

Cependant, les documents pour findOne(ID)ne le mentionnent pas.

L'indice se trouve également dans les noms des référentiels. JpaRepositoryest spécifique à JPA et peut donc déléguer des appels au gestionnaire d'entités si nécessaire. CrudRepositoryest indépendant de la technologie de persistance utilisée. Regardez ici . Il est utilisé comme interface de marqueur pour plusieurs technologies de persistance telles que JPA, Neo4J, etc.

Il n'y a donc pas vraiment de «différence» entre les deux méthodes pour vos cas d'utilisation, c'est juste que findOne(ID)c'est plus générique que la plus spécialisée getOne(ID). Celui que vous utilisez dépend de vous et de votre projet, mais je m'en tiendrai personnellement au findOne(ID)car cela rend votre code moins spécifique à l'implémentation et ouvre les portes pour passer à des choses comme MongoDB, etc. à l'avenir sans trop de refactorisation :)

Donovan Muller
la source
Merci Donovan, a du sens votre réponse.
Manuel Jordan
20
Je pense qu'il est très trompeur de dire cela there's not really a 'difference' in the two methodsici, car il y a vraiment une grande différence dans la façon dont l'entité est récupérée et ce que vous devez attendre de la méthode. La réponse plus bas de @davidxxx le met très bien en évidence, et je pense que tous ceux qui utilisent Spring Data JPA devraient en être conscients. Sinon, cela peut causer un peu de maux de tête.
fridberg
16

Les getOneméthodes ne retournent que la référence de DB (chargement différé). Donc, fondamentalement, vous êtes en dehors de la transaction (la Transactionalclasse de service que vous avez déclarée n'est pas prise en compte) et l'erreur se produit.

Bogdan Mata
la source
Semble EntityManager.getReference (Class, Object) renvoie "rien" puisque nous sommes dans une nouvelle portée de transaction.
Manuel Jordan
2

Je trouve vraiment très difficile à partir des réponses ci-dessus. Du point de vue du débogage, j'ai presque passé 8 heures à connaître l'erreur stupide.

J'ai testé le projet spring + hibernate + dozer + Mysql. Pour être clair.

J'ai une entité utilisateur, une entité de livre. Vous faites les calculs de cartographie.

Les livres multiples étaient-ils liés à un seul utilisateur. Mais dans UserServiceImpl, j'essayais de le trouver par 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;
}

Le résultat Rest est

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

}

Le code ci-dessus n'a pas récupéré les livres qui sont lus par l'utilisateur, disons.

La bookList était toujours nulle à cause de getOne (ID). Après avoir changé pour findOne (ID). Le résultat est

{
"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
la source
-1

alors que spring.jpa.open-in-view était vrai, je n'ai eu aucun problème avec getOne mais après l'avoir défini sur false, j'ai obtenu LazyInitializationException. Ensuite, le problème a été résolu en le remplaçant par findById.
Bien qu'il existe une autre solution sans remplacer la méthode getOne, et qui est placée @Transactional à la méthode qui appelle repository.getOne (id). De cette façon, la transaction existera et la session ne sera pas fermée dans votre méthode et lors de l'utilisation de entity, il n'y aura pas d'exception LazyInitializationException.

Farshad Falaki
la source
-2

J'ai eu un problème similaire à comprendre pourquoi JpaRespository.getOne (id) ne fonctionne pas et génère une erreur.

Je suis allé changer pour JpaRespository.findById (id) qui vous oblige à retourner un facultatif.

C'est probablement mon premier commentaire sur StackOverflow.

akshaymittal143
la source
Malheureusement, cela ne fournit pas de réponse à la question, ni n'améliore les réponses existantes.
JSTL
Je vois, pas de problème.
akshaymittal143