piątek, 2 marca 2012

Racjonalne wykorzystanie JPA

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

15 komentarzy:

ags pisze...

Po części ze względu na legacy, po części ze względu na smutek ziejący z tabelek, pomieszaliśmy ostatnio jdbcTemplate i JPA i nawet fajnie było.

Sławek Sobótka pisze...

Dokładnie o to chodzi...
btw: warto obadać http://www.mybatis.org/core/statement-builders.html

Maciej Hadam pisze...

No tak!!!
młotek do gwoździ
wkrętak do wkrętów
zapamiętać!!

A mówiąc serio, to przychodzą mi do głowy 2 przykłady potwierdzające Twoje zdanie.
1. Command

Załóżmy, że mamy tabelę bazodanową z 20 kolumnami, gdzie 5 z tych kolumn jest powiązana kluczami obcymi z innymi tabelami.
Przyglądnijmy się teraz wydajności bazy przy zmianie 1 atrybutu obiektu odwzorowującego tą tabelę.
Wartośc zmienianego atrybutu nie jest ograniczona kluczem obcym.

A) Hibernate: wykona update 1 kolumny dynamicznie budując zapytanie DML
B) myBatis: zapewne ze względu na 1 operację "update" w DAO wykona update wszystkich kolumn tabeli

W podejśu A oszczędzimy bazie danych weryfikacji spójności kluczy obcych.
W podejściu B musimy liczyć Się z dużo większą pracą związaną z tworzeniem dopasywanych do przypadku użycia poleceń SQL.

Do DDL wybieram A.

2. Query
Powiedzmy, że w naszej aplikacji pojawiło się zapytanie(query), które rozgrzewa do czerwoności macierz dyskową i procek podgrzewając temperaturę w mieście o 0,5 stopnia.
Administrator zwrócił nam raport pokazujący obciążające zapytanie.

Jak odnaleźć winowajcę?
A) Hibernate: wykonać "reverse magic ORM" i odnaleźć zapytanie w HQL
B) myBatis: wyszukać fragment zapytania w plikach XML(preferuję rozdzielenie zapytań SQL od kodu Java)

Do zapytań wybieram B.

Pozdrawiam,

Anonimowy pisze...

Nie łączę dwóch mapperów ORM w jednej aplikacji. Hibernate udostępnia "NamedQuery", które można trzymać w pliku XML i mogą to być zapytania HQL jak i SQL. Obiekty DTO odczytuję zapytaniami SQL z NamedQuery, w operacjach wyszukiwania z GUI korzystam z HibernateCriteria i obiekt DTO jest zmapowany na widok. Jeśli używam Hibernate, nie jest mi już potrzebny IBatis.

Sławek Sobótka pisze...

Różne są sytuacje - warto znać różne podejścia i narzędzia...

Pomijając już shreding.

Ogólny kontekst persystencji (SessionFactory/EntityManagerFactory) musi ze swej natury "stać na" puli połączeń READ/WRITE. Jeżeli wiem, że będę czytał, wówczas mogę zestawić pulę połączeń READ (ONLY) - niektóre bazy będą działać nieco szybciej.

Oczywiście można na tej drugiej puli postawić drugi kontekst persystencji - ale po co...

Rafi pisze...

A może denormalizacja i indeksowanie?

Sławek Sobótka pisze...

tak, jest to jakieś podejście. Czasem jedyne z dostępnych - jeżeli spójność jest wymogiem. Ale jeżeli nie jest, to próbując stworzyć jeden model, który będzie służył zarówno do zapisu jak i do odczytu wprowadzamy złożoność przypadkową (w sensie http://en.wikipedia.org/wiki/Accidental_complexity ), która zawsze się "mści" podczas rozbudowy i utrzymania:)

Rafi pisze...

Dla mnie generalnie jeśli mówimy o DDD to wyszukiwanie obiektów musi się odbywać przez stosowne Repository. To jak ono zostanie zaimplementowane to jest druga sprawa, ale nie wyobrażam sobie posługiwania się SQLem czy HQLem na poziomie API domeny. I w sumie pod kątem efektywności wydobywania informacji to właśnie mamy pole do popisu na poziomie implementacji Repository i tutaj zaczyna się cała zabawa jak to zrobić. SQL jest jakimś wyjściem, natomiast to jest ryzykowna decyzja i musi być podjęta świadomie i z automatu nakłada na oprogramowanie pewne ograniczenia, którego wszyscy "shareholders" powinni być świadomi.

Sławek Sobótka pisze...

Repo zarządza agregatem: pobiera po numerze/id, utrwala

Ew. może wyszukiwać agregaty na potrzeby operacji biznesowych, np: wyszukaj niezatwierdzone zamówienia danego użytkownika - ponieważ chcemy in nadać rabaty.

Natomiast wyszukiwanie na potrzeby UI, to już odpowiedzialność poza Repo - np "zwykłe" serwisy wyszukujące. No i tutaj zaczyna się walka o wydajność:P

Rafi pisze...

Wyszukiwanie na potrzeby UI jest zwykle wyszukiwaniem jednocześnie na potrzeby operacji biznesowych. Jeśli mam UI, który składa się z tabeli prezentującej zamówienia w trakcie realizacji to muszę znać model domenowy, żeby wiedzieć co to w praktyce znaczy "zamówienie w trakcie realizacji" (czy cokolwiek sobie tu wstawimy, zawsze będzie to jakieś kryterium wyszukiwania). Wobec czego jak dla mnie UI budowane jest NAD modelem domenowym, a nie jak Pan proponuje OBOK i do mnie trafia rozwiązanie, w którym CQRS budujemy NAD modelem domenowym. Jeśli użyjemy CQRS obok i zaczniemy sobie SQLem wyciągać coś z bazy to mamy 2 miejsca, w których dublujemy wiedzę o strukturach danych. Jeśli jedynym zyskiem jest tutaj szybkość, to mam wrażenie, że jest to zysk obarczony za dużymi kosztami.

Sławek Sobótka pisze...

Zgadza się, koszty utrzymania są duże, dlatego "maksymalna" separacja w CqRS jaką jest stworzenie osobnego modelu do odczytu jest:
- ostatnią fazą optymalizacji
- nie stosujemy jej w całej rozciągłości projektu

Natomiast co do pobierania danych domenowych i ich przepakowywania w obiekty transferowe, to będzie działać chyba raczej przy niedużym obciążeniu... (zależy jeszcze od sprzętu:)

Rafi pisze...

Raczej bym powiedział, że to jest ostatnia deska ratunku :).

