Так уж внезапно получилось, что старт второй группы «Разработчик 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
at tributeoverride
convert
joincolumn
markeyjoincolumn
namedentit ygraph
namednativequery
namedquery
namedstoredprocedurequery
persistenceconte xt
persistenceunit
primarykeyjoincolumn
secondary table
sqlresultse tmapping
Заключение
Версия JPA 2.2 немного изменений, но включенные в нее улучшения являются значительными. Наконец, JPA приводят в соответветствие с Java SE 8, позволяя разработчикам использовать такие функции, как Date and Time API, стриминг результатов запросов и повторяющиеся аннотации. Этот релиз также улучшает согласованность с CDI, добавляя возможность внедрения ресурсов CDI в конвертеры атрибутов. Сейчас JPA 2.2 доступна и является частью Java EE 8, думаю, вам понравится ее использовать.
THE END
Как всегда ждём вопросы и комментарии.
Комментарии (5)
NoRegrets
14.09.2018 01:12В первой функции findByCustomer(PoolCustomer customer) аргумент не используется, скорее всего вы там забыли .setParameter(«customer», customer). И job.getCustomerId().getCustomerId().getCustomerId() это ctrl-v залип видимо. Или я совсем уже старый и не понимаю новых трендов.
MaxRokatansky Автор
14.09.2018 12:19Да. Из пдф не очень удобно было переносить :(
Поправил всё, большое спасибо.
Throwable
14.09.2018 12:53Очень странно, что не сделали java.time.Instant, который есть натуральная замена java.sql.Timestamp. И вот еще очень интересно во что же мепится java.time.OffsetDateTime? Ведь очень немногие базы данных предоставляют временнЫе типы с зоной/смещением. Обычно когда нужно сохранять зону, делается дополнительное поле. Полагаю что OffsetDateTime конвертится в LocalDateTime, а затем сохраняется как обычное локальное время. При чтении, однако, оригинальная зона/смещение теряется.
MaxRokatansky Автор
14.09.2018 15:23Действительно кажется странным не включение поддержки Instant в спецификации JPA 2.2 (хотя изначально об этом также заявлялось). Более того, эту поддержку удалось реализовать в Hibernate 5.3. Что касается OffsetDateTime, то основная идея как раз и состояла от ухода в необходимости хранения информации о зоне в дополнительных полях и отображению данных посредством TIMESTAMP WITH TIME ZONE. И это в действительности удалось. Подобный вопрос затронут тут.
igor_suhorukov
Ура, стримы!!!