Czy nam się to podoba, czy nie, często używamy ORM-ów w naszych aplikacjach. Możemy je jednocześnie kochać i nienawidzić. Z jednej strony bardzo pomagają w pisaniu kodu zorientowanego obiektowo. Z drugiej strony, ich złożoność powoduje problem złego użycia, często z powodu innych frameworków, które ukrywają ORM-y za inną abstrakcją (na przykład framework Spring Data) lub przez niewłaściwe użycie samych ORM-ów.

Kilka miesięcy temu spotkałem się z naprawdę interesującą sytuacją dotyczącą wykorzystania ORM w jednym z projektów opartych na Springu.

Spis treści:

Definicja problemu

Po pierwsze, chciałbym przedstawić funkcję Entity Graph, która pochodzi ze specyfikacji JPA 2.1. Koncepcja ta pozwala oznaczać i grupować niektóre kolekcje wewnątrz encji, aby pobierać je w bardzo elastyczny sposób w czasie wykonywania.

Jak zapewne wiesz, JPA 2.0 wprowadziło dwa rodzaje strategii pobierania danych:

  1. FetchType.LAZY
  2. FetchType.EAGER

Implementacja jest bardzo prosta i pozwala zdefiniować strategię typu pobierania dla każdej kolekcji wewnątrz encji w sposób statyczny. Innymi słowy, nie można przełączać się między tymi dwiema strategiami w czasie wykonywania i wybierać odpowiedniego zachowania w zależności od różnych scenariuszy przypadków użycia.

Nowa koncepcja, zwana Entity Graph, pozwala na tworzenie meta-definicji za pomocą dedykowanych adnotacji. Adnotacja @NamedEntityGraph pozwala na wyszczególnienie atrybutów, które należy uwzględnić podczas pobierania danych z relacyjnej bazy danych (patrz przykład poniżej):

@NamedEntityGraphs({
            @NamedEntityGraph(
        name = "ContractWithGuaranteesOnly,
        attributeNodes = {
                @NamedAttributeNode(“attribute1”),
                @NamedAttributeNode(“attribute2”),
                @NamedAttributeNode(“guarantees”)
        },
            @NamedEntityGraph(
        name = "ContractWithTermsAndConditionsOnly,
        attributeNodes = {
                @NamedAttributeNode(“attribute1”),
                @NamedAttributeNode(“attribute2”),
                @NamedAttributeNode(“termsConditions”)
        }
)
 
})
public class Contract {
 
            @OneToMany
            private Set<Guarantee> guarantees;
 
            @OneToMany 
            private Set<TermsConditions> termsConditions;
            //….
 
}

W tym przykładzie adnotacja @NamedAttributeNode została użyta do zdefiniowania powiązanych jednostek, które mają zostać załadowane, gdy używamy określonego @NamedEntityGraph. Wygląda niesamowicie, prawda? Jednak, jak w przypadku każdego narzędzia, może to być miecz obosieczny, który może łatwo cię zabić lub przynajmniej zranić. W moim konkretnym przypadku jeden z deweloperów zdecydował się zdefiniować graf encji o nazwie Contract.getFull. Co gorsza, encja Contract miała tam zdefiniowanych ponad 20 kolekcji (jest to system legacy i nie pytaj dlaczego ta encja jest tak duża. Pozostawiam to bez komentarza). W każdym razie, sytuacja nie była dobra, ponieważ EntityGraph był pobierany w wielu miejscach, co powodowało naprawdę duże problemy z wydajnością i zużywało dużo pamięci.

Opis problemu

Gdy używany jest @NamedEntityGraph, pod maską używane jest zapytanie LEFT OUTER JOIN. W naszym konkretnym przykładzie wyglądało to następująco:

select * from contract
            left outer join terms_conditions termscondi1_ on contract0_.terms_conditions_id=termscondi1_.id 
            left outer join guarantee guarantees11_ on contract0_.id=guarantees11_.contract_id
            //+22 similar outer joins
where SOME_NOT_IMPORTANT_CONDITIONS

Zapytanie to zwraca iloczyn kartezjański w celu zbudowania JEDNEJ prostej encji kontraktowej. W naszym konkretnym przypadku było to około 200 000 rekordów z bazy danych, które musiały zostać zmapowane przez ORM do jednego obiektu. Taka ilość rekordów powodowała zawieszanie się aplikacji (ORM nie był w stanie przetworzyć takiej ilości danych). Problem został oficjalnie utworzony na Hibernate ORM Jira:https://hibernate.atlassian.net/browse/HHH-12897

Rozwiązanie problemu

W tej chwili nie ma stałego rozwiązania dla tego przypadku. Oczywiście w pełni rozumiem, że problem jest wynikiem złego projektu i zbyt złożonej encji oraz sposobu, w jaki framework został użyty. W moim konkretnym przypadku całkowicie usunąłem @NamedEntityGraph, który pobiera wszystkie kolekcje. Zamiast tego wprowadziłem stare dobre mapowanie FetchType.LAZY jako obejście. Tworzy ono podzapytania zamiast ogromnej konstrukcji LEFT OUTER JOIN. Jednym z efektów ubocznych jest umożliwienie Lazy Loading poza transakcją. W aplikacji Spring należy ustawić następującą właściwość:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

Wyobrażam sobie, że to rozwiązanie nie jest srebrną kulą i ma wiele innych skutków ubocznych, ale w mojej konkretnej sytuacji działa. Zamiast czekać 1 minutę na pobranie danych, teraz jest to mniej niż 1 sekunda. Wystarczająco dobrze, prawda? 🙂

Aby w pełni rozwiązać ten problem, konieczna byłaby głęboka refaktoryzacja kodu, a jednostka musiałaby zostać rozłożona na mniejsze części.

Przeczytaj również: Ustawianie domyślnego profilu Spring dla testów z opcją zastępowania

5/5 - (10 głosów)