Всех с праздником!
Так уж внезапно получилось, что старт второй группы «Разработчик Java Enterprise» совпал с 256-м днём в году.Совпадение? Не думаю.
Ну и делимся предпоследней интересностью: что же нового привнёс JPA 2.2 — cтриминг результатов, улучшенное преобразование даты, новые аннотации — лишь несколько примеров полезных улучшений.
Поехали!
Java Persistence API (JPA) — основополагающая спецификация Java EE, которая широко используется в индустрии. Независимо от того, разрабатываете вы для платформы Java EE или для альтернативного фреймворка Java, JPA — ваш выбор для сохранения данных. JPA 2.1 улучшили спецификацию, позволив разработчикам решать такие задачи, как автоматическая генерация схемы базы данных и эффективная работа с процедурами, хранящимися в базе данных. Последняя версия, JPA 2.2, улучшает спецификацию на основе этих изменений.
В этой статье я расскажу о новом функционале и приведу примеры, которые помогут начать с ним работать. В качестве образца я использую проект “Java EE 8 Playground”, который есть на GitHub. Пример приложения основан на спецификации Java EE 8 и использует фреймворк JavaServer Faces (JSF), Enterprise JavaBeans (EJB) и JPA для персистентности. Чтобы понять, о чем речь, вы должны быть знакомы с JPA.

