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 stanManaged
.Removed
– usunięta,flush
spowoduje usunięcie z bazy.New Transient
– nowa, wymaganypersist
, aby przeszła w stanManaged
.
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!
Dodaj komentarz