Pull to refresh
114.54
Haulmont
Создаем современные корпоративные системы

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

Reading time 7 min
Views 4K


Тема статьи достаточно узконаправленная, но, возможно, окажется полезной тем, кто разрабатывает свои собственные хранилища данных и думает об интеграции со Spring Framework.


Предпосылки


Разработчики обычно не очень любят менять свои привычки (зачастую, в список привычек входят и фреймворки). Когда я начал работать с 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<Customer> loadContext = LoadContext.create(Customer.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 LastNsme 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

Tags:
Hubs:
+8
Comments 0
Comments Leave a comment

Articles

Information

Website
www.haulmont.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Haulmont