Przepakowywanie w DTO. Po pierwsze przepakowywanie można zrobić optymalnie i nieoptymalnie. Zawsze jest kwestia tego na co możemy sobie pozwolić, niemniej jeśli chodzi o moje zdanie, to przy aplikacjach klasy enterprise, jeśli możemy sobie pozwolić na sprzęt to jest to całkiem niezły pomysł. Dlaczego? Bo:
1. Nie psujemy w ten sposób architektury.
2. Programista nie musi mieć rozległej wiedzy o strukturach danych, co przy dużej rozległości projektach jest sporą zaletą i elegancko zwiększa hermetyzację i ułatwia zarządzanie projektem.
3. Usuwamy wszystkie problemy związane z Lazy Loading, jeśli korzystamy np. z Hibernate.

O Java i technologie pokrewnych można powiedzieć wiele, ale nie to, że są wydajne (w sensie jednostkowym). Po to stworzono szereg technologii z szeroko pojętej skalowalności, żeby móc zachować dobrą architekturę. Ja kieruję się (przy poziomie enterprise) zasadą, że wolę dopłacić za dodatkową maszynę niż po 2 latach zastanawiać się czy kod da się jeszcze zmieniać czy już można tylko zaorać :).

Rafi pisze...

Aha i jeszcze uwaga numer 2 :), żeby nie było, że bredzę :).

Oczywiście, jeśli potrzebujemy zrobić zestawienie statystyczne/wykres to to jest w ogóle inaczej postawiony problem. Oczywiście można by teoretycznie wyciągnąć 10000 obiektów biznesowych, przełożyć do DTO i wykonać analizę, no ale to gołym okiem widać, że zalatuje absurdem.

Więc w momencie kiedy Pan pisze o zestawieniach statycznych/wykresach to trzeba zmienić narzędzie. To jest wredny problem, do którego podchodziłem wiele razy na różne sposoby. Na dziś unikam SQL/HQL i wolę pójść w przetwarzanie takich danych "z boku" i trzymanie sobie zdenormalizowanych danych pomocniczych tak jak robi się to w hurtowaniach. Bo skoro mamy tych obiektów 100 000 i musimy wytworzyć analizę to zaczynamy wychodzić poza system transakcyjny i przechodzić do systemu typu hurtownia.

Bo skoro już dyskutujemy o narzędziach to trzeba pamiętać, że DDD jest narzędziem przeznaczonym głównie do systemów transakcyjnych, a nie analitycznych. W systemach analitycznych podejście się nie sprawdzi, bo główna zaleta DDD czyli spójność modelu (tak dla odczytu jak i zapisu) ma jednocześnie duży koszt i kompletnie żadnego sensu na w zastosowaniach analitycznych gdzie "model" traci w dużym stopniu sens, a zapisów po prostu nie ma, czyli nie ma co się wysilać.

Sławek Sobótka pisze...

"Enterprise" nie jedno ma imię:)

Wystarczy wyobrazić sobie ekran z podglądem produktu przed jego zakupem, gdzie sugerujemy zamienniki/produkty dodatkowe z grafu powiedzmy znajomych, którzy również kupili ten produkt. Wówczas można rozważyć oparcie Read Modelu noSql grafowy.

Ale generalnie widzę, że się zgadzamy, chyba tylko rozjechały się nam konteksty - dlatego warto skupić uwagę na konkretnych przykładach. Bo w CqRS generalnie o to chodzi, że może istnieć model domenowy (transakcyjny) i pewne "projekcie" danych (ale nie wszystkich, zwykle wybranych ze względu na wydajność lub specyfikę - np wspomniany graf).

Rafi pisze...

Jeszcze z ciekawych podejść do problemu jest pomysł Reporting Database opisywany przez Fowlera i Evansa:

http://martinfowler.com/bliki/ReportingDatabase.html

Koncept bardzo ciekawy i w sumie rozszerzalny.