Анализ и оптимизация одного запроса в EclipseLink

В этой статье я решил собрать несколько полезных практик, которым я научился за два года работы с 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>

Небольшое примечание

Не пытайтесь включить 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 как таковой.

ссылка на оригинал статьи http://habrahabr.ru/post/164495/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *