Всех с праздником!

Так уж внезапно получилось, что старт второй группы «Разработчик 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

Как всегда ждём вопросы и комментарии.