web-dev-qa-db-fra.com

Lorsque vous utilisez les méthodes getOne et findOne Spring Data JPA

J'ai un cas d'utilisation où il appelle les éléments suivants:

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

Observez le @Transactional a Propagation.REQUIRES_NEW et le référentiel utilise getOne. Lorsque je lance 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 modifie le getOne(id) par findOne(id) tout ira bien.

BTW, juste avant que le cas d'utilisation 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 audit

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

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

Mes questions sont: 

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

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

Selon l’API getOne:

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

Selon l’API findOne

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

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

4) Quelle est la méthode recommandée?

Merci d'avance.

107
Manuel Jordan

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

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

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

Sans fouiller dans les éléments internes de Spring Data JPA, la différence semble être liée au mécanisme utilisé pour extraire 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 ne fait que déléguer à la mise en œuvre du gestionnaire d'entités JPA.

Cependant, les docs for findOne(ID) ne le mentionnent pas.

L'indice est également dans les noms des référentiels .JpaRepository est spécifique à JPA et peut donc déléguer des appels au responsable d'entité si nécessaire .CrudRepository est agnostique de la technologie de persistance utilisée. Regardez ici . Il est utilisé comme interface de marqueur pour plusieurs technologies de persistance comme JPA, Neo4J etc.

Donc, il n'y a pas vraiment de «différence» entre les deux méthodes pour vos cas d'utilisation, c'est simplement que findOne(ID) est plus générique que la plus spécialisée getOne(ID). Lequel vous utilisez dépend de vous et de votre projet, mais je tiens personnellement à ce que le nom de findOne(ID) rend le code moins spécifique à l'implémentation et ouvre la porte à des choses comme MongoDB, etc. sans trop de refactoring :)

63
Donovan Muller

La différence fondamentale est que getOne est chargé paresseux et que findOne ne 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!!!
}
96
Marek Halmo

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 entité rapide.

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

findOne()/findById() est vraiment plus clair et simple à utiliser que getOne().
Donc, dans la plupart des cas, préférez findOne()/findById() à getOne().


Changement d'API

Du moins, la version 2.0, Spring-Data-Jpa a modifié findOne().
Auparavant, il était défini dans l'interface CrudRepository comme:

T findOne(ID primaryKey);

Maintenant, la méthode unique findOne() que vous trouverez dans CrudRepository est celle qui est définie dans l'interface QueryByExampleExecutor comme étant:

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

C'est finalement implémenté par SimpleJpaRepository, l'implémentation par défaut de l'interface CrudRepository.
Cette méthode est une requête par exemple et vous ne voulez pas que cela 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'interface CrudRepository :

Optional<T> findById(ID id); 

Maintenant, il retourne une Optional. Ce qui n’est pas si grave pour empêcher NullPointerException.

Le choix actuel est donc maintenant entre Optional<T> findById(ID id) et T getOne(ID id)


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

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

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

Lorsque nous examinons l'implémentation, nous pouvons voir qu'il repose sur EntityManager.find() pour effectuer 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() est une méthode EntityManager déclarée comme suit:

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

La récupération d'une entité chargée semble donc attendue. 

2) Alors que le T getOne(ID id) javadoc déclare (l’emphase est à moi):

Renvoie un reference à l'entité avec l'identifiant donné.

En fait, la terminologie référence est vraiment une carte et l'API JPA ne spécifie aucune méthode getOne().
Donc, la meilleure chose à faire pour comprendre ce que le wrapper Spring fait est de regarder dans la mise en œuvre:

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

Ici em.getReference() est une méthode EntityManager déclarée comme:

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

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

Obtient une instance dont l'état peut être récupéré paresseusement. Si le demandé instance n'existe pas dans la base de données, l'entité EntityNotFoundException est levé lorsque l'état de l'instance est accédé pour la première fois. (Le moteur d'exécution de persistance Fournisseur est autorisé à émettre une exception EntityNotFoundException Lorsque getReference est appelée.) L'application ne doit pas s'attendre à ce que l'état d'instance sera disponible lors du détachement, à moins qu'il ne s'agisse de accessible par l’application alors que le gestionnaire d’entités était ouvert.

Ainsi, l'invocation de getOne() peut renvoyer 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 vraiment imprévisible.
Par exemple, si l'objet proxy est sérialisé, vous pouvez obtenir une référence null comme résultat sérialisé ou si une méthode est invoquée sur l'objet proxy, une exception telle que LazyInitializationException est levée.
Donc, dans ce genre de situation, le jet de EntityNotFoundException est la principale raison d'utiliser getOne() pour traiter une instance qui n'existe pas dans la base de données car une situation d'erreur peut ne jamais être exécutée tant 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 alternative, utilisez 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 des entités n'est vraiment pas un détail.

  • pourquoi avez-vous besoin d'introduire getOne() pour envelopper EM.getReference()?
    Pourquoi ne pas simplement s'en tenir à la méthode encapsulée: getReference()? Cette méthode EM est vraiment très particulière alors que getOne() transmet un traitement si simple. 

72
davidxxx

Les méthodes getOne ne renvoient que la référence de la base de données (chargement différé) . Donc, en principe, vous êtes en dehors de la transaction (la Transactional que vous avez déclarée dans la classe de service n'est pas prise en compte) et l'erreur se produit.

14
Bogdan Mata

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

J'ai testé Spring + hibernate + bulldozer + projet 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 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 reste 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 lus par l'utilisateur.

La liste de lecture a toujours été 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

}

0
Tot