Статьи

Получение данных с помощью ORM очень просто!

Вступление

Практически любая система каким-то образом работает с внешними хранилищами данных. В большинстве случаев это реляционная база данных, и очень часто выборка данных делегируется некоторой реализации ORM. ORM охватывает много рутины и приносит взамен несколько новых абстракций.

Мартин Фаулер написал интересную статью об ORM и высказал одну из ключевых мыслей: «ORM помогают нам решать очень реальную проблему для большинства корпоративных приложений. … Это не красивые инструменты, но и проблема, с которой они сталкиваются, тоже не очень приятная. Я думаю, что они заслуживают немного большего уважения и большего понимания ».

В рамках CUBA мы очень интенсивно используем ORM и много знаем о его ограничениях, поскольку у нас есть различные проекты по всему миру. Есть много вещей, которые можно обсудить, но мы сосредоточимся на одном из них: ленивый против страстного извлечения данных. Мы поговорим о различных подходах к извлечению данных (в основном в JPA API и Spring), о том, как мы справляемся с ними в CUBA и о том, как мы работаем с RnD, чтобы улучшить уровень ORM в CUBA. Мы рассмотрим основы, которые могут помочь разработчикам не сталкиваться с проблемами с ужасной производительностью при использовании ORM.

Извлечение данных: ленивый или нетерпеливый путь?

Если ваша модель данных содержит только одну сущность, проблем с использованием ORM не возникнет. Давайте посмотрим на пример. У нас есть пользователь с идентификатором и именем:

1
2
3
4
5
6
7
8
public class User {
   @Id
   @GeneratedValue
   private int id;
   private String name;
 
   //Getters and Setters here
}

Чтобы получить его, нам просто нужно красиво спросить EntityManager:

1
2
EntityManager em = entityManagerFactory.createEntityManager();
User user = em.find(User.class, id);

Вещи становятся интересными, когда мы имеем отношение один-ко-многим между сущностями:

01
02
03
04
05
06
07
08
09
10
public class User {
   @Id
   @GeneratedValue
   private int id;
   private String name;
   @OneToMany
   private List<Address> addresses;
 
   //Getters and Setters here
}

Если мы хотим получить пользовательскую запись из базы данных, возникает вопрос: «Должны ли мы получить адрес тоже?». И «правильный» ответ будет: «Это зависит». В некоторых случаях нам может понадобиться адрес, а в некоторых — нет. Обычно ORM предоставляет две опции для извлечения данных: ленивый и нетерпеливый. Большинство из них по умолчанию устанавливают режим отложенной выборки. И когда мы пишем следующий код:

1
2
3
4
EntityManager em = entityManagerFactory.createEntityManager();
User user = em.find(User.class, 1);
em.close();
System.out.println(user.getAddresses().get(0));

Мы получаем так называемое “LazyInitException” которое сильно смущает новичков ORM. И здесь нам нужно объяснить концепцию «Присоединенных» и «Отдельных» объектов, а также рассказать о сеансах и транзакциях базы данных.

Хорошо, тогда экземпляр сущности должен быть присоединен к сеансу, чтобы мы могли получать подробные атрибуты. В этом случае у нас возникла другая проблема — транзакции становятся длиннее, поэтому риск возникновения тупика возрастает. А разделение нашего кода на цепочку коротких транзакций может привести к «гибели миллионов комаров» для базы данных из-за увеличения числа очень коротких отдельных запросов.

Как уже было сказано, вам может понадобиться или не потребоваться получение атрибута Адреса, поэтому вам нужно «касаться» коллекции только в некоторых случаях, добавляя больше условий. Хммм …. Похоже, это становится все сложнее.

Хорошо, поможет другой тип извлечения?

01
02
03
04
05
06
07
08
09
10
public class User {
   @Id
   @GeneratedValue
   private int id;
   private String name;
   @OneToMany(fetch = FetchType.EAGER)
   private List<Address> addresses;
 
   //Getters and Setters here
}

Ну, не совсем так. Мы избавимся от раздражающего отложенного исключения init и не должны проверять, подключен или отключен экземпляр. Но у нас есть проблемы с производительностью, потому что, опять же, нам не нужны адреса для всех случаев, но выбирайте их всегда. Есть еще идеи?

Spring JDBC

Некоторые разработчики настолько раздражаются ORM, что переключаются на «полуавтоматические» отображения с помощью Spring JDBC. В этом случае мы создаем уникальные запросы для уникальных вариантов использования и возвращаем объекты, которые содержат атрибуты, действительные только для конкретного варианта использования.

Это дает нам большую гибкость. Мы можем получить только один атрибут:

1
2
3
String name = this.jdbcTemplate.queryForObject(
       "select name from t_user where id = ?",
       new Object[]{1L}, String.class);

Или весь объект:

01
02
03
04
05
06
07
08
09
10
11
User user = this.jdbcTemplate.queryForObject(
       "select id, name from t_user where id = ?",
       new Object[]{1L},
       new RowMapper<User>() {
           public User mapRow(ResultSet rs, int rowNum) throws SQLException {
               User user = new User();
               user.setName(rs.getString("name"));
               user.setId(rs.getInt("id"));
               return user;
           }
       });

Вы также можете получать адреса, используя ResultSetExtractor, но это требует написания дополнительного кода, и вы должны знать, как писать объединения SQL, чтобы избежать проблемы выбора n + 1 .

Ну, это снова становится сложным. Вы контролируете все запросы и управляете отображением, но вам нужно написать больше кода, изучить SQL и узнать, как выполняются запросы к базе данных. Хотя я думаю, что знание основ SQL является необходимым навыком почти для каждого разработчика, некоторые из них так не считают, и я не собираюсь с ними спорить. Знание ассемблера x86 не является жизненно важным навыком для всех в наше время. Давайте просто подумаем о том, как мы можем упростить разработку.

JPA EntityGraph

Давайте сделаем шаг назад и попытаемся понять, чего мы собираемся достичь? Кажется, что все, что нам нужно сделать, это точно сказать, какие атрибуты мы собираемся получить в разных случаях использования. Давай сделаем это тогда! В JPA 2.1 появился новый API — Entity Graph. Идея этого API проста — вы просто пишете несколько аннотаций, описывающих то, что следует извлечь. Давайте посмотрим на пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Entity
@NamedEntityGraphs({
       @NamedEntityGraph(name = "user-only-entity-graph"),
       @NamedEntityGraph(name = "user-addresses-entity-graph",
               attributeNodes = {@NamedAttributeNode("addresses")})
       })
public class User {
   @Id
   @GeneratedValue
   private int id;
   private String name;
   @OneToMany(fetch = FetchType.LAZY)
   private Set<Address> addresses;
 
   //Getters and Setters here
 
}

Для этой сущности мы описали два графа user-only-entity-graph не выбирает атрибут Addresses (который помечен как ленивый), в то время как второй граф инструктирует ORM выбирать адреса. Если мы пометим атрибут как нетерпеливый, настройки графа сущности будут проигнорированы, и атрибут будет выбран.

Итак, начиная с JPA 2.1, вы можете выбирать сущности следующим образом:

1
2
3
4
5
EntityManager em = entityManagerFactory.createEntityManager();
EntityGraph graph = em.getEntityGraph("user-addresses-entity-graph");
Map<String, Object> properties = Map.of("javax.persistence.fetchgraph", graph);
User user = em.find(User.class, 1, properties);
em.close();

Такой подход значительно упрощает работу разработчика, нет необходимости «трогать» ленивые атрибуты и создавать длинные транзакции. Самое замечательное в том, что граф сущностей можно применять на уровне генерации SQL, поэтому из базы данных приложения Java не извлекаются дополнительные данные. Но проблема все еще есть. Мы не можем сказать, какие атрибуты были получены, а какие нет. Для этого есть API, вы можете проверить атрибуты с помощью класса PersistenceUnit :

1
2
PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil();
System.out.println("User.addresses loaded: " + pu.isLoaded(user, "addresses"));

Но это довольно скучно. Можем ли мы упростить это и просто не показывать необработанные атрибуты?

Весенние прогнозы

Spring Framework предоставляет фантастическое средство под названием « Проекции» (и оно отличается от проекций Hibernate). Если мы хотим получить только некоторые свойства объекта, мы можем указать интерфейс, и Spring выберет «экземпляры» интерфейса из базы данных. Давайте посмотрим на пример. Если мы определим следующий интерфейс:

1
2
3
interface NamesOnly {
   String getName();
}

