В этой статье я решил собрать несколько полезных практик, которым я научился за два года работы с ORM фреймворком EclipseLink на основе реального примера.
Статья расчитана на тех, кто уже работал с фрэймворком основанным на JPA, будь то Hibernate или OpenJPA.
Проект, примеры которого я буду приводить, основан на Spring.
Имеются следующие таблицы
ROLES представляет собой стандартную lookup table, с малым количеством строк.
Соответственно, в entity Article мы определяем связь через JoinTable:
Теперь мы определяем query — getAllArticles следующим образом:
И спустя неделю имея десять тысяч статей в БД, начинаем получать жалобы на низкую производительность. Проблема.
Для начала, воспользуемся PerformanceMonitor'ом EclipseLink'а, чтобы замерить сколько запросов к БД реально проходят через JPA.
Проще всего включить его через persistence.xml
Но persistence.xml у нас может быть общим и для тестов, и для аппликации.
Зато beans.xml у них разный. Так что для тестов достаточно прописать в нем:
Теперь воспользуемся нашим профайлером в тесте.
PerformanceMonitor содержит в себе Map, в котором он хранит всю информацию, начиная с общего количества запросов к БД, и заканчивая временем для каждого.
Нас интересуют два конкретных параметра: Counter:ReadAllQuery и Counter:ReadObjectQuery.
Получим их и сравним до и после
Чтобы обнаружить, что разница составляет не 1, как можно было бы ожидать, а 10001. Ой.
Дело в том, что не смотря на fetch = FetchType.EAGER, при использовании JoinTable JPA решает генерировать запрос для каждой строчки, чтобы получить соответствующий объект Role.
Добавим Hint, указывающий JPA как приносить данные
Рассмотрим синтакс этого hint'а.
Часть до дочки должна соответствовать alias'у объекта в тексте query.
Если по ошибке воспользоваться другой буквой, Hint не сработает, молча, не выбросив ошибку.
Часть после точки должна соотвествовать имени member'а внутри Article.
Все отлично, теперь БД достигает ровно один запрос. Но мы внезапно обнаруживает, что количество возвращаемых запросом теперь не десять тысяч, как раньше, а только девять. Проблема.
Дело в том, что QueryHints.FETCH переписывает запрос на использования JOIN'а. Но если в JOIN TABLE нет соответствующей строки (у статьи не определена необходимая роль), то не вернется и основная строка.
К счастью, на этот случай есть QueryHints.LEFT_FETCH.
Финальное решение будет выглядеть так:
Один запрос к БД, все объекты, без нужны менять текст query как таковой.
Статья расчитана на тех, кто уже работал с фрэймворком основанным на 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>
Небольшое примечание
Не пытайтесь включить Profiler через анотацию @PersistenceContext, таким вот образом:
Этот способ не сработает.
@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 как таковой.