В этой статье я решил собрать несколько полезных практик, которым я научился за два года работы с ORM фреймворком EclipseLink на основе реального примера.
Статья расчитана на тех, кто уже работал с фрэймворком основанным на JPA, будь то Hibernate или OpenJPA.
Проект, примеры которого я буду приводить, основан на Spring.
Проблема:
Имеются следующие таблицы
ARTICLES -> Article.java
(
ID int,
NAME varchar
);
ARTICLE_ROLES
(
ARTICLE_ID int,
ROLE_ID int
);
ROLES -> Role.java
(
ID int,
NAME varchar
);
ROLES представляет собой стандартную lookup table, с малым количеством строк.
Соответственно, в entity Article мы определяем связь через JoinTable:
@ManyToOne(optional = false, targetEntity = Role.class, fetch = FetchType.EAGER, cascade = {
CascadeType.MERGE, CascadeType.REFRESH })
@JoinTable(name = "ARTICLE_ROLES",
joinColumns = {@JoinColumn(name = "ARTICLE_ID", referencedColumnName="ID", nullable = false},
inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID", nullable = false)})
public Role getRole() {
return role;
}
Теперь мы определяем query — getAllArticles следующим образом:
@NamedQuery(name = "Article. getAllArticles", query = "SELECT s FROM Article s")
И спустя неделю имея десять тысяч статей в БД, начинаем получать жалобы на низкую производительность. Проблема.
Анализ проблемы:
Для начала, воспользуемся PerformanceMonitor'ом EclipseLink'а, чтобы замерить сколько запросов к БД реально проходят через JPA.
Проще всего включить его через persistence.xml
<persistence>
…
<properties>
…
<property name="eclipselink.profiler" value="PerformanceMonitor"/>
</properties>
</persistence-unit>
</persistence>
Но persistence.xml у нас может быть общим и для тестов, и для аппликации.
Зато beans.xml у них разный. Так что для тестов достаточно прописать в нем:
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
…
<property name="jpaPropertyMap">
<map>
<entry key="eclipselink.profiler" value="PerformanceMonitor" />
</map>
</property>
</bean>
@PersistenceContext(properties={@PersistenceProperty(name="eclipselink.profiler",value="PerformanceMonitor")})
protected EntityManager em;
Этот способ не сработает.
Теперь воспользуемся нашим профайлером в тесте.
PerformanceMonitor profiler = (PerformanceMonitor)em.unwrap(Session.class).getProfiler();
PerformanceMonitor содержит в себе Map, в котором он хранит всю информацию, начиная с общего количества запросов к БД, и заканчивая временем для каждого.
Нас интересуют два конкретных параметра: Counter:ReadAllQuery и Counter:ReadObjectQuery.
Получим их и сравним до и после
Long before = profiler.getOperationTimings.get("Counter:ReadAllQuery") + profiler.getOperationTimings.get("Counter:ReadObjectQuery ");
em.createNamedQuery("Article.getAllArticles").getResultList();
Long after = profiler .getOperationTimings.get("Counter:ReadAllQuery") + profiler.getOperationTimings.get("Counter:ReadObjectQuery ");
Чтобы обнаружить, что разница составляет не 1, как можно было бы ожидать, а 10001. Ой.
Дело в том, что не смотря на fetch = FetchType.EAGER, при использовании JoinTable JPA решает генерировать запрос для каждой строчки, чтобы получить соответствующий объект Role.
Решение, первая версия:
Добавим Hint, указывающий JPA как приносить данные
@NamedQuery(name = "Article. getAllArticles", query = "SELECT s FROM Article s", hints = {
<b>@QueryHint(name = QueryHints. FETCH, value = "s.role")</b>)
Рассмотрим синтакс этого hint'а.
Часть до дочки должна соответствовать alias'у объекта в тексте query.
Если по ошибке воспользоваться другой буквой, Hint не сработает, молча, не выбросив ошибку.
Часть после точки должна соотвествовать имени member'а внутри Article.
public class Article {
…
Role <b>role</b>;
…
}
Все отлично, теперь БД достигает ровно один запрос. Но мы внезапно обнаруживает, что количество возвращаемых запросом теперь не десять тысяч, как раньше, а только девять. Проблема.
Решение, вторая версия.
Дело в том, что QueryHints.FETCH переписывает запрос на использования JOIN'а. Но если в JOIN TABLE нет соответствующей строки (у статьи не определена необходимая роль), то не вернется и основная строка.
К счастью, на этот случай есть QueryHints.LEFT_FETCH.
Финальное решение будет выглядеть так:
@NamedQuery(name = "Article. getAllArticles", query = "SELECT s FROM Article s", hints = {
@QueryHint(name = QueryHints.LEFT_FETCH, value = "s.role"))
Один запрос к БД, все объекты, без нужны менять текст query как таковой.
Автор: Aleosha