А затем определите репозиторий Spring JPA для извлечения наших пользовательских сущностей:

1
2
3
interface UserRepository extends CrudRepository<User, Integer> {
   Collection<NamesOnly> findByName(String lastname);
}

В этом случае после вызова метода findByName мы просто не сможем получить доступ к необработанным атрибутам! Тот же принцип применим и к детальным классам сущностей. Таким образом, вы можете получить как основные, так и подробные записи таким образом. Более того, в большинстве случаев Spring генерирует «правильный» SQL и выбирает только атрибуты, указанные в проекции, то есть проекции работают подобно описаниям графа сущностей.

Это очень мощная концепция, вы можете использовать выражения SpEL, использовать классы вместо интерфейсов и т. Д. В документации есть дополнительная информация, которую вы можете проверить, если вам интересно.

Единственная проблема с проекциями заключается в том, что под капотом они реализованы в виде карт и, следовательно, доступны только для чтения. Следовательно, если вы можете определить метод установки для проекции, вы не сможете сохранить изменения, не используя ни хранилища CRUD, ни EntityManager. Вы можете рассматривать проекции как DTO, и вы должны написать свой собственный код преобразования DTO в сущность.

CUBA Реализация

С самого начала разработки фреймворка CUBA мы пытались оптимизировать код, который работает с базой данных. В рамках мы используем EclipseLink для реализации API уровня доступа к данным. Хорошая особенность EclipseLink — он с самого начала поддерживал частичную загрузку сущностей, поэтому мы выбрали его вместо Hibernate. В этом ORM вы можете указать, какие именно атрибуты должны быть загружены до того, как JPA 2.1 станет стандартом. Поэтому мы добавили нашу внутреннюю концепцию, похожую на Entity Graph, в нашу платформу — CUBA Views . Представления довольно мощные — вы можете расширять их, комбинировать и т. Д. Вторая причина создания CUBA Views — мы хотели использовать короткие транзакции и сосредоточиться на работе в основном с отсоединенными объектами, в противном случае мы не могли бы сделать богатый веб-интерфейс быстрым и отзывчивым. ,

В CUBA view описания хранятся в XML-файле и выглядят так:

1
2
3
4
5
6
7
8
<view class="com.sample.User"
     extends="_local"
     name="user-minimal-view">
   <property name="name"/>
   <property name="addresses"
             view="address-street-only-view"/>
   </property>
</view>

Это представление инструктирует CUBA DataManager выбирать сущность пользователя с ее локальным атрибутом имени и выбирать адреса, применяя просмотр адресов только улиц, одновременно выбирая их (важно!) На уровне запроса. Когда представление определено, вы можете применить его для получения сущностей, используя класс DataManager:

1
List<User> users = dataManager.load(User.class).view("user-edit-view").list();

Он работает как чудо и экономит большой сетевой трафик, не загружая неиспользуемые атрибуты, но, как и в JPA Entity Graph, существует небольшая проблема: мы не можем сказать, какие атрибуты объекта User были загружены. И в CUBA у нас есть раздражающее “IllegalStateException: Cannot get unfetched attribute [...] from detached object” . Как и в JPA, вы можете проверить, был ли атрибут извлечен, но написание этих проверок для каждой извлекаемой сущности — скучная работа, и разработчики не довольны ею.

CUBA View Interfaces PoC

А что если бы мы могли взять лучшее из двух миров? Мы решили реализовать так называемые сущностные интерфейсы, которые используют подход Spring, но эти интерфейсы преобразуются в представления CUBA во время запуска приложения и затем могут использоваться в DataManager. Идея довольно проста: вы определяете интерфейс (или набор интерфейсов), которые определяют граф сущностей. Он выглядит как Spring Projection и работает как Entity Graph:

01
02
03
04
05
06
07
08
09
10
interface UserMinimalView extends BaseEntityView<User, Integer> {
   String getName();
   void setName(String val);
   List<AddressStreetOnly> getAddresses();
 
   interface AddressStreetOnly extends BaseEntityView<Address, Integer> {
      String getStreet();
      void setStreet(String street);
   }
}

Обратите внимание, что интерфейс AddressStreetOnly может быть вложенным, если он используется только в одном случае.