Использование JPA 2.2
Версия JPA 2.2 — часть платформы Java EE 8. Стоит отметить, что только Java EE 8 совместимые серверы приложений предоставляют спецификацию, готовую к использованию out of the box. В момент написания этой статьи (конец 2017 года), таких серверов приложений было довольно мало. Тем не менее, пользоваться JPA 2.2 при использовании Java EE7 — легко. Сначала необходимо скачать соответствующие JAR файлы с помощью Maven Central и добавить их в проект. Если вы используете Maven в своем проекте, добавьте координаты в файл POM Maven:
Затем, выберете имплементацию JPA, которую хотите использовать. Начиная с версии JPA 2.2 и EclipseLink, и Hibernate имеют совместимые реализации. В качестве примеров в этой статье, я использую EclipseLink, добавляя следующую зависимость:
Если вы используете Java EE 8 совместимый сервер, например GlassFish 5 или Payara 5, то должны иметь возможность уточнить область “provided” для этих зависимостей в файле POM. Иначе, укажите область “compile”, чтобы включить их в сборку проекта.
Поддержка Даты и Времени Java 8
Возможно, одним из наиболее положительно встреченных дополнений является поддержка Java 8 Date and Time API. С момента релиза Java SE 8 в 2014 году, разработчики пользовались обходными путями, чтобы использовать Date and Time API с JPA. Хотя большинство обходных решений довольно простые, необходимость добавления базовой поддержки обновленного Date and Time API давно назревала. JPA поддержка Date and Time API включает в себя следующие типы:
Для лучшего понимания, сначала объясню, как поддержка Date and Time API работает без JPA 2.2. JPA 2.1 может работать только с более старыми конструктами дат, такими как
Listing 1
В JPA 2.2 больше нет необходимости писать такой конвертер, так как вы используете поддерживаемые типы даты-времени. Поддержка таких типов встроена, поэтому вы можете просто уточнить поддерживаемый тип в поле класса сущности без дополнительного кода. Отрывок кода, приведенный ниже, демонстрирует эту концепцию. Заметьте, нет необходимости добавлять в код
Так как поддерживаемые типы даты-времени — объекты первого класса в JPA, их можно указывать без дополнительных церемоний. В JPA 2.1
Стоит заметить, что только часть типов дата-время поддерживается в этой версии, но конвертер атрибутов может быть легко сгенерирован для работы и с другими типами, например для преобразования
Код в Listing 2 показывает, как конвертировать время из
Listing 2
Конкретно этот пример очень прямолинеен, потому что
Внедряемые Конвертеры Атрибутов
Конвертеры атрибутов были очень приятным дополнением в JPA 2.1, так как они позволяли типам атрибутов быть более гибкими. Обновление JPA 2.2 добавляет полезную возможность делать конвертеры атрибутов внедряемыми. Это значит, что вы можете внедрять ресурсы Contexts и Dependency Injection (CDI) прямо в конвертер атрибутов. Эта модификация согласуется с другими улучшениями CDI в спецификациях Java EE 8, например с усовершенствованными JSF конвертерами, так как теперь они тоже могут использовать CDI внедрение.
Чтобы воспользоваться этой новой функцией, просто внедрите CDI ресурсы в конвертер атрибутов, по мере необходимости. В Listing 2 приводится пример конвертера атрибутов, и сейчас я разберу его, пояснив все важные детали.
Класс конвертера должен имплементировать интерфейс
Чтобы автоматически применять конвертер каждый раз, когда используется указанный тип данных, добавьте “automatic”, как в
Конвертер может также быть применен на уровне класса:
Предположим, я хочу зашифровать значения, содержащиеся в поле
Listing 3
В этом коде процесс выполняется путем внедрения класса
Стриминг Результатов Выполнения Запросов
Теперь можно с легкостью в полной мере использовать возможности функций потоков (streams) Java SE 8 при работе с результатами выполнения запросов. Потоки не только упрощают чтение, запись и поддержку кода, но и помогают улучшить работу запросов в некоторых ситуациях. Некоторые реализации потоков также помогают избежать чрезмерно большого одновременного количества запросов к данным, хотя в некоторых случаях использование
Чтобы включить эту функцию, был добавлен метод
Провайдеры персистентности могут решить переопределить новый метод
Помимо производительности, возможность стримить результаты — приятное дополнение в JPA, которое обеспечивает удобный способ работы с данными. Я продемонстрирую пару сценариев, где это может пригодиться, но сами возможности безграничны. В обоих сценариях, я запрашиваю сущность
Этот метод можно немного изменить, чтобы он возвращал список результатов, используя метод
В следующем сценарии, показанном ниже, я нахожу
Как я упоминал раньше, важно помнить о производительности в сценариях, где возвращаются большие объемы данных. Существуют условия, в которых потоки оказываются полезней в запрашивании баз данных, но также существуют и те, где они могут вызвать ухудшение производительности. Хорошее правило состоит в том, что если данные могут запрашиваться в рамках SQL-запроса, имеет смысл поступить именно так. Иногда преимущества использования элегантного синтаксиса потоков не перевешивают лучшую производительность, которой можно добиться, используя стандартную фильтрацию SQL.
Поддержка Повторяющихся Аннотаций
Когда была выпущен а Java SE 8, повторяющиеся аннотации стали возможны, позволяя использовать аннотацию в декларации повторно. Некоторые ситуации требуют использования одной и той же аннотации на классе или поле несколько раз. Например, может быть более одной
Работает это следующим образом: реализация класса аннотации должна быть помечена мета-аннотацией
Приведем пример. Если вы хотите добавить более одной аннотации
Listing 4
Однако в JPA 2.2 все иначе. Так как
Listing 5
Список повторяющихся аннотаций:
Заключение
Версия JPA 2.2 немного изменений, но включенные в нее улучшения являются значительными. Наконец, JPA приводят в соответветствие с Java SE 8, позволяя разработчикам использовать такие функции, как Date and Time API, стриминг результатов запросов и повторяющиеся аннотации. Этот релиз также улучшает согласованность с CDI, добавляя возможность внедрения ресурсов CDI в конвертеры атрибутов. Сейчас JPA 2.2 доступна и является частью Java EE 8, думаю, вам понравится ее использовать.
THE END
Как всегда ждём вопросы и комментарии.
Так уж внезапно получилось, что старт второй группы «Разработчик Java Enterprise» совпал с 256-м днём в году.
Ну и делимся предпоследней интересностью: что же нового привнёс JPA 2.2 — cтриминг результатов, улучшенное преобразование даты, новые аннотации — лишь несколько примеров полезных улучшений.
Поехали!
Java Persistence API (JPA) — основополагающая спецификация Java EE, которая широко используется в индустрии. Независимо от того, разрабатываете вы для платформы Java EE или для альтернативного фреймворка Java, JPA — ваш выбор для сохранения данных. JPA 2.1 улучшили спецификацию, позволив разработчикам решать такие задачи, как автоматическая генерация схемы базы данных и эффективная работа с процедурами, хранящимися в базе данных. Последняя версия, JPA 2.2, улучшает спецификацию на основе этих изменений.
В этой статье я расскажу о новом функционале и приведу примеры, которые помогут начать с ним работать. В качестве образца я использую проект “Java EE 8 Playground”, который есть на GitHub. Пример приложения основан на спецификации Java EE 8 и использует фреймворк JavaServer Faces (JSF), Enterprise JavaBeans (EJB) и JPA для персистентности. Чтобы понять, о чем речь, вы должны быть знакомы с JPA.

