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:
- FetchType.LAZY
- 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