Во время запуска приложения CUBA (фактически это в основном Spring Context Initialization), мы создаем программное представление для представлений CUBA и сохраняем их во внутреннем компоненте репозитория в контексте Spring.

После этого нам нужно настроить DataManager, чтобы он мог принимать имена классов в дополнение к именам строк в CUBA View, а затем мы просто передали интерфейсный класс:

1
List<User> users = dataManager.loadWithView(UserMinimalView.class).list();

Мы генерируем прокси, реализующие представление сущностей для каждого экземпляра, извлеченного из базы данных, как это делает Hibernate. И когда вы пытаетесь получить значение атрибута, прокси перенаправляет вызов реальной сущности.

С помощью этой реализации мы пытаемся убить двух зайцев одним выстрелом:

  • Данные, которые не указаны в интерфейсе, не загружаются в код приложения Java, что экономит ресурсы сервера.
  • Разработчик использует только те свойства, которые были получены, поэтому больше нет ошибок «UnfetchedAttribute» (иначе LazyInitException в Hibernate).

В отличие от Spring Projection, Entity Views оборачивают сущности и реализуют интерфейс Entity CUBA, поэтому их можно рассматривать как сущности: вы можете обновить свойство и сохранить изменения в базе данных.

«Третья птица» здесь — вы можете определить интерфейс «только для чтения», который содержит только геттеры, полностью предотвращая изменения сущностей на уровне API.

Кроме того, мы можем реализовать некоторые операции над отсоединенной сущностью, такие как преобразование имени этого пользователя в нижний регистр

1
2
3
4
@MetaProperty
default String getNameLowercase() {
   return getName().toLowerCase();
}

В этом случае все вычисляемые атрибуты могут быть перемещены из модели сущностей, поэтому вы не смешиваете логику выборки данных с бизнес-логикой, специфичной для конкретного случая.

Еще одна интересная возможность — вы можете наследовать интерфейсы. Это дает вам возможность подготовить несколько представлений с различным набором атрибутов, а затем смешать их при необходимости. Например, у вас может быть один интерфейс, который содержит имя пользователя и адрес электронной почты, а другой — имя и адреса. И если вам нужен интерфейс третьего представления, который должен содержать имя, адрес электронной почты и адреса, вы можете сделать это, просто комбинируя оба — благодаря множественному наследованию интерфейсов в Java. Обратите внимание, что вы можете передать этот третий интерфейс методам, которые используют первый или второй интерфейс, принципы ООП работают здесь как обычно.

Мы также реализовали преобразование сущностей между представлениями — у каждого представления сущности есть метод reload (), который принимает другой класс представления в качестве параметра:

1
UserFullView userFull = userMinimal.reload(UserFullView.class);

UserFullView может содержать дополнительные атрибуты, поэтому объект будет перезагружен из базы данных. А перезагрузка сущности — это ленивый процесс, он будет выполняться только при попытке получить значение атрибута сущности. Мы сделали это специально, потому что в CUBA у нас есть «веб» модуль, который отображает богатый пользовательский интерфейс и может содержать пользовательские контроллеры REST. В этом модуле мы используем те же сущности, и его можно развернуть на отдельном сервере. Поэтому каждая перезагрузка объекта вызывает дополнительный запрос к базе данных через основной модуль (промежуточное программное обеспечение). Итак, вводя ленивую перезагрузку сущностей, мы экономим некоторый сетевой трафик и запросы к базе данных.

PoC можно скачать с GitHub — не стесняйтесь играть с ним.

Вывод

ORM будут массово использоваться в корпоративных приложениях в ближайшем будущем. Мы просто должны предоставить что-то, что преобразует строки базы данных в объекты Java. Конечно, в сложных приложениях с высокой нагрузкой мы продолжим видеть уникальные решения, но ORM будет жить столько же, сколько RDBMS.

В рамках CUBA мы пытаемся упростить использование ORM, чтобы сделать его максимально безболезненным для разработчиков. И в следующих версиях мы собираемся внести больше изменений. Я не уверен, будут ли это интерфейсы просмотра или что-то еще, но я уверен в одном — работа с ORM в следующей версии с CUBA будет упрощена.

Опубликовано на Java Code Geeks с разрешения Андрея Беляева, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Загрузка данных с помощью ORM очень проста! Это?

Мнения, высказанные участниками Java Code Geeks, являются их собственными.