W przypadku korzystania z metod getOne i findOne Spring Data JPA

154

Mam przypadek użycia, w którym wywołuje to:

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

Zauważ, że @Transactionalma Propagation.REQUIRES_NEW i repozytorium używa getOne . Po uruchomieniu aplikacji otrzymuję następujący komunikat o błędzie:

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

Ale jeśli zmienić getOne(id)przez findOne(id)wszystkich utworów grzywny.

BTW, tuż przed wywołaniem metody getUserControlById przez przypadek użycia już wywołał metodę insertUserControl

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

Obie metody to Propagation.REQUIRES_NEW, ponieważ wykonuję prostą kontrolę audytu .

Używam tej getOnemetody, ponieważ jest zdefiniowana w interfejsie JpaRepository , a mój interfejs Repozytorium się stamtąd rozciąga, oczywiście pracuję z JPA.

JpaRepository interfejs rozciąga się od CrudRepository . findOne(id)Sposób jest zdefiniowany w CrudRepository.

Moje pytania to:

  1. Dlaczego zawiodłaś getOne(id)metoda?
  2. Kiedy należy stosować tę getOne(id)metodę?

Pracuję z innymi repozytoriami i wszystkie używam tej getOne(id)metody i wszystko działa dobrze, tylko wtedy, gdy używam Propagation.REQUIRES_NEW, nie udaje się.

Zgodnie z getOne API:

Zwraca odwołanie do jednostki o podanym identyfikatorze.

Zgodnie z findOne API:

Pobiera jednostkę na podstawie jej identyfikatora.

3) Kiedy należy stosować findOne(id)metodę?

4) Jaka metoda jest zalecana?

Z góry dziękuję.

Manuel Jordan
źródło
Szczególnie nie powinieneś używać getOne () do testowania istnienia obiektu w bazie danych, ponieważ z getOne zawsze otrzymujesz obiekt! = Null, podczas gdy findOne dostarcza null.
Uwe Allner

Odpowiedzi:

137

TL; DR

T findOne(ID id)(nazwa w starym API) / Optional<T> findById(ID id)(nazwa w nowym API) polega na EntityManager.find()tym, że wykonuje żądne ładowanie jednostki .

T getOne(ID id)polega na EntityManager.getReference()tym, że wykonuje leniwe ładowanie jednostki . Aby zapewnić efektywne ładowanie jednostki, wymagane jest wywołanie metody.

findOne()/findById()jest naprawdę bardziej przejrzysty i prostszy w użyciu niż getOne().
Tak więc w większości przypadków bardzo sprzyjają findOne()/findById()ponad getOne().


Zmiana interfejsu API

Od przynajmniej 2.0wersji Spring-Data-Jpazmodyfikowanej findOne().
Wcześniej był zdefiniowany w CrudRepositoryinterfejsie jako:

T findOne(ID primaryKey);

Teraz jedyną findOne()metodą, którą znajdziesz, CrudRepositoryjest ta zdefiniowana w QueryByExampleExecutorinterfejsie jako:

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

Jest to ostatecznie realizowane przez SimpleJpaRepositorydomyślną implementację CrudRepositoryinterfejsu.
Ta metoda jest zapytaniem przez przykładowe wyszukiwanie i nie chcesz tego jako zamiennika.

W rzeczywistości metoda o tym samym zachowaniu jest nadal dostępna w nowym interfejsie API, ale nazwa metody uległa zmianie.
Jego nazwa została zmieniona od findOne()celu findById()w CrudRepositoryinterfejsie:

Optional<T> findById(ID id); 

Teraz zwraca Optional. Którym nie jest tak źle zapobiegać NullPointerException.

Tak więc rzeczywisty wybór jest teraz między Optional<T> findById(ID id)a T getOne(ID id).


Dwie różne metody, które opierają się na dwóch różnych metodach pobierania JPA EntityManager

1) Optional<T> findById(ID id)Javadoc stwierdza, że:

Pobiera jednostkę na podstawie jej identyfikatora.

Kiedy przyglądamy się implementacji, widzimy, że polega ona na EntityManager.find()pobieraniu:

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

I tu em.find()jest EntityManagermetoda zadeklarowana jako:

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

Jego javadoc stwierdza:

Znajdź według klucza podstawowego, używając określonych właściwości

Zatem pobranie załadowanej jednostki wydaje się oczekiwane.

2) Podczas gdy T getOne(ID id)javadoc stwierdza (podkreślenie moje):

Zwraca odwołanie do jednostki o podanym identyfikatorze.

