W poprzednim poście opisałem klasyczny problem wydajnościowy
N+1 Select Problem występujący podczas korzystania z Java Persistence API, wraz z kilkoma podejściami do zabezpieczenia się przed jego powstawaniem.
W komentarzach pojawił się pewien wątek, który chciałbym teraz rozwinąć...
Zastanowimy się nad racjonalnym wykorzystaniem narzędzia jakim jest maper relacyjno-obiektowy. Jak to zwykle z racjonalnym myśleniem bywa - niesie ono ze sobą zwykle dodatkowe skutki uboczne w postaci nieoczekiwanych korzyści. W tym wypadku będzie to dodatkowa poprawa wydajności.
Co? Po co? Dlaczego tak?
W jakim celu mapujemy świat relacyjny na obiektowy?
Być może po to aby:
- Pobierać w wygodny sposób obiekty biznesowe - wraz z wygodnymi mechanizmami typu Lazy Loading
- Wykonywać na nich operacje biznesowe zmieniające ich stan - możemy tutaj tworzyć zarówno anemiczne encje modyfikowane przez serwisy jak również projektować prawdziwe obiekty modelujące reguły i niezmienniki biznesowe (styl Domain Driven Design)
- Utrwalać stan obiektów biznesowych - stan, który zmienił się w poprzednim kroku (korzystając z wygodnych mechanizmów wykrywania "brudzenia" i mechanizmu kaskadowego zapisu całych grafów obiektów)
Jeżeli używasz JPA do tych klas problemów, to używasz odpowiedniego młotka do odpowiedniej klasy problemu. Czyli pobieram obiekt (JEDEN, no dwa, góra cy;) biznesowy, zmieniam jego stan, zapisuję go.
Pisząc zmieniam stan nie mam na myśli "edytuję podpinając pod formularz". Mam na myśli logikę aplikacji (modelującą Use Case/User Story), która modyfikuje mój obiekt biznesowy (uruchamiając jego metody biznesowe lub settery jeżeli jest on anemiczny). Przykład gdzie Order i Invoice to obiekty persystentne:
public void approveOrder(Long orderId) {
Order order = orderRepository.load(orderId);
//sample: Specification Design Pattern
Specification<Order> orderSpecification = generateSpecification(systemUser);
if (!orderSpecification.isSatisfiedBy(order))
throw new OrderOperationException("Order does not meet specification", order.getEntityId());
// sample: call Domain Logic
order.submit();
// sample: call Domain Service (Bookkeeper)
Invoice invoice = invoicingService.issuance(order, generateTaxPolicy(systemUser));
invoiceRepository.save(invoice);
orderRepository.save(order);
}
Link do kodu.
Zawsze?
Jeżeli natomiast chcę wyświetlić na ekranie dane, np. dane przekrojowe, np w postaci tabelki (ludzie biznesu uwielbiają tabelki, najlepiej aby dało się przestawiać kolejność ich kolumn;) to narzędzie pod tytułem JPA nie jest odpowiednim młotkiem do tego problemu. W tym wypadku na każdym etapie postępuję nieracjonalnie:
- Pobieram z mapera listę obiektów (zamapowanych na całe tabelki w bazie) gdy potrzebuję na ekranie jedynie kilku kolumn z każdej tabelki (dla bazy nie robi to różnicy, ale gdy maszyna serwująca bazę lub klient je zdalna to wówczas zaznamy odczuwać skutki tej decyzji)
- Mam możliwość korzystania z mechanizmu Lazy Loadingu, który nie ma sensu dla operacji typu "pobierz dane do wyświetlenia"
- Silnik mapera wykonuje niepotrzebne operacje związane z LL i wykrywaniem "brudzenia" - przecież nie będę modyfikował tych obiektów, chcę jedynie coś wyświetlić
- Zdradzam model biznesowy warstwie prezentacji. Być może w prostych aplikacjach z prezentacją w technologii webowej (ta sama maszyna pobiera i prezentuje dane) nie jest to problem - dodatkowo zyskujemy produktywność w pracy. Ale jeżeli klienty (nie klienci) są zdalne (np Android)? Zdradzanie modelu domenowego wiąże się z drastycznym spadkiem bezpieczeństwa (wsteczna inżynieria) oraz z koniecznością koordynacji prac zespołów pracujących nad "klientem" i "serwerem", że o zapewnianiu kompatybilności starszych wersji klientów nie wspomnę. Niby banały, jednak w niektórzy ewangelizatorzy EE zachęcają do zwracania encji ponad warstwę serwisów (sic!)
Klasa problemu <=> Klasa rozwiązania
W każdym systemie mamy wyraźnie rozróżnienie na odczyt danych i modyfikację danych. Pisałem już jakiś czas temu o paradygmacie
Command-query Seapration, z którego wyłoniła się architektura
Command-query Responsibility Segregation.
Nie chcę się powtarzać, zatem w materiałach bloga (link na górze po prawej) znajdziecie prezentację na ten temat, polecam też artykuł samego mistrza:
Martina Fowlera.
Separacja
Tak więc donosząc się do pytania Andrzeja z poprzedniego posta: Jak najbardziej konstrukcja SELECY NEW MyDTO(encja.pole1, ecnja.pole2) FROM Encja encja jest na miejscu. W przypadku gdy chodzi o zwrócenie
danych do prezentacji (modelowanych jako DTO) a nie pobranie obiektów biznesowych do wykonania na nich operacji biznesowych.
Separację możemy poprowadzić jeszcze "głębiej" i dokonać projekcji modelu domenowego, który utrzymujemy w III postaci normalnej do postaci płaskiej, odpowiedniej do odczytu. W tym celu możemy zastosować Widoki Zmaterializowane lub np. odświeżać model Read przy pomocy zdarzeń domenowych.
Najmniej racjonalną rzeczą jaką możemy zrobić to pobierać encje JPA i przepakowywać je na DTO. Po co pobierać te dane (ryzykując N+1SP) skoro i tak musimy wykonać pracę (kodowanie, załączenie automatu) przepakowania?
Wydajność ++
Ale zostawmy zaawansowane architektury przygotowane do skalowania...
Jeżeli już decydujemy się na pobieranie poszczególnych kolumn z bazy, to dlaczego nie użyć czystego SQL zamiast HQL (konstrukcji SELECT NEW)? Przecież skoro wiem, że pewnych miejscach pobieram dane jedynie do odczytu, to być może warto zestawić osobną pulę połączeń z bazą - połączeń read-only. Być może baza, której używasz będzie wówczas działać nieco lepiej...:)
W takim wypadku warto użyć lekkiego mapera typu
myBatis, którego użyję w celu mapowania Result Set na paczki danych (DTO) a nie na obiekty biznesowe służce do wykonywania operacji biznesowych!
//======================================
Pracując z Hibernate (od 2003r, od wersji 2.0) zawsze, w każdym jednym projekcie - małym i dużym (1200 tabel) dochodziło do sytuacji przepisania pewnych zapytań na czysty SQL z powodu wydajności. MyBatis na prawdę działa:)
Przykłady architektury, która wyraźnie rozdziela operacje odczytu i zapisu:
Domain Driven Design & Command-query Responsibility Segregation sample project - Java, Spring, JPA