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

в 16:44, , рубрики: java, jpa, метки:

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

Автор: Aleosha

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js