Использование JPA 2.2
Версия JPA 2.2 — часть платформы Java EE 8. Стоит отметить, что только Java EE 8 совместимые серверы приложений предоставляют спецификацию, готовую к использованию out of the box. В момент написания этой статьи (конец 2017 года), таких серверов приложений было довольно мало. Тем не менее, пользоваться JPA 2.2 при использовании Java EE7 — легко. Сначала необходимо скачать соответствующие JAR файлы с помощью Maven Central и добавить их в проект. Если вы используете Maven в своем проекте, добавьте координаты в файл POM Maven:
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
Затем, выберете имплементацию JPA, которую хотите использовать. Начиная с версии JPA 2.2 и EclipseLink, и Hibernate имеют совместимые реализации. В качестве примеров в этой статье, я использую EclipseLink, добавляя следующую зависимость:
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.7.0 </version>
</dependency>
Если вы используете Java EE 8 совместимый сервер, например GlassFish 5 или Payara 5, то должны иметь возможность уточнить область “provided” для этих зависимостей в файле POM. Иначе, укажите область “compile”, чтобы включить их в сборку проекта.
Поддержка Даты и Времени Java 8
Возможно, одним из наиболее положительно встреченных дополнений является поддержка Java 8 Date and Time API. С момента релиза Java SE 8 в 2014 году, разработчики пользовались обходными путями, чтобы использовать Date and Time API с JPA. Хотя большинство обходных решений довольно простые, необходимость добавления базовой поддержки обновленного Date and Time API давно назревала. JPA поддержка Date and Time API включает в себя следующие типы:
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.OffsetTime
java.time.OffsetDateTime
Для лучшего понимания, сначала объясню, как поддержка Date and Time API работает без JPA 2.2. JPA 2.1 может работать только с более старыми конструктами дат, такими как
java.util.Date
и java.sql.Timestamp
. Поэтому необходимо использовать конвертер для преобразования даты, хранимой в базе данных, в старую конструкцию, которая поддерживается версией JPA 2.1, а затем конвертировать в обновленную Date and Time API для использования в приложении. Конвертер даты в JPA 2.1, способный на такое преобразование, может выглядеть примерно так, как показано в Listing 1. Конвертер в нем используется для преобразования между LocalDate
и java.util.Date
. Listing 1
@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDate, Date> {
@Override
public Date convertToDatabaseColumn(LocalDate entityValue) {
LocalTime time = LocalTime.now();
Instant instant = time.atDate(entityValue)
.atZone(ZoneId.systemDefault())
.toInstant();
return Date.from(instant);
}
@Override
public LocalDate convertToEntityAttribute(Date databaseValue){
Instant instant = Instant.ofEpochMilli(databaseValue.getTime());
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalDate();
}
}
В JPA 2.2 больше нет необходимости писать такой конвертер, так как вы используете поддерживаемые типы даты-времени. Поддержка таких типов встроена, поэтому вы можете просто уточнить поддерживаемый тип в поле класса сущности без дополнительного кода. Отрывок кода, приведенный ниже, демонстрирует эту концепцию. Заметьте, нет необходимости добавлять в код
@Temporal
аннотацию, потому что маппинг типов происходит автоматически. public class Job implements Serializable {
. . .
@Column(name = "WORK_DATE")
private LocalDate workDate;
. . .
}
Так как поддерживаемые типы даты-времени — объекты первого класса в JPA, их можно указывать без дополнительных церемоний. В JPA 2.1
@Temporal
аннотация должна быть описана во всех постоянных поля и свойствах типа java.util.Date
и java.util.Calendar
.Стоит заметить, что только часть типов дата-время поддерживается в этой версии, но конвертер атрибутов может быть легко сгенерирован для работы и с другими типами, например для преобразования
LocalDateTime
в ZonedDateTime
. Самая большая проблема в написании такого конвертера — определить, каким образом лучше проводить преобразование между разными типами. Чтобы сделать все еще проще, конвертеры атрибутов теперь можно внедрять. Я приведу пример внедрения ниже.Код в Listing 2 показывает, как конвертировать время из
LocalDateTime
в ZonedDateTime
.Listing 2
@Converter
public class LocalToZonedConverter implements AttributeConverter<ZonedDateTime, LocalDateTime> {
@Override
public LocalDateTime convertToDatabaseColumn(ZonedDateTime entityValue) {
return entityValue.toLocalDateTime();
}
@Override
public ZonedDateTime convertToEntityAttribute(LocalDateTime databaseValue) {
return ZonedDateTime.of(databaseValue, ZoneId.systemDefault());
}
}
Конкретно этот пример очень прямолинеен, потому что
ZonedDateTime
содержит методы, простые для преобразования. Конвертация происходит путем вызова toLocalDateTime()
метода. Обратное преобразование может быть выполнено с помощью вызова метода ZonedDateTimeOf()
и передачи значения LocalDateTime
вместе с ZoneId
для использования часовым поясом. Внедряемые Конвертеры Атрибутов
Конвертеры атрибутов были очень приятным дополнением в JPA 2.1, так как они позволяли типам атрибутов быть более гибкими. Обновление JPA 2.2 добавляет полезную возможность делать конвертеры атрибутов внедряемыми. Это значит, что вы можете внедрять ресурсы Contexts и Dependency Injection (CDI) прямо в конвертер атрибутов. Эта модификация согласуется с другими улучшениями CDI в спецификациях Java EE 8, например с усовершенствованными JSF конвертерами, так как теперь они тоже могут использовать CDI внедрение.
Чтобы воспользоваться этой новой функцией, просто внедрите CDI ресурсы в конвертер атрибутов, по мере необходимости. В Listing 2 приводится пример конвертера атрибутов, и сейчас я разберу его, пояснив все важные детали.
Класс конвертера должен имплементировать интерфейс
javax.persistence.AttributeConverter
, передавая значения X и Y. Значение X соответствует типу данных в объекте Java, а значение Y должно соответствовать типу столбца базы данных. Затем, класс конвертера должен быть аннотирован @Converter
. И наконец, класс должен переопределить методы convertToDatabaseColumn()
и convertToEntityAttribute()
. Реализация в каждом из этих методов должна конвертировать значения из определенных типов и обратно в них.Чтобы автоматически применять конвертер каждый раз, когда используется указанный тип данных, добавьте “automatic”, как в
@Converter(autoApply=true)
. Чтобы применить конвертер к одному атрибуту, используйте аннотацию @Converter на уровне атрибута, как показано здесь:@Convert(converter=LocalDateConverter.java)
private LocalDate workDate;
Конвертер может также быть применен на уровне класса:
@Convert(attributeName="workDate",
converter = LocalDateConverter.class)
public class Job implements Serializable {
. . .
Предположим, я хочу зашифровать значения, содержащиеся в поле
creditLimit
сущности Customer
при его сохранении. Чтобы реализовать такой процесс, значения должны быть зашифрованы до того, как будут сохранены, и расшифрованы после извлечения из базы данных. Это может быть сделано конвертером и, используя JPA 2.2, я могу внедрить объект шифрования в конвертер для достижения желаемого результата. В Listing 3 приведен пример.Listing 3
@Converter
public class CreditLimitConverter implements AttributeConverter<BigDecimal, BigDecimal> {
@Inject
CreditLimitEncryptor encryptor;
@Override
public BigDecimal convertToDatabaseColumn
(BigDecimal entityValue) {
String encryptedFormat = encryptor.base64encode(entityValue.toString());
return BigDecimal.valueOf(Long.valueOf(encryptedFormat));
}
...
}
В этом коде процесс выполняется путем внедрения класса
CreditLimitEncryptor
в конвертер и его последующего использования для помощи процессу. Стриминг Результатов Выполнения Запросов
Теперь можно с легкостью в полной мере использовать возможности функций потоков (streams) Java SE 8 при работе с результатами выполнения запросов. Потоки не только упрощают чтение, запись и поддержку кода, но и помогают улучшить работу запросов в некоторых ситуациях. Некоторые реализации потоков также помогают избежать чрезмерно большого одновременного количества запросов к данным, хотя в некоторых случаях использование
ResultSet
пагинации может сработать лучше, чем потоки.Чтобы включить эту функцию, был добавлен метод
getResultStream()
к интерфейсам Query
и TypedQuery
. Это незначительное изменение позволяет JPA просто возвращать поток результатов вместо списка. Таким образом, если вы работаете с большим ResultSet
, имеет смысл сравнить производительность между новой имплементацией потоков и прокручиваемым ResultSets
или пагинацией. Причина в том, что реализации потоков извлекают все записи одновременно, сохраняют их в список и затем возвращают. Прокручиваемый ResultSet
и техника пагинации извлекают данные по частям, что может быть лучше для больших наборов данных.Провайдеры персистентности могут решить переопределить новый метод
getResultStream()
улучшенной имплемен��ацией. Hibernate уже включает метод stream(), который использует прокручиваемый ResultSet
для парсинга результатов записей вместо их полного возврата. Это позволяет Hibernate работать с очень большими наборами данных и делать это хорошо. Можно ожидать, что и другие провайдеры переопределят этот метод, чтобы предоставить похожие функции, выгодные для JPA.Помимо производительности, возможность стримить результаты — приятное дополнение в JPA, которое обеспечивает удобный способ работы с данными. Я продемонстрирую пару сценариев, где это может пригодиться, но сами возможности безграничны. В обоих сценариях, я запрашиваю сущность
Job
и возвращаю поток. Во-первых, посмотрим на следующий код, где я просто анализирую поток Jobs
по определенному Customer
, вызывая метод интерфейса Query
getResultStream()
. Затем, я используют этот поток для вывода деталей касательно customer
и work date
Job’a.public void findByCustomer(PoolCustomer customer){
Stream<Job> jobList = em.createQuery("select object(o) from Job o " +
"where o.customer = :customer")
.setParameter("customer", customer)
.getResultStream();
jobList.map(j -> j.getCustomerId() +
" ordered job " + j.getId()
+ " - Starting " + j.getWorkDate())
.forEach(jm -> System.out.println(jm));
}
Этот метод можно немного изменить, чтобы он возвращал список результатов, используя метод
Collectors .toList()
следующим образом.public List<Job> findByCustomer(PoolCustomer customer){
Stream<Job> jobList = em.createQuery(
"select object(o) from Job o " +
"where o.customerId = :customer")
.setParameter("customer", customer)
.getResultStream();
return jobList.collect(Collectors.toList());
}
В следующем сценарии, показанном ниже, я нахожу
List
задач, относящихся к пулам определенной формы. В этом случае, я возвращаю все задачи, совпадающие с формой, переданной в виде строки. Аналогично первому примеру, сначала я возвращаю поток записей Jobs
. Затем, я фильтрую записи на основе формы пула customer. Как видим, полученный код очень компактный и легко читаемый. public List<Job> findByCustPoolShape(String poolShape){
Stream<Job> jobstream = em.createQuery(
"select object(o) from Job o")
.getResultStream();
return jobstream.filter(
c -> poolShape.equals(c.getCustomerId().getPoolId().getShape()))
.collect(Collectors.toList());
}
Как я упоминал раньше, важно помнить о производительности в сценариях, где возвращаются большие объемы данных. Существуют условия, в которых потоки оказываются полезней в запрашивании баз данных, но также существуют и те, где они могут вызвать ухудшение производительности. Хорошее правило состоит в том, что если данные могут запрашиваться в рамках SQL-запроса, имеет смысл поступить именно так. Иногда преимущества использования элегантного синтаксиса потоков не перевешивают лучшую производительность, которой можно добиться, используя стандартную фильтрацию SQL.
Поддержка Повторяющихся Аннотаций
Когда была выпущен а Java SE 8, повторяющиеся аннотации стали возможны, позволяя использовать аннотацию в декларации повторно. Некоторые ситуации требуют использования одной и той же аннотации на классе или поле несколько раз. Например, может быть более одной
@SqlResultSetMapping
аннотации для данного класса сущности. В таких ситуациях, когда требуется поддержка повторной аннотации, должна использоваться контейнерная аннотация. Повторяющиеся аннотации не только уменьшают требование заворачивать коллекции одинаковых аннотаций в контейнерную аннотацию, но и могут облегчить чтение кода.Работает это следующим образом: реализация класса аннотации должна быть помечена мета-аннотацией
@Repeatable
, чтобы показывать, что она может использоваться более одного раза. Мета-аннотация @Repeatable
принимает тип класса контейнерной аннотации. Например, класс аннотации NamedQuery
теперь помечен @Repeatable(NamedQueries.class)
аннотацией. В таком случае, контейнерная аннотация все еще используется, но вам не придется думать об этом при использовании той же аннотации на декларации или классе, потому что @Repeatable
абстрагирует эту деталь. Приведем пример. Если вы хотите добавить более одной аннотации
@NamedQuery
к классу сущности в JPA 2.1, вам нужно инкапсулировать их внутри аннотации @NamedQueries
, как показано в Listing 4.Listing 4
@Entity
@Table(name = "CUSTOMER")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Customer.findAll",
query = "SELECT c FROM Customer c")
, @NamedQuery(name = "Customer.findByCustomerId",
query = "SELECT c FROM Customer c "
+ "WHERE c.customerId = :customerId")
, @NamedQuery(name = "Customer.findByName",
query = "SELECT c FROM Customer c "
+ "WHERE c.name = :name")
. . .)})
public class Customer implements Serializable {
. . .
}
Однако в JPA 2.2 все иначе. Так как
@NamedQuery
является повторяющейся аннотацией, она может указываться в классе сущности более одного раза, как показано в Listing 5.Listing 5
@Entity
@Table(name = "CUSTOMER")
@XmlRootElement
@NamedQuery(name = "Customer.findAll",
query = "SELECT c FROM Customer c")
@NamedQuery(name = "Customer.findByCustomerId",
query = "SELECT c FROM Customer c "
+ "WHERE c.customerId = :customerId")
@NamedQuery(name = "Customer.findByName",
query = "SELECT c FROM Customer c "
+ "WHERE c.name = :name")
. . .
public class Customer implements Serializable {
. . .
}
Список повторяющихся аннотаций:
@AssociationOverride
@AttributeOverride
@Convert
@JoinColumn
@MapKeyJoinColumn
@NamedEntityGraphy
@NamedNativeQuery
@NamedQuery
@NamedStoredProcedureQuery
@PersistenceContext
@PersistenceUnit
@PrimaryKeyJoinColumn
@SecondaryTable
@SqlResultSetMapping
Заключение
Версия JPA 2.2 немного изменений, но включенные в нее улучшения являются значительными. Наконец, JPA приводят в соответветствие с Java SE 8, позволяя разработчикам использовать такие функции, как Date and Time API, стриминг результатов запросов и повторяющиеся аннотации. Этот релиз также улучшает согласованность с CDI, добавляя возможность внедрения ресурсов CDI в конвертеры атрибутов. Сейчас JPA 2.2 доступна и является частью Java EE 8, думаю, вам понравится ее использовать.
THE END
Как всегда ждём вопросы и комментарии.