Niedawno opublikowałem taką oto publikację na LinkedIn. Dotyczy ona sposobu wrapowania listy encji pobieranej z repozytorium Spring Data. Kod wygląda następująco.
Ten pomysł zrodził mi się w głowie po zapoznaniu się z zasadami zawartymi w Object Calisthenics, a dokładniej jedną z nich - First Class Collections. Korzysta się z tego naprawdę przyjemnie, jednak ma to pewną wadę, o której się niedawno dowiedziałem (pozdrowienia dla Kamila 👋).
Problem wydajnościowy
Załóżmy taki scenariusz - z powodów biznesowych musimy w dwóch miejscach pobrać kolekcję encji używając tego samego id agregującego. Oczywiście wszystko dzieje się w ramach jednego przypadku biznesowego. Dodatkowo nie chcemy, aby ta lista była przekazywana przez wiele warstw jako parametr metod.
Spójrzmy jak to może wyglądać w kodzie. Wyjątkowo będzie on dzisiaj napisany w Kotlinie z racji tego, że chciałbym z nim poobcować. 😜
Tak prezentuje się ten trywialny kod. Oczywiście już nie opakowywałem zbioru Inmate w klasę, bo to nie ma tutaj znaczenia (nawet nie wiem czy można korzystać ze słowa kluczowego default w interfejsach w Kotlinie). Napiszmy teraz kod, który będzie nam potrzebny, aby sprawdzić wcześniej opisaną sytuację.
Hibernate:
/\* <criteria> \*/ select
i1_0.id,
i1_0.house_id,
i1_0.name,
i1_0.role
from
inmate i1_0
where
i1_0.house_id=?
2024-05-17T12:21:08.572+02:00 DEBUG 39328 ---[jpa-list-optimization] [ main] p.c.jpalistoptimization.HouseService : first size 4
Hibernate:
/\* <criteria> \*/ select
i1_0.id,
i1_0.house_id,
i1_0.name,
i1_0.role
from
inmate i1_0
where
i1_0.house_id=?
2024-05-17T12:21:08.573+02:00 DEBUG 39328 ---[jpa-list-optimization] [ main] p.c.jpalistoptimization.HouseService : second size 4
Robimy dwa strzały do bazy danych, aby pobrać listę mieszkańców. Cache Hibernate nie zadziałał. Jak możemy sobie z tym poradzić? Oto jedno z możliwych rozwiązań.
Możemy stworzyć encję, która odnosi się do tej samej tabeli co pierwotna encja. W jej ciele umieszczamy tylko zbiór encji, które chcemy wyciągać razem.
@ComponentclassHouseService(valhouseRepository:HouseRepository,valinmateRepository:InmateRepository,valhouseInmatesRepository:HouseInmatesRepository,valtransactionTemplate:TransactionTemplate){vallog=LoggerFactory.getLogger(HouseService::class.java)funcall(){valhouseId=transactionTemplate.execute{log.debug("saving house")valhouse=houseRepository.save(House("Pogodny domek"))log.debug("saving inmates")inmateRepository.save(Inmate("Tadzik",house.id,"Tata"))inmateRepository.save(Inmate("Grażyna",house.id,"Mama"))inmateRepository.save(Inmate("Albert",house.id,"Syn"))inmateRepository.save(Inmate("Roksana",house.id,"Córka"))return@executehouse.id}transactionTemplate.execute{log.debug("fetching inmates by house id with aggregation")valthird=houseId?.let{houseInmatesRepository.findById(it)}?:Optional.empty()third.ifPresentOrElse({log.debug("third size {}",it.size())},{log.debug("third size is 0")})valfourth=houseId?.let{houseInmatesRepository.findById(it)}?:Optional.empty()fourth.ifPresentOrElse({log.debug("fourth size {}",it.size())},{log.debug("fourth size is 0")})}}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Hibernate:
select
hi1_0.id,
i1_0.house_id,
i1_0.id,
i1_0.name,
i1_0.role
from
house hi1_0
left join
inmate i1_0
on hi1_0.id=i1_0.house_id
where
hi1_0.id=?
2024-05-17T12:21:08.592+02:00 DEBUG 39328 ---[jpa-list-optimization] [ main] p.c.jpalistoptimization.HouseService : third size 4
2024-05-17T12:21:08.593+02:00 DEBUG 39328 ---[jpa-list-optimization] [ main] p.c.jpalistoptimization.HouseService : fourth size 4
Mamy już tylko jeden strzał. Spory uzysk jak na tak małą ilość pracy.
Podsumowanie
Pewnie takie podejście nie każdemu się spodoba. To prawda, nie jest eleganckie, bo mamy dwie encje działające na jeden tabelce. Ale działa i poprawia driver architektoniczny jakim jest wydajność. Jeśli faktycznie zaczniemy cierpieć z powodu zbyt wielu uderzeń do bazy danych to może warto rozważyć takie rozwiązanie. Oczywiście zdaje sobie sprawę, że to nie jest jedyny sposób poprawy wydajności. Jeśli masz jakiś inny pomysł to podziel się nim w komentarzu.