fbpx

Nie używaj JpaRepository

Nie używaj JpaRepository

Jak pewnie wiesz, JpaRepository dostarcza bardzo wiele wygodnych, gotowych do użycia operacji, m.in.:

  • findAll() – pobranie wszystkich danych.
  • save(...) – zapis danych.

Co jest ciekawe, to to, że właśnie te dwie operacje to anty-patterny!

Dlaczego findAll jest zły?

Szczerze mówiąc, praktycznie nigdy nie ma przypadku, w którym ta metoda byłaby potrzebna.
Zazwyczaj wyszukujemy jakichś konkretnych danych, potrzebujemy tylko części kolumn, potrzebujemy dane posortowane w jakiś konkretny sposób, czy nawet po prostu chcemy wszystko, ale w mniejszych paczkach, bo przecież nie rzucimy wszystkiego na front w jednej wielkiej liście!

Mając pod ręką taką super wygodną metodę, która zwróci nam wszystko, bardzo często kończy się to tak:

List<String> postTitlesStreamRecords = postRepository.findAll()
    .stream()
    .filter(
        post -> post.getTags()
                    .stream()
                    .map(Tag::getName)
                    .anyMatch(matchingTags::contains)
    )
    .sorted(Comparator.comparing(Post::getId))
    .map(Post::getTitle)
    .collect(Collectors.toList());

Czyli pobieramy wszystko z bazy, a następnie w wygodny sposób, obrabiamy sobie te dane w kodzie. Super, prawda?

No właśnie nie. Pobieramy tutaj ogromne ilości danych, tylko po to, aby wynikowo z tych danych wyciągnąć potrzebne nam informacje, które stanowią tylko mały procent całości.
Poprawnym rozwiązaniem byłoby stworzenie metody, która wykona filtrowanie, sortowanie oraz ograniczenie danych już na poziomie zapytania do bazy.

List<String> findPostTitleByTags(List<String> tags) {
    // ...
}

Jaki problem stwarza save(…)?

Podstawowym problemem jest to, że w specyfikacji JPA nie ma takiej operacji!
W JPA mamy dwie operacje wykonujące zapis:

  • persist – zapisanie nowej lub zarządzanej encji.
  • merge – odświeżenie niezarządzanej encji, po czym wykonanie zapisu.
  • flush – zapis zmian do bazy.

Istotne jest też, w jakim stanie może być encja:

  • Managed – zarządzana, flush zapisze zmiany na niej do bazy.
  • Detached – nie zarządzana, wymaga merge (odświeżenia), aby przeszła w stan Managed.
  • Removed – usunięta, flush spowoduje usunięcie z bazy.
  • New Transient – nowa, wymagany persist, aby przeszła w stan Managed.

Z powyższego, jasno wynika, że operacja merge, jest potrzebna tylko wtedy, kiedy nasz obiekt nie jest zarządzany, czyli albo utworzyliśmy go ręcznie, albo otrzymaliśmy z zewnątrz, spoza sesji bazodanowej.

Nie mniej, w kodzie, zazwyczaj spotkasz takie konstrukcje:

@Transactional
public void saveAntiPattern(Long postId, String postTitle) {         
    Post post = postRepository.findById(postId).orElseThrow(); 
    post.setTitle(postTitle); 
    postRepository.save(post);
}

Pobieramy dane z bazy, modyfikujemy i wołamy save.
W sumie logiczne, niestety, save w JpaRepository jest zaimplementowany tak:

@Transactional
public <S extends T> S save(S entity) {
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

Co sprawia, że w powyższym przypadku, zostanie uruchomiona operacja merge, więc mimo tego, że właśnie wykonaliśmy SELECT na bazie, aby pobrać dane, to podczas próby zapisu, zostanie wykonany kolejny SELECT, aby odświeżyć naszą encję! Wykonujemy dwa razy to samo!

W takim przypadku w ogóle nie powinniśmy wołać metody save, ani merge, ani persist, jedynie flush, który i tak zostanie wywołany automatycznie na zakończenie sesji.

Hypersistance Utils

Jest to biblioteka, wcześniej znana jako Hibernate Types, która oprócz tego, że wprowadza wsparcie dla nie-standardowych typów kolumn (np. JSON, ARRAY, Range, Inet, YearMonth) to wprowadza jeszcze implementacje bazowych repozytoriów: BaseJpaRepository oraz HibernateRepostory.

To, co jest ciekawe w BaseJpaRepository, to to, że nie znajdziesz w nim metod powyższych, problematycznych metod.

Do pobierania danych, domyślnie masz tylko pobieranie po identyfikatorze findById(ID) lub po liście identyfikatorów findAllById(Iterable<ID>). Takie podejście sprawia, że nie ma gotowej metody dla leniwych, jak ktoś potrzebuje, oczywiście może sobie stworzyć, ale w takim przypadku, równie dobrze może stworzyć wyspecjalizowaną metodę, skrojoną do potrzeb.

W kwestii zapisu też próżno szukać metody save. Mamy za to szereg innych opcji:

  • merge, mergeAndFlush itp.
  • persist, persistAndFlush itp.
  • update, updateAndFlush itp.
  • flush

Teraz, aby dokonać zapisu, trzeba mieć większą świadomość tego, co się robi, co oczywiście ma swoje plusy i minusy. Jednak w mojej ocenie, jest to zdecydowanie lepsza droga, bo nawet jak ktoś w zespole nie wie, to jest szansa, że po prostu zapyta innych, albo chociaż się na chwilę zatrzyma i zastanowi.

Poniżej znajdziesz bardzo szczegółowe opisy każdego z przypadków z rozbudowanymi przykładami w kodzie, dla mnie była to bardzo ciekawa lektura i Ciebie też zachęcam do zagłębienia się w temat.


Źródla


Ten artykuł znalazł się w Mailingu EffectiveDev

Wydanie #23

Nie używaj JpaRepository

  • 👉 Nie używaj JpaRepository
    Czyli dlaczego findAll i save to same problemy
  • 👉 Event Store od środka
    Czyli jakie ficzery musi mieć na pokładzie
  • 👉 Apache Pulsar
    Czyli czym się różni od Kafki
  • 👉 22 artykuły, które zmienią Twoją karierę
    Czyli co ciekawego na Twitterze

Dołącz TUTAJ
Co tydzień, podobne artykuły otrzymasz prosto do swojej skrzynki mailowej!

Awatar Łukasz Monkiewicz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *