Все статьи
Содержание

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

    Предпосылки

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

    text

    При помощи 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 на выполнение. Следующая диаграмма отображает упрощенный процесс взаимодействия ключевых компонентов модуля.

    text

    Использование репозиториев в 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

    Jmix - это open-source платфора быстрой разработки бизнес-приложений на Java