Quando si utilizzano i metodi getOne e findOne Spring Data JPA


154

Ho un caso d'uso in cui si chiama quanto segue:

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

Osservare @Transactionalhas has Propagation.REQUIRES_NEW e il repository utilizza getOne . Quando eseguo l'app, ricevo il seguente messaggio di errore:

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

Ma Se cambio il getOne(id)da findOne(id)tutto funziona benissimo.

A proposito, poco prima che il caso d'uso chiama il metodo getUserControlById , ha già chiamato il metodo insertUserControl

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

Entrambi i metodi sono Propagation.REQUIRES_NEW perché sto eseguendo un semplice controllo di controllo.

Uso il getOnemetodo perché è definito nell'interfaccia JpaRepository e la mia interfaccia Repository si estende da lì, ovviamente sto lavorando con JPA.

L' interfaccia JpaRepository si estende da CrudRepository . Il findOne(id)metodo è definito in CrudRepository.

Le mie domande sono:

  1. Perché fallire il getOne(id)metodo?
  2. Quando dovrei usare il getOne(id)metodo?

Sto lavorando con altri repository e tutti usano il getOne(id)metodo e tutti funzionano bene, solo quando uso Propagation.REQUIRES_NEW non riesce.

Secondo l' API getOne :

Restituisce un riferimento all'entità con l'identificatore specificato.

Secondo l' API findOne :

Recupera un'entità in base al suo ID.

3) Quando dovrei usare il findOne(id)metodo?

4) Quale metodo si consiglia di utilizzare?

Grazie in anticipo.


In particolare non dovresti usare getOne () per testare l'esistenza di un oggetto nel database, perché con getOne ottieni sempre un oggetto! = Null, mentre findOne consegna null.
Uwe Allner,

Risposte:


137

TL; DR

T findOne(ID id)(nome nella vecchia API) / Optional<T> findById(ID id)(nome nella nuova API) si basa sul fatto EntityManager.find()che esegue un caricamento desideroso di entità .

T getOne(ID id)fa affidamento sul fatto EntityManager.getReference()che esegue un caricamento lento dell'entità . Quindi, per garantire l'effettivo caricamento dell'entità, è necessario invocare un metodo su di essa.

findOne()/findById()è davvero più chiaro e semplice da usare rispetto a getOne().
Così nella maggior parte dei casi molto, favorire findOne()/findById()sopra getOne().


Cambio API

Almeno, la 2.0versione, Spring-Data-Jpamodificata findOne().
In precedenza, era definito CrudRepositorynell'interfaccia come:

T findOne(ID primaryKey);

Ora, il singolo findOne()metodo in cui troverai CrudRepositoryè quello definito QueryByExampleExecutornell'interfaccia come:

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

Questo è finalmente implementato dall'implementazione SimpleJpaRepositorypredefinita CrudRepositorydell'interfaccia.
Questo metodo è una query di ricerca di esempio e non si desidera sostituirlo.

In effetti, il nuovo metodo con lo stesso comportamento è ancora presente nella nuova API ma il nome del metodo è cambiato.
E 'stato ribattezzato dalla findOne()a findById()alla CrudRepositoryinterfaccia di:

Optional<T> findById(ID id); 

Ora restituisce un Optional. Che non è così male da prevenire NullPointerException.

Quindi, la scelta effettiva è ora tra Optional<T> findById(ID id)e T getOne(ID id).


Due metodi distinti che si basano su due distinti metodi di recupero di EntityManager JPA

1) Il Optional<T> findById(ID id)javadoc afferma che:

Recupera un'entità in base al suo ID.

Mentre esaminiamo l'implementazione, possiamo vedere che si affida EntityManager.find()per fare il recupero:

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

Ed ecco em.find()un EntityManagermetodo dichiarato come:

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

Il suo javadoc afferma:

Trova per chiave primaria, usando le proprietà specificate

Quindi, il recupero di un'entità caricata sembra previsto.

