Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я собираюсь представить лучший способ предотвращения проблем с производительностью при использовании JPA и гибернации.
Много лет назад я работал руководителем группы, и однажды генеральный менеджер попросил меня взглянуть на проект, который был в большой беде.
Рассматриваемое приложение разрабатывалось командой разработчиков программного обеспечения более 9 месяцев, и клиент только что протестировал его в производственной среде.
Клиент очень расстроился, когда понял, что приложение едва ползает. Например, мне сказали, что запрос выполнялся в течение 10 часов без каких-либо признаков остановки.
Проанализировав проект, я определил множество областей, которые можно было бы улучшить, и именно так родилась моя страсть к высокопроизводительному доступу к данным.
Лучший способ предотвратить проблемы с производительностью JPA и гибернации. https://t.co/9FDS0NwAPt pic.twitter.com/5tycyhfj6X
JPA и спящий режим
Это был 2004 год, когда я впервые услышал о Hibernate. Тогда я работал над проектом .NET для своей дипломной работы в колледже и был не очень доволен ADO.NET в то время. Поэтому я начал читать о Nhibernate, который в то время все еще находился в стадии бета-тестирования. NHibernate пытался адаптировать проект Hibernate 2 с Java на .NET, и даже бета-версия в то время была гораздо лучшей альтернативой ADO.NET.
С этого момента Hibernate стал по-настоящему популярным. На самом деле API сохранения Java, появившийся в 2006 году, во многом основан на Hibernate.
Благодаря JPA популярность Hibernate возросла еще больше, поскольку большинство проектов Java EE или Spring использовали его прямо или косвенно. Даже сегодня большинство проектов Spring Boot также используют режим гибернации с помощью модуля Spring Data JPA.
Регистрация инструкций SQL
При использовании структуры доступа к данным, где все запросы должны быть указаны явно, очевидно, какие SQL-запросы будут выполняться приложением.
С другой стороны, JPA и Hibernate выполняют инструкции SQL на основе переходов состояний сущностей, управляемых кодом уровня доступа к данным.
По этой причине очень важно всегда регистрировать инструкцию SQL, сгенерированную JPA, и переходить в спящий режим.
Лучший способ регистрации инструкций SQL-это использовать JDBC Источник данных
или Драйвер
прокси, как описано в этой статье .
Модель предметной области
Давайте рассмотрим, что вы сопоставляете родительскую таблицу post
и дочернюю таблицу post_comment
. Существует связь “один ко многим” между таблицами post
и post_comment
через столбец post_id
Внешний ключ в таблице post_comment
.
Вы можете сопоставить таблицы post
и post_comment
как объекты JPA следующим образом:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id private Long id; private String title; public Long getId() { return id; } public Post setId(Long id) { this.id = id; return this; } public String getTitle() { return title; } public Post setTitle(String title) { this.title = title; return this; } } @Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment { @Id private Long id; @ManyToOne private Post post; private String review; public PostComment setId(Long id) { this.id = id; return this; } public Post getPost() { return post; } public PostComment setPost(Post post) { this.post = post; return this; } public String getReview() { return review; } public PostComment setReview(String review) { this.review = review; return this; } }
Обратите внимание, что Сообщение
и Комментарий к сообщению
используют API в стиле fluent. Для получения более подробной информации о преимуществах использования этой стратегии ознакомьтесь с этой статьей .
Теперь давайте предположим, что мы добавляем три Сообщения
сущности в нашу базу данных, каждая Запись
содержит три Сообщения
дочерние сущности:
doInJPA(entityManager -> { long pastId = 1; long commentId = 1; for (long i = 1; i <= 3; i++) { Post post = new Post() .setId(pastId++) .setTitle( String.format( "High-Performance Java Persistence, part %d", i ) ); entityManager.persist(post); for (int j = 0; j < 3; j++) { entityManager.persist( new PostComment() .setId(commentId++) .setPost(post) .setReview( String.format( "The part %d was %s", i, reviews[j] ) ) ); } } });
Извлечение данных
Предположим, вы хотите загрузить Комментарий к сообщению
из базы данных. Для этого вы можете вызвать метод find
JPA следующим образом:
PostComment comment = entityManager.find( PostComment.class, 1L );
При выполнении метода find
Hibernate генерирует следующий SQL – запрос:
SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_, p.id AS id1_0_1_, p.title AS title2_0_1_ FROM post_comment pc LEFT OUTER JOIN post p ON pc.post_id=p.id WHERE pc.id=1
Откуда взялось это ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ
?
Ну, это потому, что @ManyToOne
ассоциация в Комментарии к сообщению
использует стратегию выборки по умолчанию, которая является Типом выборки.НЕТЕРПЕЛИВЫЙ
.
Таким образом, Hibernate должен выполнить ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ
, как указано в отображении, оно всегда должно инициализировать сообщение
ассоциацию при извлечении сообщения
сущности.
Теперь посмотрите, что происходит, когда вы выполняете запрос JPQL для получения одного и того же Комментария к сообщению
сущности:
PostComment comment = entityManager .createQuery( "select pc " + "from PostComment pc " + "where pc.id = :id", PostComment.class) .setParameter("id",1L) .getSingleResult();
Вместо ЛЕВОГО ВНЕШНЕГО СОЕДИНЕНИЯ
теперь у нас есть дополнительный запрос:
SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_ FROM post_comment pc WHERE pc.id = 1 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
Теперь на этот раз был один дополнительный запрос, но если мы выберем все Комментарий к сообщению
сущности, связанные с данным Сообщением
заголовком:
List comments = entityManager .createQuery( "select pc " + "from PostComment pc " + "join pc.post p " + "where p.title like :titlePatttern", PostComment.class) .setParameter( "titlePatttern", "High-Performance Java Persistence%" ) .getResultList(); assertEquals(9, comments.size());
Hibernate теперь будет выдавать 4 запроса:
SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_ FROM post_comment pc JOIN post p ON pc.post_id=p.id WHERE p.title LIKE 'High-Performance Java Persistence%' SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
На этот раз есть четыре SQL-запроса. Первый из них предназначен для фактического запроса JPQL, который фильтрует записи таблицы post_comment
, в то время как остальные три предназначены для быстрого извлечения сущности Post
.
Проверка и проверка всех этих @ManyToOne
ассоциаций и проверка того, что они всегда используют FetchType.ЛЕНИВЫЙ
потребует времени. Более того, вы не можете гарантировать, что однажды кто-то другой придет и изменит данную ассоциацию из FetchType.ЛЕНИВЫЙ
в Тип выборки.НЕТЕРПЕЛИВЫЙ
.
Автоматическое обнаружение проблем с производительностью
Гораздо лучшим подходом к решению этой проблемы является использование оптимизатора персистентности Hy .
После установки зависимости Maven:
io.hypersistence hypersistence-optimizer ${hypersistence-optimizer.version}
Все, что вам нужно сделать, это добавить следующий код в любой из ваших интеграционных тестов:
@Test public void testNoPerformanceIssues() { HypersistenceOptimizer hypersistenceOptimizer = new HypersistenceOptimizer( new JpaConfig(entityManagerFactory()) ); assertTrue(hypersistenceOptimizer.getEvents().isEmpty()); }
Вот и все!
Теперь, если вы попытаетесь запустить тесты, набор завершится неудачей со следующей ошибкой:
ERROR [main]: Hypersistence Optimizer - CRITICAL - EagerFetchingEvent - The [post] attribute in the [io.hypersistence.optimizer.config.PostComment] entity uses eager fetching. Consider using a lazy fetching which, not only that is more efficient, but it is way more flexible when it comes to fetching data. For more info about this event, check out this User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#EagerFetchingEvent java.lang.AssertionError at org.junit.Assert.fail(Assert.java:86) at org.junit.Assert.assertTrue(Assert.java:41) at org.junit.Assert.assertTrue(Assert.java:52) at io.hypersistence.optimizer.config.FailFastOnPerformanceIssuesTest.testNoPerformanceIssues(FailFastOnPerformanceIssuesTest.java:41)
Потрясающе, правда?
Вы даже не сможете создать проект с такими проблемами производительности, как эта, из-за проникновения в ваш код доступа к данным.
Вывод
Использование JPA и Hibernate очень удобно, но вам нужно уделять особое внимание базовым операторам SQL, которые создаются от вашего имени, так как в противном случае вы можете столкнуться с проблемами производительности.
Хотя вы можете вручную просмотреть каждую фиксацию, выполняемую в вашей кодовой базе, чтобы убедиться, что никакие изменения не вызовут проблемы с производительностью JPA и гибернации доступа к данным, гораздо лучше, если вы сможете автоматизировать эту задачу и выполнить сборку с ошибкой, если обнаружена проблема, связанная с производительностью.
С помощью такого инструмента , как Hy persistence Optimizer, вы, наконец, можете потратить свое время, сосредоточившись на требованиях к приложениям, вместо того, чтобы гоняться за JPA и решать проблемы с производительностью.