‘Несоответствие импеданса’. Нет двух слов, охватывающих проблемы, головные боли и причуды, с которыми сталкивается большинство разработчиков при попытке связать приложения с реляционными базами данных (RDBMS). Но давайте посмотрим правде в глаза, объектно-ориентированные проекты не уйдут в ближайшее время от основных языков, а также реляционные системы хранения используются в большинстве приложений. Одна сторона работает с объектами, а другая с таблицами. Устранение этих различий — или как это технически называется «несоответствие объекта / реляционного импеданса» — может привести к значительным накладным расходам, что, в свою очередь, может привести к снижению производительности приложения.
В Java Java Persistence API (JPA) является одним из самых популярных механизмов, используемых для преодоления разрыва между объектами (т. Е. Языком Java) и таблицами (т. Е. Реляционными базами данных). Хотя существуют и другие механизмы, позволяющие приложениям Java взаимодействовать с реляционными базами данных, такие как JDBC и JDO, JPA получила более широкое распространение благодаря своим основам: объектно-реляционное сопоставление (ORM). Популярность ORM объясняется именно тем, что он специально разработан для взаимодействия между объектом и таблицами.
В случае с JPA существует стандартный орган, отвечающий за настройку курса. Этот процесс уступил место нескольким реализациям JPA, среди трех самых популярных вы найдете: EclipseLink (развитый из TopLink), Hibernate и OpenJPA. Но даже несмотря на то, что все три основаны на одном и том же стандарте, а ORM — такая глубокая и сложная тема, помимо основных функциональных возможностей, каждая реализация имеет различия в пределах от конфигурации до методов оптимизации.
Далее я объясню ряд тем, связанных с оптимизацией использования JPA приложением, с использованием и сравнением каждой из предыдущих реализаций JPA. Хотя JPA способен автоматически создавать реляционные таблицы и может работать с рядом поставщиков реляционных баз данных, я не буду полагаться на уже существующие данные, развернутые в реляционной базе данных MySQL, в дополнение к использованию среды Spring для облегчения использования JPA. Это не только сделает более справедливое сравнение, но и сделает описанные методы привлекательными для более широкой аудитории, поскольку проблемы с производительностью становятся серьезной проблемой, когда у вас большой объем данных, в дополнение к тому, что MySQL и Spring — общий выбор из-за корни их сообщества (то есть с открытым исходным кодом).См. Раздел «Исходный код / приложение» в конце для инструкций по настройке кода приложения, который обсуждается в оставшейся части разделов.
Загрузите исходный код, связанный с этой статьей (~ 45 МБ)
Основы: Метрика
Чтобы установить уровни производительности JPA в приложении, важно сначала получить серию метрик, связанных с внутренней работой реализации JPA. К ним относятся такие вещи, как:
- Какие фактические запросы выполняются к СУБД?
- Сколько времени занимает каждый запрос?
- Постоянно ли выполняются запросы к СУБД или используется кеш?
Эти показатели будут иметь решающее значение для нашего анализа производительности, поскольку они пролят свет на основные операции, выполняемые реализацией JPA, и в процессе показывают эффективность или неэффективность определенных методов.
В этой области вы найдете первые различия между реализациями, и я говорю не о результатах метрик, а о том, как получить эти метрики. Чтобы начать, сначала я затрону тему ведения журнала. По умолчанию все три реализации JPA, обсуждаемые здесь, — EclipseLink, Hibernate и OpenJPA — регистрируют запросы, выполненные для RDBMS, что будет преимуществом в определении того, являются ли запросы, выполняемые ORM, оптимальными для конкретной реляционной модели данных. Тем не менее, дальнейшая настройка уровня ведения журнала реализации JPA может быть полезна для одной из двух вещей: получения еще большей информации о базовых операциях, выполняемых JPA — которые можно отключить по умолчанию (например, сведения о подключении к базе данных) — или не получать никакой информации о регистрации — что может повысить производительность производственной системы.
Ведение журнала в реализациях JPA управляется с помощью одной из нескольких сред ведения журналов, таких как Apache Commons Logging или Log4J. Это требует наличия таких библиотек в приложении. Ведение журнала конфигурации реализации JPA в основном выполняется через значение <property> в файле persistence.xml приложения или, в некоторых случаях, непосредственно в файлах конфигурации среды ведения журнала. В следующей таблице описаны параметры конфигурации ведения журнала JPA:
Большой стол, так вот внешняя ссылка
В дополнение к информации, получаемой с помощью регистрации, существует другой набор показателей производительности JPA, которые требуют различных шагов, которые должны быть получены. Одним из таких показателей является время, необходимое для выполнения запроса. Хотя некоторые реализации JPA предоставляют эту информацию с использованием определенных конфигураций, некоторые этого не делают. Несмотря на это, я решил использовать отдельный подход и применить его ко всем трем рассматриваемым реализациям JPA. В конце концов, метрики времени, измеренные в миллисекундах, могут быть искажены определенным образом в зависимости от критериев времени начала и окончания. Поэтому для измерения времени запроса я буду использовать аспекты с помощью среды Spring.
Аспекты позволят нам измерить время, необходимое для выполнения метода, содержащего запрос, без смешивания логики синхронизации с реальной логикой запроса — последняя особенность которой заключается в цели использования Аспектов. Дальнейшее обсуждение Аспектов выходит за рамки производительности, поэтому далее я сосредоточусь на самом Аспекте. Я советую вам просмотреть прилагаемый исходный код , Аспекты и Аспекты Spring для получения более подробной информации по этим темам и их конфигурации. Следующий аспект используется для измерения времени выполнения в методах запроса.
package com.webforefront.aop;import org.apache.commons.lang.time.StopWatch;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.annotation.Aspect; class DAOInterceptor { private Log log = LogFactory.getLog(DAOInterceptor.class); ("execution(* com.webforefront.jpa.service..*.*(..))") public Object logQueryTimes(ProceedingJoinPoint pjp) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Object retVal = pjp.proceed(); stopWatch.stop(); String str = pjp.getTarget().toString(); log.info(str.substring(str.lastIndexOf(".")+1, str.lastIndexOf("@")) + " - " + pjp.getSignature().getName() + ": " + stopWatch.getTime() + "ms"); return retVal; }}
Основная часть Аспекта — аннотация @Around. Значение, назначенное этой последней аннотации, указывает на выполнение метода аспекта — logQueryTimes — каждый раз, когда выполняется метод, принадлежащий классу в пакете com.webforefront.jpa.service — этот последний пакет — то, где все запросы JPA нашего приложения методы будут жить. Задача логики, выполняемая методом аспекта logQueryTimes, заключается в вычислении времени выполнения и выводе его в виде информации регистрации с использованием регистрации Apache Commons.
Другой набор важных метрик JPA связан со статистикой, выходящей за рамки стандартной регистрации. Статистика, на которую я ссылаюсь, относится к кешам, сеансам и транзакциям. Поскольку стандарт JPA не предписывает какой-либо конкретный подход к статистике, каждая реализация JPA также различается по типу и способу сбора статистики. И Hibernate, и OpenJPA имеют свой собственный класс статистики, в котором EclipseLink использует профилировщик для сбора похожих метрик.
Поскольку я уже полагаюсь на Аспекты, я также буду использовать Аспект для получения статистики как до, так и после выполнения метода запроса JPA. Следующий аспект получает статистику для приложения, использующего Hibernate.
package com.webforefront.aop;import org.hibernate.stat.Statistics;import org.hibernate.SessionFactory;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.beans.factory.annotation.Autowired;import javax.persistence.EntityManagerFactory;import org.hibernate.ejb.HibernateEntityManagerFactory;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory; class CacheHibernateInterceptor { private Log log = LogFactory.getLog(DAOInterceptor.class); private EntityManagerFactory entityManagerFactory; ("execution(* com.webforefront.jpa.service..*.*(..))") public Object log(ProceedingJoinPoint pjp) throws Throwable { HibernateEntityManagerFactory hbmanagerfactory = (HibernateEntityManagerFactory) entityManagerFactory; SessionFactory sessionFactory = hbmanagerfactory.getSessionFactory(); Statistics statistics = sessionFactory.getStatistics(); String str = pjp.getTarget().toString(); statistics.setStatisticsEnabled(true); log.info(str.substring(str.lastIndexOf(".")+1, str.lastIndexOf("@")) + " - " + pjp.getSignature().getName() + ": (Before call) " + statistics); Object result = pjp.proceed(); log.info(str.substring(str.lastIndexOf(".")+1, str.lastIndexOf("@")) + " - " + pjp.getSignature().getName() + ": (After call) " + statistics); return result; } }
Обратите внимание на структуру, аналогичную предыдущему аспекту синхронизации, за исключением того, что в этом случае выходные данные журнала содержат значения, принадлежащие классу Statistics Hibernate, полученные через EntityManagerFactory приложения. Следующий аспект используется для получения статистики для приложения, использующего OpenJPA.
package com.webforefront.aop;import org.apache.openjpa.datacache.CacheStatistics;import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;import org.apache.openjpa.persistence.OpenJPAPersistence;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.beans.factory.annotation.Autowired;import javax.persistence.EntityManagerFactory;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory; class CacheOpenJPAInterceptor { private Log log = LogFactory.getLog(DAOInterceptor.class); private EntityManagerFactory entityManagerFactory; ("execution(* com.webforefront.jpa.service..*.*(..))") public Object log(ProceedingJoinPoint pjp) throws Throwable { OpenJPAEntityManagerFactory ojpamanagerfactory = OpenJPAPersistence.cast(entityManagerFactory); CacheStatistics statistics = ojpamanagerfactory.getStoreCache().getStatistics(); String str = pjp.getTarget().toString(); log.info(str.substring(str.lastIndexOf(".")+1, str.lastIndexOf("@")) + " - " + pjp.getSignature().getName() + ": (Before call) Statistics [start time=" + statistics.start() + ",read count=" + statistics.getReadCount() + ",hit count=" + statistics.getHitCount() +",write count=" + statistics.getWriteCount() + ",total read count=" + statistics.getTotalReadCount() + ",total hit count=" + statistics.getTotalHitCount() +",total write count=" + statistics.getTotalWriteCount()); Object result = pjp.proceed(); log.info(str.substring(str.lastIndexOf(".")+1, str.lastIndexOf("@")) + " - " + pjp.getSignature().getName() + ": (After call) Statistics [start time=" + statistics.start() + ",read count=" + statistics.getReadCount() + ",hit count=" + statistics.getHitCount() +",write count=" + statistics.getWriteCount() + ",total read count=" + statistics.getTotalReadCount() + ",total hit count=" + statistics.getTotalHitCount() +",total write count=" + statistics.getTotalWriteCount()); return result; } }
Еще раз, обратите внимание на структуру Aspect, аналогичную предыдущему Aspect, который основан на EntityManagerFactory приложения. В этом случае выходные данные журнала содержат значения, которые принадлежат классу CacheStatistics OpenJPA. Поскольку OpenJPA не включает статистику по умолчанию, вам необходимо добавить следующие два свойства в файл persistence.xml приложения:
<property name="openjpa.DataCache" value="true(EnableStatistics=true)"/><property name="openjpa.RemoteCommitProvider" value="sjvm"/>
Первое свойство обеспечивает сбор статистики, а второе свойство указывает, что сбор статистики происходит в одной JVM. ПРИМЕЧАНИЕ. Значение «true (EnableStatistics = true)» также включает кэширование в дополнение к статистике.
Поскольку EclipseLink не имеет какого-либо конкретного класса статистики и использует профилировщик для определения расширенных метрик, самый простой способ получить статистику, аналогичную статистике в Hibernate и OpenJPA, — через сам профилировщик. Чтобы активировать EclipseLink’s Profiler, вам просто нужно добавить следующее свойство в файл persistence.xml приложения: <property name = «eclipselink.profiler» value = «PerformanceProfiler» />. Таким образом, EclipseLink Profiler выводит несколько метрик при каждом выполнении метода запроса JPA в виде информации журнала.
Теперь, когда вы знаете, как получить несколько метрик из всех трех реализаций JPA, и понимаете, что они будут получены максимально справедливо для всех трех поставщиков, пришло время проверить каждую реализацию JPA вместе с несколькими методами повышения производительности.
JPQL-запросы, ткачество и преобразования классов
Давайте начнем с создания запроса, который извлекает данные, принадлежащие уже существующей таблице RDBMS с именем «Master». Таблица «Мастер» содержит более 17 000 записей, принадлежащих бейсболистам. Чтобы упростить задачу, я создам класс Java с именем «Player» и сопоставлю его с таблицей «Master», чтобы извлечь записи в виде объектов. Далее, опираясь на функциональность JpaTemplate среды Spring, я настрою запрос для извлечения всех объектов «Player», при этом запрос будет иметь следующую форму:
getJpaTemplate().find("select e from Player e");
См. Прилагаемый исходный код для получения дополнительной информации об этом последнем процессе.
Затем я развертываю приложение, используя каждую из трех реализаций JPA в Apache Tomcat, делая это отдельно, а также запускаю и останавливаю сервер в каждом развертывании, чтобы обеспечить справедливые результаты. Вот результаты этого на 64-битной оперативной памяти Ubuntu-4GB с использованием Java 1.6:
Все объекты игрока — 17,468 записей |
Время |
запрос |
Hibernate |
3558 мс |
выберите player0_.lahmanID в качестве lahmanID0_, player0_.nameFirst в качестве nameFirst0_, player0_.nameLast в качестве nameLast0_ из Master player0_ |
EclipseLink (Weaver во время выполнения — Spring ReflectiveLoadTimeWeaver weaver) |
3215 мс |
ВЫБЕРИТЕ lahmanID, nameLast, nameFirst FROM Master |
EclipseLink (ткачество во время сборки) |
3571 мс |
ВЫБЕРИТЕ lahmanID, nameLast, nameFirst FROM Master |
EclipseLink (нет плетения) |
3996 мс |
ВЫБЕРИТЕ lahmanID, nameLast, nameFirst FROM Master |
OpenJPA (расширенные классы времени сборки) |
5998 мс |
ВЫБЕРИТЕ t0.lahmanID, t0.nameFirst, t0.nameLast FROM Master t0 |
OpenJPA (расширенные классы времени выполнения — энхансер OpenJPA) |
6136 мс |
ВЫБЕРИТЕ t0.lahmanID, t0.nameFirst, t0.nameLast FROM Master t0 |
OpenJPA (не расширенные классы) |
7677 мс |
ВЫБЕРИТЕ t0.lahmanID, t0.nameFirst, t0.nameLast FROM Master t0 |
Как вы можете заметить, запросы, выполняемые каждой реализацией JPA, довольно похожи, причем два из них используют сокращенную запись (например, t0 и player0 для таблицы с именем «Master»). Однако это изменение синтаксиса оказывает минимальное влияние на производительность, поскольку <i> прямой </ i> запрос к СУБД с использованием любого из этих вариантов обозначений показывает идентичные результаты. Однако время выполнения запросов в нескольких реализациях JPA с использованием различных параметров значительно различается. Одним из важных факторов, приводящих к этой разнице во времени, является то, как каждая реализация обрабатывает сущности JPA.
Давайте начнем с реализации OpenJPA, которая имела самые плохие времена. OpenJPA может выполнить процесс расширения на объектах Java (например, в этом случае класс ‘Player’). Этот процесс усовершенствования может быть выполнен, когда сущности построены, во время выполнения или полностью предрешены. Как вы можете заметить, вышеупомянутое улучшение сущностей в OpenJPA привело к увеличению времени запроса. В то время как улучшающие сущности во время сборки или во время выполнения давали относительно лучшие результаты, при этом первое опережало второе. По умолчанию OpenJPA ожидает улучшения сущностей. Это означает, что вам нужно либо явно настроить приложение для поддержки не расширенных классов, добавив следующее:
<property name="openjpa.RuntimeUnenhancedClasses" value="supported"/>
… свойство файла persistence.xml приложения или расширять классы во время сборки или во время выполнения, полагаясь на энхансер OpenJPA, в противном случае приложение, использующее OpenJPA, выдаст ошибку.
Учитывая эти результаты OpenJPA, остальные тесты OpenJPA будут основаны на расширенных классах сущностей во время сборки. Для получения дополнительной информации по теме улучшения OpenJPA обратитесь к документации OpenJPA в дополнение к ознакомлению с прилагаемым исходным кодом для этой статьи.
Вам может быть интересно, что именно представляет собой расширение OpenJPA? Усовершенствование сущности OpenJPA — это этап обработки, применяемый к байт-коду, сгенерированному компилятором Java, который добавляет специфические инструкции JPA для обеспечения оптимальной производительности во время выполнения. Эти инструкции могут включать в себя такие вещи, как гибкая отложенная загрузка и отслеживание грязного чтения. Так почему же Hibernate или EclipseLink не улучшают сущности? Короче говоря, Hibernate и EclipseLink также расширяют возможности JPA, они просто не называют это «улучшением».
EclipseLink называет этот процесс «улучшения» более техническим термином: ткачество. Подобно процессу улучшения OpenJPA, переплетение в EclipseLink может происходить во время сборки (то есть статическое переплетение), во время выполнения или вообще прекращается. Как видно из результатов, все тесты EclipseLink представляют меньшие вариации по сравнению с OpenJPA. Самая длинная вариация EclipseLink включала не использование ткачества. Если задуматься, это довольно логично, учитывая, что цель создания состоит в изменении байтового кода Java с целью добавления оптимизированных инструкций JPA, которые включают в себя отложенную загрузку, отслеживание изменений, группы выборки и внутреннюю оптимизацию.
Для тестов EclipseLink, использующих плетение, и плетение во время сборки, и во время выполнения дают лучшие результаты. Для ткачества во время сборки я использовал библиотеку EclipseLink вместе с задачей Apache Ant, где для ткачества во время выполнения я использовал платформу Spring ReflectiveLoadTimeWeaver. Я могу только предположить, что чуть более высокая производительность использования ткачества во время выполнения по сравнению с ткачеством во время сборки в EclipseLink была обусловлена тем фактом, что ткач использовался в интегрированной среде Spring, что, в свою очередь, могло привести к лучшей оптимизации JPA, разработанной для приложений Spring. Тем не менее, учитывая результаты теста отказа от ткачества, ткачество не оказывает существенного влияния на производительность при использовании EclipseLink, при прочих равных условиях.
По умолчанию EclipseLink ожидает включения во время выполнения, в противном случае вы получите ошибку в форме «Невозможно применить преобразователь классов без указания LoadTimeWeaver». Это означает, что для случаев, в которых используется плетение во время сборки или вообще нет плетения, вам необходимо явно указать это поведение. Чтобы отключить переплетение EclipseLink, вам нужно будет либо сконфигурировать бин EntityManagerFactory Spring приложения с помощью:
<property name="jpaPropertyMap"><map><entry key="eclipselink.weaving" value="false"/></map></property>
… или добавить ….
<property name="eclipselink.weaving" value="false"/>
… свойство файла persistence.xml приложения. Чтобы указать, что сущности приложения создаются с использованием ткачества во время сборки, замените значение «false» предыдущего свойства на «static». Чтобы сконфигурировать ткача по умолчанию, ожидаемого EclipseLink, добавьте следующее:
<property name="loadTimeWeaver"><bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/></property>
… свойство Spring-компонента приложения EntityManagerFactory.
Учитывая эти результаты EclipseLink, остальные тесты EclipseLink будут основаны на ткачестве во время выполнения, предоставляемом средой Spring. Более подробную информацию о теме EclipseLink ткачества, обратитесь к документации EclipseLink на http://wiki.eclipse.org/Introduction_to_EclipseLink_Application_Development_(ELUG)#Using_Weaving , в дополнение к консультационным сопутствующую исходный код для этой статьи.
Hibernate не требует ни улучшения сущностей JPA, ни ткачества. По этой причине, есть только один результат теста. Это не только упрощает настройку Hibernate, но, судя по его единственному результату тестирования — какие часы стоят на втором месте по сравнению со всеми остальными тестами, — производительность Hibernate по сравнению с аналогами выше. Однако в том, что я бы назвал Hibernate эквивалентным процессу улучшения OpenJPA или переплетению EclipseLink, вы найдете ряд свойств Hibernate. Например, Hibernate имеет такие свойства, как
hibernate.default_batch_fetch_size предназначен для оптимизации отложенной загрузки. Как вы, возможно, помните, одной из целей процесса улучшения OpenJPA и ткачества EclipseLink является оптимизация отложенной загрузки. Поэтому, поскольку OpenJPA и EclipseLink требуют отдельного и монолитного шага — во время сборки или во время выполнения — для реализации методов оптимизации JPA, Hibernate прибегает к использованию гранулярных свойств, указанных в файле persistence.xml приложения. Тем не менее, учитывая, что стандартное поведение Hibernate оказалось на одном уровне с лучшими временами запросов, я не чувствовал необходимости дальнейшего изучения этих свойств Hibernate.
Чтобы получить другое представление о времени и процедурах сопоставления каждой реализации JPA, я сделаю более выборочные запросы на основе имени и фамилии объекта Player. Это результаты выполнения запроса для всех объектов Player, чье имя — Джон, и запроса для всех объектов Player, чья фамилия в Smith.
Все объекты игрока, имя которого Джон — 472 записи |
Время |
запрос |
EclipseLink |
1265 мс |
ВЫБЕРИТЕ lahmanID, nameLast, nameFirst ОТ Master WHERE (nameFirst =?) |
Hibernate |
613 мс |
выберите player0_.lahmanID в качестве lahmanID0_, player0_.nameFirst в качестве nameFirst0_, player0_.nameLast в качестве nameLast0_ из Master player0_, где player0_.nameFirst =? |
OpenJPA |
1643 мс |
ВЫБЕРИТЕ t0.lahmanID, t0.nameFirst, t0.nameLast FROM Master t0 ГДЕ (t0.nameFirst =?) [Params =?] |
Все объекты игрока, чья фамилия Смит — 146 записей |
Время |
запрос |
EclipseLink |
986 мс |
ВЫБЕРИТЕ lahmanID, nameLast, nameFirst ОТ Master WHERE (nameLastt =?) |
Hibernate |
537 мс |
выберите player0_.lahmanID в качестве lahmanID0_, player0_.nameFirst в качестве nameFirst0_, player0_.nameLast в качестве nameLast0_ из Master player0_, где player0_.nameLast =? |
OpenJPA |
1452 мс |
ВЫБЕРИТЕ t0.lahmanID, t0.nameFirst, t0.nameLast FROM Master t0 ГДЕ (t0.nameLast =?) [Params =?] |
Эти результаты тестирования рассказывают немного другую историю: все три реализации JPA демонстрируют существенные различия во времени между собой. При меньшем количестве записей стандартная конфигурация Hibernate обеспечила почти вдвое более быстрые запросы, чем у ближайшего конкурента, и почти в три раза быстрее запросов, чем у другого конкурента.
Чтобы получить еще более широкое представление о времени и процедурах сопоставления каждой реализации JPA, я сделаю запрос к одному объекту Player на основе его идентификатора. Это результаты выполнения такого запроса.
Одиночный проигрыватель, чей ID 777-1 запись |
Время |
запрос |
EclipseLink |
521 мс |
ВЫБЕРИТЕ lahmanID, nameLast, nameFirst ОТ МАСТЕРА ГДЕ (lahmanID =?) |
Hibernate |
157 мс |
выберите player0_.lahmanID в качестве lahmanID0_0_, player0_.nameFirst в качестве nameFirst0_0_, player0_.nameLast в качестве nameLast0_0_ из основного player0_, где player0_.lahmanID =? |
OpenJPA |
1052 мс |
ВЫБЕРИТЕ t0.nameFirst, t0.nameLast FROM Master t0 ГДЕ t0.lahmanID =? [PARAMS =?] |
За исключением более быстрого времени запросов — поскольку это запрос для одного объекта Player — время между реализациями JPA практически пропорционально запросам, используемым для извлечения нескольких объектов Player по имени и фамилии.
Это будет сделано в том, что касается тестовых запросов. Однако при обсуждении этих тем по оптимизации / усовершенствованию / ткачеству следует соблюдать осторожность. Несмотря на то, что предыдущие тесты состояли из запроса более 17 000 записей и подтверждения явных преимуществ использования одного поставщика и метода перед другим, они все еще являются одномерными, поскольку они основаны на операциях чтения, выполняемых для одного типа объекта и одной таблицы РСУБД. JPA может выполнять большой массив операций, которые также включают в себя обновление, запись и удаление записей СУБД, не говоря уже о выполнении более сложных запросов, которые могут охватывать несколько объектов и таблиц. Кроме того, сами СУБД могут иметь влияющие факторы (например, индексы) на время запроса JPA. Итак, все это говорит о том, что не слишком надумано думать об использовании расширения сущностей OpenJPA,Свойства сплетения EclipseLink или свойства Hibernate могут иметь различные степени — как полезные, так и вредные — в зависимости от используемых запросов (например, с несколькими таблицами, с несколькими объектами) и типа операции JPA (например, чтение, запись, обновление, удаление).
Next, I will describe one of the most popular techniques used to boost performance in JPA applications.
Caches
A cache allows data to remain closer to an application’s tier without constantly polling an RDBMS for the same data. I entitled the section in plural — caches — because there can be several caches involved in an application using JPA. This of course doesn’t mean you have to configure or use all the caches provided by an application relying on JPA, but properly configuring caches can go a long way toward enhancing an application’s JPA performance.
So lets start by analyzing what it’s each JPA implementation offers in its out-of-the-box state in terms of caching. The following table illustrates tests done by simply invoking the previous JPA queries for a second and third consecutive time, without stopping the server. Note that the same process of deploying a single application at once was used, in addition to the server being re-started on each set of tests.
Query / Implementation |
EclipseLink |
Hibernate |
OpenJPA |
All records (1st time) |
3215 ms |
3558 ms |
5998 ms |
All records (2nd time) |
507 ms |
272 ms |
521 ms |
All records (3rd time) |
439 ms |
218 ms |
263 ms |
First name (1st time) |
1265 ms |
613 ms |
1643 ms |
First name (2nd time) |
151 ms |
115 ms |
239 ms |
First name (3rd time) |
154 ms |
101 ms |
227 ms |
Last name (1st time) |
986 ms |
537 ms |
1452 ms |
Last name (2nd time) |
41 ms |
41 ms |
112 ms |
Last name (3rd time) |
65 ms |
38 ms |
117 ms |
By ID (1st time) |
521 ms |
157 ms |
1052 ms |
By ID (2nd time) |
1 ms |
6 ms |
3 ms |
By ID (3rd time) |
1 ms |
3 ms |
3 ms |
As you can observe, on both the second and third invocation all the queries show substantial improvements with respect to the first invocation. The primary cause for these improvements is unequivocally due to the use of a cache. But what type of cache exactly ? Could it be an RDBMS’s own caching engine ? JPA ? Spring ? Or some other variation ?. In order to shed some light on cache usage, the following table illustrates the cache statistics generated on each of the previous JPA queries.
Query / Impleme)ntation |
EclipseLink |
Hibernate |
OpenJPA |
All records (2nd time) |
number of objects=17468, total time=506, local time=506, row fetch=65, object building=328, cache=112, sql execute=47, objects/second=34521, |
sessions opened=2, |
N/A |
All records (3rd time) |
number of objects=17468, total time=435, local time=435, profiling time=1, row fetch=28, object building=323, cache=106, logging=1, sql execute=27, objects/second=40156,
|
sessions opened=3, |
N/A |
First name (2nd time) |
number of objects=472, total time=148, local time=148, row fetch=27, object building=106, cache=7, logging=1, sql execute=3, objects/second=3189,
|
sessions opened=2, |
N/A |
First name (3rd time) |
number of objects=472, total time=152, local time=152, row fetch=20, object building=121, cache=7, sql execute=3, objects/second=3105,
|
sessions opened=3, |
N/A |
Last name (2nd time) |
number of objects=146, total time=40, local time=40, row fetch=7, object building=27, cache=2, logging=1, sql execute=3, objects/second=3650, |
sessions opened=2, |
N/A |
Last name (3rd time) |
number of objects=146, total time=63, local time=63, profiling time=1, row fetch=6, object building=19, cache=5, sql prepare=1, sql execute=23, objects/second=2317, |
sessions opened=3, |
N/A |
By ID (2nd time) |
number of objects=1, total time=1, local time=1, time/object=1, objects/second=1000,
|
sessions opened=2, |
N/A |
By ID (3rd time) |
number of objects=1, total time=1, local time=1, time/object=1, objects/second=1000,
|
sessions opened=3, |
N/A |
Notice the statistics generated by each JPA implementation are different. EclipseLink reports a single cache statistic, OpenJPA doesn’t even report statistics unless a cache is enabled — see previous section on metrics for details on this behavior — and Hibernate reports two cache related statistics: second level cache and query cache.
At this juncture, if you look at the test results and statistics for the second and third invocation, something won’t add up. How is it that OpenJPA’s test results came out faster when caching is disabled by default ? An how about Hibernate returning 0’s on its cache related statistics, even when its test results came out faster ? The reason for this performance increase is due to RDBMS caching. On the first query, the RDBMS needs to read data from its own file system (i.e. perform an I/O operation), on subsequent requests the data is present in RDBMS memory (i.e. its cache) making the entire JPA query much faster. A closer look at the Hibernate statistics field ‘queries executed to the database’ can confirm this. Notice that on every second query it shows 2 and on every third query it shows 3, meaning the data was read directly from the database. NOTE: The only exception to this occurs when a query is made on a single entity (i.e. by id), I will address this shortly.
Next, lets start breaking down the caches you will encounter when using JPA applications. The JPA 2.0 standard defines two types of caches: A first level cache and a second level cache. The first level cache or EntityManager cache is used to properly handle JPA transactions. A first level cache only exist for the duration of the EntityManager. With the exception of long lived operations performed against a RDBMS, JPA EntityManager’s are short lived and are created & destroyed per request or per transaction. In this case, given the nature of the queries, first level caches are cleared on every query. A second level cache on the other hand is a broader cache that can be used across transactions and users. This makes a JPA second level cache more powerful, since it can avoid constantly polling an RDBMS for the same data.
But even though the JPA 2.0 standard now addresses second level cache features, this was not the case in JPA 1.0. In the 1.0 version of the JPA standard only a first level cache was addressed, leaving the door completely open on the topic of a second level cache. This created a fragmented approach to caching in JPA implementations, which even now as JPA 2.0 compliant implementations emerge, some non-standard features continue to be part of certain implementations given the value they provide to JPA caching in general. So as I move forward, bear in mind that just like previous JPA topics, each JPA implementation can have its own particular way of dealing with second level caching.
I will start with OpenJPA, which has the least amount of proprietary caching options. To enable OpenJPA caching (i.e. second level caching) you need to declare the following two properties in an application’s persistence.xml file:
<property name="openjpa.DataCache" value="true(EnableStatistics=true)"/><property name="openjpa.RemoteCommitProvider" value="sjvm"/>
The first property ensures caching and statistics are activated, while the second property is used to indicate caching take place on a single JVM. The following results and statistics were obtained with OpenJPA’s second level cache enabled.
Query with OpenJPA caching |
Time |
Statistics |
Time without statistics |
All records (2nd time) |
420 ms |
read count=34936, |
347 ms |
All records (3rd time) |
254 ms |
read count=52404, |
230 ms |
First name (2nd time) |
125 ms |
read count=944, |
127 ms |
First name (3rd time) |
114 ms |
read count=1416, |
132 ms |
Last name (2nd time) |
63 ms |
read count=292, |
53 ms |
Last name (3rd time) |
49 ms |
read count=438, |
50 ms |
By ID (2nd time) |
5 ms |
read count=2, |
1 ms |
By ID (3rd time) |
4 ms |
read count=3, |
1 ms |
As these test results illustrate, executing subsequent JPA queries with OpenJPA’s second level cache produce superior results. Another important behavior illustrated in some of these test cases is that by simply disabling statistics — and still using the second level cache — query times improve even more. The OpenJPA statistics also demonstrate how the cache is being used. Notice that on each subsequent query the statistics field ‘hit count’ is duplicated, which means data is being read from the cache (i.e. a hit). Also notice the statistics field ‘write count’ remains static, which means data is only written once from the RDBMS to the cache.
This is pretty basic functionality for a second level cache. On certain occasions a need may arise to interact directly with a cache. These interactions can range from prohibiting an entity from being cached, assigning a particular amount of memory to a cache, forcing an entity to always be cached, flushing all the data contained in a cache, or even plugging-in a third party caching solution to provide a more robust strategy, among other things.
The JPA 2.0 standard provides a very basic feature set in terms of second level caching through javax.persistence.Cache. Upon consulting this interface, you’ll realize it only provides four methods charged with verifying the presence of entities and evicting them. This feature set not only proves to be limited, but also cumbersome since it can only be leveraged programmatically (i.e. through an API). In this sense, and as I’ve already mentioned, JPA implementations have provided a series of features ranging from persistence.xml properties to Java annotations related to second level caching.
OpenJPA offers several of these second level caching features, including a separate and supplemental cache called a ‘query cache’ which can further improve JPA performance. For such cases, I will point you directly to OpenJPA’s cache documentation available at http://openjpa.apache.org/builds/apache-openjpa-2.1.0-SNAPSHOT/docs/manual/ref_guide_caching.html#ref_guide_cache_query so you can try these parameters for yourself on the accompanying application source code.
Hibernate just like OpenJPA has its second level cache disabled. To enable Hibernate’s second level cache you need to add the following properties to an application’s persistence.xml file:
<property name="hibernate.cache.use_second_level_cache" value="true"/><property name="hibernate.cache.provider_class" value="org.hibernate.cache.HashtableCacheProvider"/>
Its worth mentioning that Hibernate has integral support for other second level caches. The previous properties displayed how to enable the HashtableCacheProvider cache — the simplest of the integral second level caches — but Hibernate also provides support for five additional caches, which include: EHCache, OSCache, SwarmCache, JBoss cache 1 and JBoss cache 2, all of which provide distinct features, albeit require additional configuration. Besides these properties, Hibernate also requires that each JPA entity be declared with a caching strategy. In this case, since the Person entity is read only, a caching strategy like the following would be used:
<class name="com.webforefront.jpa.domain.Player"><cache usage="read-only"/></class>
Similar to OpenJPA, Hibernate also offers several second level caching features through proprietary annotations and configurations, as well as support for the separate and supplemental cache called a ‘query cache’ which can further improve JPA performance. For such cases, I will also point you directly to Hibernate’s cache documentation available at http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html#performance-cache so you can try these parameters for yourself on the accompanying application source code.
Unlike OpenJPA and Hibernate, EclipseLink’s second level cache is enabled by default, therefore there is no need to provide any additional configuration. However, similar to its counterparts, EclipseLink also has a series of proprietary second level cache features which can enhance JPA performance. You can find more information on these features by consulting EclipseLink’s cache documentation available at: http://wiki.eclipse.org/Introduction_to_Cache_(ELUG)
With this we bring our discussion on object relational mapping performance with JPA to a close. I hope you found the various tests and metrics presented here a helpful aid in making decisions about your own JPA applications. In addition, don’t forget you can rely on the accompanying source code to try out several JPA variations more ad-hoc to your circumstances.
About the author
Daniel Rubio is an independent technology consultant specializing in enterprise and web-based software. He blogs regularly on these and other software areas at http://www.webforefront.com. He’s also authored and co-authored three books on Java technology.
Source code/Application installation
* Install MySQL on your workstation (Tested on MySQL 5.1.37-64 bits) — http://dev.mysql.com/downloads/
* Install data set on MySQL — Go to http://www.baseball-databank.org/ and click on the link titled ‘Database in MySQL form’. This will download a zipped file with a series of MySQL data structures containing baseball statistics. First create a MySQL database to host the data using the command: ‘mysqladmin -p<password> create jpaperformance’. This will create a database named ‘jpaperformance’. Next, load the baseball statistics using the following command: ‘mysql -p<password> -D jpaperformance < BDB-sql-2009-11-25.sql’ where ‘BDB-sql-2009-11.25.sql’ represents the unzipped SQL script obtained by extracting the zip file you dowloaded.
* Create JPA application WARs — The download includes source code, library dependencies and an Ant build file. This includes all three JPA implementations Hibernate 3.5.3, EclipseLink 2.1 and OpenJPA 2.1.
- To build the JPA Hibernate WAR — ant hibernate
- To build the JPA EclipseLink WAR — ant eclipselink
- To build the JPA OpenJPA WAR — ant openjpa
- All builds are placed under the dist/<jpa_implementation> directories.
* Deploy to Tomcat 6.0.26 — Copy the MySQL Java driver and Spring Tomcat Weaver — included in the download directory ‘tomcat_jar_deps’ — to Apache Tomcat’s /lib directory.
— Copy each JPA application WAR to Apache Tomcat’s /webapps directory, as needed.
* Deployment URL’s
- http://localhost:8080/hibernate/hibernate/home ( Query all Player objects )
- http://localhost:8080/eclipselink/eclipselink/home ( Query all Player objects )
- http://localhost:8080/openjpa/openjpa/home ( Query all Player objects )
- http://localhost:8080/hibernate/hibernate/firstname/<player_firstname> ( Query Player objects by first name)
- http://localhost:8080/eclipselink/eclipselink/firstname/<player_firstname> ( Query Player objects by first name)
- http://localhost:8080/openjpa/openjpa/firstname/<player_firstname> ( Query Player objects by first name)
- http://localhost:8080/hibernate/hibernate/lastname/<player_lastname> ( Query Player objects by last name)
- http://localhost:8080/eclipselink/eclipselink/lastname/<player_lastname> ( Query Player objects by last name)
- http://localhost:8080/openjpa/openjpa/lastname/<player_lastname> ( Query Player objects by last name)
- http://localhost:8080/hibernate/hibernate/playerid/<player_id> (Query Player by id)
- http://localhost:8080/eclipselink/eclipselink/playerid/<player_id> ( Query Player by id)
- http://localhost:8080/openjpa/openjpa/playerid/<player_id> ( Query Player by id)