Spring JPA репозитории в CUBA

Аватар пользователя Andrey Belyaev

Предпосылки

Разработчики обычно не очень любят менять свои привычки (зачастую, в список привычек входят и фреймворки). Когда я начал работать с CUBA, мне не пришлось учить слишком много всего нового, активно включаться в работу над проектом можно было почти сразу. Но была одна вещь, над которой пришлось посидеть подольше — это была работа с данными.

В Spring есть несколько библиотек, которые можно использовать для работы с БД, одна из наиболее популярных — spring-data-jpa, которая позволяет в большинстве случаев не писать SQL или JPQL. Нужно всего лишь создать специальный интерфейс с методами, которые названы специальным образом и Spring сгенерирует и выполнит за вас всю оставшуюся часть работы по выборке данных из БД и созданию экземпляров объектов-сущностей.

Ниже представлен интерфейс, с методом для подсчета клиентов с заданной фамилией.

interface CustomerRepository extends CrudRepository<Customer, Long> {
  long countByLastName(String lastName);
}

Этот интерфейс можно напрямую использовать в Spring сервисах, не создавая никакой имплементации, что сильно ускоряет работу.

В CUBA есть API для работы с данными, который включает в себя различную функциональность вроде частично загружаемых сущностей или хитрую систему безопасности с контролем доступа к атрибутам сущностей и строкам в таблицах БД. Но этот API немного отличается от того, к чему привыкли разработчики в Spring Data или JPA/Hibernate.

Почему же в CUBA нет JPA репозиториев и можно ли их добавить?

Работа с данными в CUBA

В CUBA три основных класса, отвечающих за работу с данными: DataStore, EntityManager и DataManager.

DataStore — высокоуровневая абстракция для любого хранилища данных: БД, файловой системы или облачного хранилища. Этот API позволяет выполнять базовые операции над данными. В большинстве случаев разработчикам нет нужды работать с DataStore напрямую, кроме случаев разработки своего собственного хранилища, или если требуется какой-то очень специальный доступ к данным в хранилище.

EntityManager — копия всем хорошо известного JPA EntityManager. В отличие от стандартной имплементации, в нем есть специальные методы для работы с представлениями CUBA, для "мягкого"(логического) удаления данных, а также для работы с запросами в CUBA. Как и в случае с DataStore, в 90% проектов обычному разработчику не придется иметь дело с EntityManager, кроме случаев, когда нужно выполнять какие-то запросы в обход системы ограничения доступа к данным.

DataManager — основной класс для работы с данными в CUBA. Предоставляет API для манипулирования данными и поддерживает контроль доступа к данным, включая доступ к атрибутам и row-level ограничения. DataManager неявно модифицирует все запросы, которые выполняются в CUBA. Например, он может исключить поля таблицы, к которым у текущего пользователя нет доступа, из оператора select и добавить условия where для исключения строк таблиц из выборки. И это сильно облегчает жизнь разработчикам, потому что не надо думать, как правильно писать запросы с учетом прав доступа, CUBA это делает автоматически на основе данных из служебных таблиц БД.

Ниже — диаграмма взаимодействия компонентов CUBA, которые участвуют в выборке данных через DataManager.

При помощи DataManager можно относительно просто загружать сущности и целые иерархии сущностей, при помощи предствлений CUBA. В самом простом виде запрос выглядит так:

dataManager.load(Customer.class).list()

Как уже упоминалось, DataManager отфильтрует "логически удаленные" записи, уберет из запроса запрещенные атрибуты, а также откроет и закроет транзакцию автоматически.

Но, когда дело доходит до запросов посложнее, то в CUBA приходится писать JPQL.

Например, если нужно посчитать клиентов с заданной фамилией, как в примере из предыдущего раздела, то нужно написать примерно такой код:

public Long countByLastName(String lastName){
   return dataManager
           .loadValue("select count(c) from sample$Customer c where c.lastName = :lastName", Long.class)
           .parameter("lastName", lastName)
           .one();
}

или такой

public Long countByLastName(String lastName){
   LoadContext<Person> loadContext = LoadContext.create(Person.class);
   loadContext
      .setQueryString("select c from sample$Customer c where c.lastName = :lastName")
      .setParameter("lastName", lastName);
   return dataManager.getCount(loadContext);
}

В CUBA API нужно передавать JPQL выражение в виде строки (Criteria API ещё не поддерживается), это читаемый и понятный способ создания запросов, но отладка таких запросов может принести немало веселых минут. Кроме того, строки JPQL не верифицируются ни компилятором, ни Spring Framework во время инициализации контейнера, что приводит к возникновению ошибок только в Runtime.

Сравните это со Spring JPA:

interface CustomerRepository extends CrudRepository<Customer, Long> {
  long countByLastName(String lastName);
}

Код в три раза короче, и никаких строк. Вдобавок, название метода countByLastName проверяется во время инициализации Spring контейнера. Если допущена опечатка и вы написали countByLastNsme, то приложение вылетит с ошибкой во время деплоя:

Caused by: org.springframework.data.mapping.PropertyReferenceException: No property LastNome 
found for type Customer!

CUBA построена вокруг Spring Framework, так что в приложение, написанное с использованием CUBA, можно подключить библиотеку spring-data-jpa, но есть небольшая проблема — контроль доступа. Имплементация CrudRepository в Spring использует свой EntityManager. Таким образом, все запросы будут выполняться в обход DataManager. Таким образом, чтобы использовать JPA репозитории в CUBA, нужно заменить все вызовы EntityManager на вызовы DataManager и добавить поддержку CUBA представлений.

Кто-то может сказать, что spring-data-jpa — это такой неконтролируемый черный ящик и всегда предпочтительнее писать чистый JPQL или даже SQL. Это вечная проблема баланса между удобством и уровнем абстракции. Каждый выбирает тот способ, который ему больше по душе, но иметь в арсенале дополнительный способ работы с данными никогда не помешает. А тем, кому нужно больше управления, в Spring есть способ определить свой собственный запрос для методов JPA репозиториев.

Реализация

JPA репозитории реализованы в виде CUBA модуля, с использованием библиотеки spring-data-commons. Мы отказались от идеи модификации spring-data-jpa, потому что объем работы был бы сильно больше по сравнению с написанием собственного генератора запросов. Тем более, что spring-data-commons делает большую часть работы. Например, разбор имени метода и связывание имени с классами и свойствами полностью делается в этой библиотеке. Spring-data-commons содержит все необходимые базовые классы для имплементации собственных репозиториев и требуется не так много усилий, чтобы это реализовать. Например, эта библиотека используется в spring-data-mongodb.

Самым сложным было аккуратно реализовать генерацию JPQL на основе иерархии объектов — результата разбора имени метода. Но, к счастью, в Apache Ignite уже была реализована похожая задача, поэтому код был взят оттуда и немного адаптирован для генерации JPQL вместо SQL и поддержки оператора delete.

В spring-data-commons используется проксирование для динамического создания реализаций интерфейсов. Когда инициализируется контекст CUBA приложения, то все ссылки на интерфейсы заменяются на ссылки на прокси-бины, опубликованные в контексте. При вызове метода интерфейса он перехватывается соответствующим прокси-объектом. Затем этот объект генерирует JPQL запрос по имени метода, подставляет параметры и отдает запрос с параметрами в DataManager на выполнение. Следующая диаграмма отображает упрощенный процесс взаимодействия ключевых компонентов модуля.

Использование репозиториев в CUBA

Чтобы исплоьзовать репозитории в CUBA, нужно просто подключить модуль в файле сборки проекта:

appComponent("com.haulmont.addons.cuba.jpa.repositories:cuba-jpa-repositories-global:0.1-SNAPSHOT")

Можно использовать XML конфигурацию для того, чтобы "включить" репозитории:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:repositories="http://www.cuba-platform.org/schema/data/jpa"
            xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context-4.3.xsd
   http://www.cuba-platform.org/schema/data/jpa      
   http://www.cuba-platform.org/schema/data/jpa/cuba-repositories.xsd">

   <!-- Annotation-based beans -->
   <context:component-scan base-package="com.company.sample"/>

   <repositories:repositories base-package="com.company.sample.core.repositories"/>

</beans:beans>

А можно воспользоваться аннотациями:

@Configuration
@EnableCubaRepositories
public class AppConfig {
   //Configuration here
}

После того, как поддержка репозиториев активирована, можно их создавать в привычном виде, например:

public interface CustomerRepository extends CubaJpaRepository<Customer, UUID> {

   long countByLastName(String lastName);
   List<Customer> findByNameIsIn(List<String> names);

   @CubaView("_minimal")
   @JpqlQuery("select c from sample$Customer c where c.name like concat(:name, '%')")
   List<Customer> findByNameStartingWith(String name);
}

Для каждого метода можно использовать аннотации:

  • @CubaView — для задания представления CUBA, которое будет использоваться в DataManager
  • @JpqlQuery — для задания JPQL запроса, который будет выполняться, вне зависимости от названия метода

Этот модуль используется в модуле global фреймворка CUBA, следовательно, можно использовать репозитории как в модуле core, так и в web. Единственное, что нужно не забыть — активировать репозитории в конфигурационных файлах обоих модулей.

Пример использования репозитория в сервисе CUBA:

@Service(CustomerService.NAME)
public class CustomerServiceBean implements PersonService {

   @Inject
   private CustomerRepository customerRepository;

   @Override
   public List<Date> getCustomersBirthDatesByLastName(String name) {
      return customerRepository.findByNameStartingWith(name)
            .stream().map(Customer::getBirthDate).collect(Collectors.toList());
   }
}

Заключение

CUBA — гибкий фреймворк. Если есть желание что-то в него добавить, то нет необходимости исправлять ядро самостоятельно или ждать новую версию. Я надеюсь, что этот модуль сделает разработку с CUBA более эффективной и быстрой. Первая версия модуля доступна на GitHub, протестировано на CUBA версии 6.10

Читать далее

Комментарии