W rzeczywistości terminologia referencyjna to tak naprawdę płyta, a API JPA nie określa żadnej getOne()metody.
Dlatego najlepszą rzeczą, jaką można zrobić, aby zrozumieć, co robi opakowanie Spring, jest sprawdzenie implementacji:

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

Tutaj em.getReference()jest EntityManagermetoda zadeklarowana jako:

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

I na szczęście EntityManagerjavadoc lepiej zdefiniował swój zamiar (podkreślenie moje):

Uzyskaj instancję, której stan można leniwie pobrać . Jeśli żądane wystąpienie nie istnieje w bazie danych, EntityNotFoundException jest generowany podczas pierwszego dostępu do stanu wystąpienia . (Środowisko wykonawcze dostawcy trwałości może zgłosić wyjątek EntityNotFoundException po wywołaniu metody getReference) . Aplikacja nie powinna oczekiwać, że stan wystąpienia będzie dostępny po odłączeniu , chyba że aplikacja uzyskała do niego dostęp, gdy menedżer jednostek był otwarty.

Zatem wywołanie getOne()może zwrócić leniwie pobraną jednostkę.
Tutaj leniwe pobieranie nie odnosi się do relacji jednostki, ale do samej jednostki.

Oznacza to, że jeśli wywołamy, getOne()a następnie kontekst Persistence zostanie zamknięty, jednostka może nigdy nie zostać załadowana, a więc wynik jest naprawdę nieprzewidywalny.
Na przykład, jeśli obiekt proxy jest serializowany, możesz uzyskać nullodwołanie jako wynik zserializowany lub jeśli metoda jest wywoływana dla obiektu proxy, zostanie zgłoszony wyjątek, taki jak LazyInitializationException.
Tak więc w tego rodzaju sytuacji wyrzucenie EntityNotFoundExceptiontego jest głównym powodem użycia getOne()do obsługi wystąpienia, które nie istnieje w bazie danych, ponieważ sytuacja błędu może nigdy nie zostać wykonana, gdy jednostka nie istnieje.

W każdym razie, aby zapewnić jego załadowanie, musisz manipulować bytem podczas otwierania sesji. Możesz to zrobić, wywołując dowolną metodę na encji.
Lub lepsze alternatywne zastosowanie findById(ID id)zamiast.


Dlaczego tak niejasny interfejs API?

Na koniec dwa pytania do programistów Spring-Data-JPA:

  • dlaczego nie mieć bardziej przejrzystej dokumentacji getOne()? Leniwe ładowanie jednostki tak naprawdę nie jest szczegółem.

  • dlaczego musisz wprowadzić getOne()do wrapa EM.getReference()?
    Dlaczego nie po prostu trzymać się zawiniętego metody: getReference()? Ta metoda EM jest naprawdę bardzo szczególna, podczas gdy getOne() przenosi tak proste przetwarzanie.

davidxxx
źródło
3
Byłem zdezorientowany, dlaczego getOne () nie zgłasza wyjątku EntityNotFoundException, ale Twój „wyjątek EntityNotFoundException jest wyrzucany przy pierwszym dostępie do stanu wystąpienia” wyjaśnił mi koncepcję. Dzięki
TheCoder
Podsumowanie tej odpowiedzi: getOne()używa leniwego ładowania i zgłasza, EntityNotFoundExceptionjeśli żaden element nie zostanie znaleziony. findById()ładuje się od razu i zwraca wartość null, jeśli nie zostanie znaleziona. Ponieważ w przypadku metody getOne () występują nieprzewidywalne sytuacje, zamiast tego zaleca się użycie funkcji findById ().
Janac Meena
124

Podstawowa różnica polega na tym, że getOneładuje się leniwie i findOnenie jest.

Rozważmy następujący przykład:

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
źródło
1
czy nie jest ładowany leniwie oznacza, że ​​będzie ładowany tylko wtedy, gdy jednostka będzie używana? więc spodziewałbym się, że getEnt będzie zerowe, a kod wewnątrz sekundy, jeśli nie zostanie wykonany. Czy mógłbyś wyjaśnić. Dzięki!
Doug
Jeśli jest opakowany w usługę sieciową CompletableFuture <>, stwierdziłem, że będziesz chciał użyć findOne () zamiast getOne () z powodu jego leniwej implementacji.
Fratt
76

1. Dlaczego metoda getOne (id) zawodzi?

Zobacz tę sekcję w dokumentacji . Zastąpienie już istniejącej transakcji może być przyczyną problemu. Jednak bez dodatkowych informacji trudno odpowiedzieć na to pytanie.