2) Mentre il T getOne(ID id)javadoc afferma (l'enfasi è mia):

Restituisce un riferimento all'entità con l'identificatore specificato.

In effetti, la terminologia di riferimento è davvero la scheda e l'API JPA non specifica alcun getOne()metodo.
Quindi la cosa migliore da fare per capire cosa fa il wrapper Spring è esaminare l'implementazione:

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

Ecco em.getReference()un EntityManagermetodo dichiarato come:

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

E per fortuna, il EntityManagerjavadoc ha definito meglio la sua intenzione (l'enfasi è mia):

Ottieni un'istanza, il cui stato può essere recuperato pigramente . Se l'istanza richiesta non esiste nel database, EntityNotFoundException viene generata al primo accesso allo stato dell'istanza . (Il runtime del provider di persistenza è autorizzato a lanciare EntityNotFoundException quando viene chiamato getReference.) L'applicazione non dovrebbe aspettarsi che lo stato dell'istanza sia disponibile al momento del distacco , a meno che non sia stato accessibile dall'applicazione mentre il gestore entità era aperto.

Quindi, invocare getOne()può restituire un'entità pigramente recuperata.
Qui, il recupero pigro non si riferisce alle relazioni dell'entità ma all'entità stessa.

Significa che se invochiamo getOne()e quindi il contesto di Persistenza viene chiuso, l'entità potrebbe non essere mai caricata e quindi il risultato è davvero imprevedibile.
Ad esempio, se l'oggetto proxy è serializzato, è possibile ottenere un nullriferimento come risultato serializzato o se un metodo viene invocato sull'oggetto proxy, LazyInitializationExceptionviene generata un'eccezione come .
Quindi, in questo tipo di situazione, la EntityNotFoundExceptioncausa principale è quella di utilizzare getOne()un'istanza che non esiste nel database poiché una situazione di errore non può mai essere eseguita mentre l'entità non esiste.

In ogni caso, per assicurarne il caricamento devi manipolare l'entità mentre la sessione è aperta. Puoi farlo invocando qualsiasi metodo sull'entità.
O un uso alternativo migliore findById(ID id)invece di.


Perché un'API così poco chiara?

Per finire, due domande per gli sviluppatori Spring-Data-JPA:

  • perché non avere una documentazione più chiara per getOne()? Il caricamento lento dell'entità non è in realtà un dettaglio.

  • perché è necessario presentare getOne()per avvolgere EM.getReference()?
    Perché non è sufficiente attenersi al metodo avvolto: getReference()? Questo metodo EM è davvero molto particolare mentre getOne() trasmette un'elaborazione così semplice.


3
Ero confuso perché getOne () non sta lanciando EntityNotFoundException, ma il tuo "EntityNotFoundException viene lanciato quando si accede allo stato dell'istanza per la prima volta" mi ha spiegato il concetto. Grazie
TheCoder

Riepilogo di questa risposta: getOne()utilizza il caricamento lento e genera un messaggio EntityNotFoundExceptionse non viene trovato alcun elemento. findById()viene caricato immediatamente e restituisce null se non trovato. Poiché ci sono alcune situazioni imprevedibili con getOne (), si consiglia di utilizzare findById ().
Janac Meena,

124

La differenza di base è che getOneè pigro e findOnenon lo è.

Considera il seguente esempio:

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!!!
}

1
Lazy Load non significa che verrà caricato solo quando l'entità verrà utilizzata? quindi mi aspetterei che getEnt sia nullo e il codice all'interno del secondo se non venga eseguito Potresti spiegare. Grazie!
Doug,

Se racchiuso in un servizio web CompletableFuture <> ho scoperto che vorrai usare findOne () vs. getOne () a causa della sua implementazione pigra.
Fratt,

76

1. Perché il metodo getOne (id) fallisce?

Vedi questa sezione nei documenti . La sostituzione della transazione già in atto potrebbe causare il problema. Tuttavia, senza ulteriori informazioni, è difficile rispondere a questa domanda.

2. Quando dovrei usare il metodo getOne (id)?

Senza scavare negli interni di Spring Data JPA, la differenza sembra essere nel meccanismo utilizzato per recuperare l'entità.

Se si guarda JavaDoc per getOne(ID)sotto Vedi anche :

See Also:
EntityManager.getReference(Class, Object)

sembra che questo metodo deleghi solo l'implementazione del gestore entità JPA.

Tuttavia, i documenti per findOne(ID)non menzionarlo.

L'indizio è anche nei nomi dei repository. JpaRepositoryè specifico dell'APP e pertanto può delegare le chiamate al gestore dell'entità, se necessario. CrudRepositoryè indipendente dalla tecnologia di persistenza utilizzata. Guardate qui . È usato come interfaccia marker per più tecnologie di persistenza come JPA, Neo4J ecc.

Quindi non c'è davvero una 'differenza' nei due metodi per i tuoi casi d'uso, è solo che findOne(ID)è più generico di quello più specializzato getOne(ID). Quale utilizzi dipende da te e dal tuo progetto, ma mi atterrei personalmente al fatto findOne(ID)che rende il tuo codice meno specifico per l'implementazione e apre le porte per passare a cose come MongoDB ecc. In futuro senza troppo refactoring :)


Grazie Donovan, ha senso la tua risposta.
Manuel Jordan,

20
Penso che sia molto fuorviante dirlo there's not really a 'difference' in the two methodsqui, perché c'è davvero una grande differenza nel modo in cui l'entità viene recuperata e in cosa dovresti aspettarti che il metodo restituisca. La risposta più in basso di @davidxxx lo evidenzia molto bene, e penso che tutti coloro che usano l'APP di Spring Data dovrebbero esserne consapevoli. Altrimenti, può causare un bel po 'di mal di testa.
fridberg,

16

Il getOnemetodo restituisce solo il riferimento dal DB (caricamento lento). Quindi, fondamentalmente, sei al di fuori della transazione ( Transactionalnon sei stato preso in considerazione nella dichiarazione in classe di servizio) e si verifica l'errore.


Sembra EntityManager.getReference (Class, Object) non restituisce "nulla" poiché siamo in un nuovo ambito Transaction.
Manuel Jordan,

2

Trovo davvero molto difficile dalle risposte sopra. Dal punto di vista del debug ho quasi trascorso 8 ore a conoscere l'errore sciocco.

Ho test di primavera + ibernazione + bulldozer + progetto Mysql. Per essere chiari.

Ho Entità utente, Entità libro. Fai i calcoli della mappatura.

I libri multipli erano legati a un utente. Ma in UserServiceImpl stavo cercando di trovarlo con 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;
}

Il risultato Rest è

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

}

Il codice sopra riportato non ha recuperato i libri letti dall'utente, ad esempio.

L'elenco libri era sempre nullo a causa di getOne (ID). Dopo aver cambiato in findOne (ID). Il risultato è

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

}


-1

mentre spring.jpa.open-in-view era vero, non ho avuto problemi con getOne ma dopo averlo impostato su false, ho ottenuto LazyInitializationException. Quindi il problema è stato risolto sostituendo con findById.
Sebbene ci sia un'altra soluzione senza sostituire il metodo getOne, e questo è messo @Transactional nel metodo che chiama repository.getOne (id). In questo modo la transazione esisterà e la sessione non verrà chiusa nel tuo metodo e durante l'utilizzo dell'entità non ci sarebbe alcuna LazyInitializationException.


-2

Ho avuto un problema simile nel capire perché JpaRespository.getOne (id) non funziona e genera un errore.

Sono andato e sono passato a JpaRespository.findById (id) che richiede di restituire un Opzionale.

Questo è probabilmente il mio primo commento su StackOverflow.


Sfortunatamente, questo non fornisce e non risponde alla domanda, né migliora le risposte esistenti.
JSTL,

Capisco, nessun problema.
akshaymittal143,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.