Статья расчитана на тех, кто уже работал с фрэймворком основанным на 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 как таковой.
ссылка на оригинал статьи http://habrahabr.ru/post/164495/
Добавить комментарий