2. Kiedy należy używać metody getOne (id)?

Bez zagłębiania się w wewnętrzne elementy Spring Data JPA wydaje się, że różnica polega na mechanizmie używanym do pobierania jednostki.

Jeśli spojrzysz na JavaDoc w getOne(ID)sekcji Zobacz też :

See Also:
EntityManager.getReference(Class, Object)

wydaje się, że ta metoda po prostu oddelegowuje do realizacji menedżera podmiotu WZP.

Jednak docs dla findOne(ID)nie wspomnieć o tym.

Wskazówka jest również w nazwach repozytoriów. JpaRepositoryjest specyficzny dla WZP i dlatego może w razie potrzeby delegować połączenia do zarządzającego podmiotem. CrudRepositoryjest agnostykiem stosowanej technologii trwałości. Spójrz tutaj . Jest używany jako interfejs znacznika dla wielu technologii trwałości, takich jak JPA, Neo4J itp.

Więc tak naprawdę nie ma „różnicy” w tych dwóch metodach dla twoich przypadków użycia, po prostu findOne(ID)jest to bardziej ogólne niż bardziej wyspecjalizowane getOne(ID). To, którego użyjesz, zależy od ciebie i twojego projektu, ale osobiście trzymałbym się tego, findOne(ID)ponieważ sprawia, że ​​twój kod jest mniej specyficzny dla implementacji i otwiera drzwi do przejścia do rzeczy takich jak MongoDB itp. W przyszłości bez zbytniego refaktoryzacji :)

Donovan Muller
źródło
Dziękuję Donovan, wyczuł twoją odpowiedź.
Manuel Jordan
20
Myślę, że stwierdzenie tego w tym there's not really a 'difference' in the two methodsmiejscu jest bardzo mylące , ponieważ naprawdę istnieje duża różnica w sposobie pobierania jednostki i oczekiwaniu, że metoda zwróci. Odpowiedź zamieszczona dalej przez @davidxxx bardzo dobrze to podkreśla i myślę, że każdy, kto używa Spring Data JPA, powinien o tym wiedzieć. W przeciwnym razie może powodować spory ból głowy.
Fridberg
16

Te getOnemetody powroty odniesienie z DB (leniwa załadunku). Więc w zasadzie jesteś poza transakcją (to Transactional, że zostałeś zadeklarowany w klasie usług nie jest brane pod uwagę) i występuje błąd.

Bogdan Mata
źródło
Wydaje się, że EntityManager.getReference (klasa, obiekt) zwraca „nic”, ponieważ znajdujemy się w nowym zakresie transakcji.
Manuel Jordan
2

Powyższe odpowiedzi są dla mnie bardzo trudne. Z punktu widzenia debugowania spędziłem prawie 8 godzin, aby poznać głupi błąd.

Mam testowanie wiosna + hibernacja + spycharka + projekt MySQL. Żeby było jasne.

Mam podmiot użytkownika, podmiot książki. Wykonujesz obliczenia mapowania.

Czy wiele książek było powiązanych z jednym użytkownikiem? Ale w UserServiceImpl próbowałem go znaleźć przez 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;
}

Wynik Reszty to

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

}

Powyższy kod nie pobrał książek, które czytają powiedzmy użytkownicy.

Lista bookList była zawsze pusta z powodu getOne (ID). Po zmianie na findOne (ID). Wynik to

{
"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
źródło
-1

podczas gdy spring.jpa.open-in-view było prawdziwe, nie miałem żadnego problemu z getOne, ale po ustawieniu go na false otrzymałem LazyInitializationException. Następnie problem został rozwiązany poprzez zastąpienie go findById.
Chociaż istnieje inne rozwiązanie bez zastępowania metody getOne, a mianowicie metoda @Transactional, która wywołuje repository.getOne (id). W ten sposób transakcja będzie istniała, a sesja nie zostanie zamknięta w Twojej metodzie, a podczas korzystania z encji nie będzie żadnego LazyInitializationException.

Farshad Falaki
źródło
-2

Miałem podobny problem ze zrozumieniem, dlaczego JpaRespository.getOne (id) nie działa i generuje błąd.

Poszedłem i zmieniłem na JpaRespository.findById (id), który wymaga zwrócenia opcjonalnego.

To chyba mój pierwszy komentarz na temat StackOverflow.

akshaymittal143
źródło
Niestety nie daje to odpowiedzi na pytanie ani nie poprawia dotychczasowych odpowiedzi.
JSTL,
Rozumiem, nie ma problemu.
akshaymittal143