All Articles ↓
3 месяца назад

Доступ к данным в Jmix: прокачиваем JPA

Введение

Модель данных - один из краеугольных камней в основании любого корпоративного приложения. Когда мы начинаем планировать модель данных, то нужно учитывать не только требования бизнеса, но также ответить на некоторые вопросы, которые могут повлиять на дизайн приложения. Например: нужен ли контроль к отдельным экземплярам данных (строкам в таблице БД)? Нужно ли логическое удаление? Понадобится ли CRUD API, и кто будет делать эту нудную работу?

JPA - де-факто стандарт для создания модели данных и доступа к данным в Java приложениях. Этот API не предоставляет возможностей для реализации, например, логического удаления “из коробки”, поэтому разработчикам приходится реализовывать свои подходы или использовать готовые фреймворки.

Фреймворк Jmix предоставляет движок, который добавляет всякие дополнительные вещи в приложения, использующие JPA. Вы получаете продвинутый контроль доступа к данным, логическое удаление, REST CRUD API и ещё пару возможностей, причем совместимость с JPA сохраняется.

В этой статье мы подробнее поговорим про слой доступа к данным в Jmix, какие возможности он предоставляет и как работает внутри.

Модель данных, JPA и @JmixEntity

Если у вас есть опыт работы с JPA, то при работе с Jmix сильно много нового учить не придется. Просто начинаете создавать сущности, в Jmix Studio для этого даже есть визуальный редактор:


В этом редакторе задайте имя класса сущности, выберите тип ID (UUID, Long, Integer, String или embedded class) и дополнительную функциональность в разделе Traits:

  • Versioned - для поддержки оптимистичных блокировок
  • Аудит создания экземпляра
  • Аудит последней модификации
  • Логическое удаление

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

@JmixEntity
@Table(name = "SPEAKER")
@Entity(name = "Speaker")
public class Speaker {
  
   @JmixGeneratedValue
   @Column(name = "ID", nullable = false)
   @Id
   private UUID id;

   @Email
   @Column(name = "EMAIL", unique = true)
   private String email;

   @Column(name = "NAME", nullable = false)
   @NotNull
   private String name;

   @Column(name = "VERSION", nullable = false)
   @Version
   private Integer version;

   @CreatedBy
   @Column(name = "CREATED_BY")
   private String createdBy;

   @LastModifiedBy
   @Column(name = "LAST_MODIFIED_BY")
   private String lastModifiedBy;

   @DeletedDate
   @Column(name = "DELETED_DATE")
   @Temporal(TemporalType.TIMESTAMP)
   private Date deletedDate;

   //Some columns, getters and setters are omitted
}

Следует заметить, что использование Jmix радикально не меняет код JPA сущностей. Класс не наследуется от базовых сущностей и не реализует никаких интерфейсов. Изо всех изменений вы можете заметить только несколько дополнительных аннотаций. Такой подход дает большую гибкость: вы можете делать свои собственные иерархии классов, в полной мере отвечающие потребностям бизнеса.

Большая часть использованных аннотаций - из библиотеки JPA или фреймворка Spring Boot, и вы с ними наверняка знакомы: @Entity, @Versioned, @CreatedBy, @LastModifiedBy. Jmix полагается на реализацию оптимистичных блокировок средствами Spring Boot, равно как и на аудит создания и обновления сущностей.

Обратите внимание на аннотации, относящиеся к фреймворку Jmix, которые появились в JPA классе:

  • @JmixEntity - говорит о том, что Jmix будет добавлять данную сущность в общий репозиторий сущностей
  • @JmixGeneratedValue - атрибут ID будет генерироваться фреймворком Jmix
  • @DeletedBy и @DeletedDate - используется для маркировки полей сущности, используемые для логического удаления

Возникает вопрос: “Что это ещё за репозиторий сущностей? У вас что, свой собственный слой доступа к данным?” И да и нет. Фреймворк сохраняет дополнительную информацию о модели данных приложения - метамодель - и использует специальный сервис - DataManager - для доступа к данным, но все равно под капотом используется одна из реализаций JPA.

Метамодель и DataManager

В основе Jmix лежит Spring Boot. Последний известен тем, что активно использует IoC, что подразумевает хранение информации обо всех сервисах в реестре - контексте приложения (ApplicationContext). В дополнение к этому, Jmix сохраняет данные о сущностях приложения в отдельном реестре - Metadata.

Metadata - сердце слоя доступа к данным в Jmix. Здесь содержится информация о сущностях, атрибутах, связях и т.д. Эти данные собираются только один раз при старте приложения во время сканирования классов и используются для магии Jmix: от ограничения доступа на уровне строк таблиц до двунаправленной привязки сущностей к элементам пользовательского интерфейса.

Для доступа к данным в Jmix используется сервис DataManager. По факту, это обертка для хорошо всем знакомого EntityManager. Так же, как и EntityManager, DataManager позволяет загружать сущности, используя JPQL, или по ID, сохранять сущности в БД или удалять их оттуда.

Примеры использования DataManager представлены ниже.

Загрузка одного экземпляра сущность по параметру ID:

Speaker loadSpeakerById(UUID speakerId) {
    return dataManager.load(Speaker.class) 
            .id(customerId)                 
            .one();                         
}

Загрузка данных с использованием JPQL:

List<Speaker> loadSpeakesByCompanyEmail() {
    return dataManager.load(Speaker.class)
            .query("select s from Speaker s where s.email like :email")
            .parameter("email", "%@company.com")
            .list();
}

DataManager использует Metamodel, чтобы предоставлять дополнительную функциональность. Запросы к БД перехватываются и модифицируются перед тем, как будут переданы в EntityManager на исполнение.

Давайте взглянем подробнее на то, что может предоставить Jmix.

Система безопасности данных

Первое, что стоит упомянуть - система контроля доступа в Jmix. В фреймворке используется система безопасности, основанная на ролях. Существует два типа ролей:

  1. Ресурсная роль - дает пользователям права на доступ к типам объектов и операциям: к сущностям, атрибутам, элементам пользовательского интерфейса, CRUD операциям над сущностями и т.д.
  2. Роль с доступом к строкам данных - дает пользователям права на редактирование отдельных строк данных в таблицах БД, т.е. к экземплярам сущностей.

Для ресурсных ролей можно определить политики для операций над сущностями и атрибутами:

@ResourceRole(name = "Speaker Admin", code = "speaker-admin-role")
public interface SpeakerAdminRole {

   @EntityPolicy(entityName = "Speaker", 
                 actions = {EntityPolicyAction.ALL})
   @EntityAttributePolicy(entityName = "Speaker", 
                          attributes = "{name, email}", 
                          action = EntityAttributePolicyAction.MODIFY)
   void speakersAccess();
}

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

Для ограничения доступа к экземплярам объектов нужно определить роль, в которой указывается предикат where, а также дополнительное условие join, что позволяет ограничивать выборку данных.

@RowLevelRole(name = "Speaker's Talks",
             code = "speakers-talks-role")
public interface SpeakersTalksRole {

   @JpqlRowLevelPolicy(entityClass = Talk.class,
                       where = "{E}.speaker.id = :current_user_id")
   void accessibleTalks();

}

Таким образом, все запросы, которые выполняются пользователем с данной ролью, подвергаются модификации. Предикат where добавляется к существующему условию с использованием оператора AND, а join добавляется к существующему списку таблиц. Если какое-то из условий отсутствует в оригинальном запросе, то оно добавляется в любом случае, дополняя запрос фразами where и/или join соответственно.

Metadata играет здесь важную роль. С её помощью происходит разбор начального JPQL, поиск ограничений и модификация оператора в соответствии с ограничениями, указанными в описаниях ролей.

Логическое удаление

В некоторых корпоративных приложениях часто требуется возможность организации логического удаления сущностей. Доводы “за” и “против” логического удаления рассматривались в статье “To Delete or to Soft Delete, That is the Question!” в блоге CUBA Platform. Jmix - наследник CUBA, поэтому также поддерживает логическое удаление.

Режим логического удаления включается автоматически для сущностей, у которых есть атрибуты, помеченные аннотациями @DeletedBy и @DeletedDate. Для этих сущностей все вызовы метода remove() DataManager’а, которые генерируют DELETE, перехватываются и подменяются оператором UPDATE, а ко всем вызовам find() (т.е. SELECT) добавляются условия where, чтобы исключить “удаленные” сущности из выборки.

@JmixEntity
@Table(name = "SPEAKER")
@Entity(name = "Speaker")
public class Speaker {

   @JmixGeneratedValue
   @Column(name = "ID", nullable = false)
   @Id
   private UUID id;

   @DeletedBy
   @Column(name = "DELETED_BY")
   private String deletedBy;

   @DeletedDate
   @Column(name = "DELETED_DATE")
   @Temporal(TemporalType.TIMESTAMP)
   private Date deletedDate;

      //Some fields are omitted
}

Здесь нам нужны метаданные сущностей, чтобы понять, какие столбцы таблицы нужно обновлять при удалении или включать в запрос при выборке. В отличие от CUBA, Jmix не требует реализации специального интерфейса для поддержки логического удаления. Все что нужно - добавить две аннотации к нужным полям.

Заметьте, что всё ещё остается возможность “настоящего” удаления записей или просмотра “удалённых” записей, если использовать EntityManager и обычный SQL.

REST и GraphQL

Автоматическое создание CRUD API - то, где метамодель Jmix показывает себя во всей красе. Когда у вас есть информация обо всех сущностях приложения, то ничего не стоит создать API для работы с ними.

Например, написание обработчика в общем виде для работы с HTTP запросами такого типа не составит большого труда:

GET  'http://server:8080/rest/entities/Speaker'

Единственный обработчик точки входа /rest/entities может принимать все запросы для работы с сущностями. Приложение “знает”, какие сущности должны быть выбраны из БД, если пользователь делает GET запрос для выборки (в нашем случае - сущности Speaker). Более того, ничего не стоит сделать генератор для Swagger документации, потому что у нас есть все метаданные по сущностям.

Jmix может сгенерировать Swagger документ, который включает все сущности и методы работы с ними. Этот документ будет полезен для разработчика, который хочет использовать встроенный REST API фреймворка:

GET  'http://server:8080/rest/docs/swagger.json'

Например, список CRUD операций может выглядеть так:

RestApi.png

Хорошая новость в том, что в Jmix вам не нужно делать нудную работу для создания всех этих API. Благодаря метамодели все это генерируется, и это может быть хорошей отправной точкой для создания своего REST API с использованием подхода “back-for-the-front”.

Если нужно что-то более гибкое, чем встроенный REST API, можно использовать GraphQL. Одна из основ GraphQL - это схема. С метамоделью Jmix у нас есть все данные для создания схемы, причем для этого не нужно прилагать никаких усилий.

Это значит, что GraphQL API для приложений Jmix можно создать в общем виде, как и REST API (и мы скоро собираемся его выпустить). И добавление GraphQL в ваше Jmix-приложение можно будет сделать буквально добавив одну зависимость в скрипт сборки.

implementation 'io.jmix.graphql:jmix-graphql-starter'

После добавления можно будет писать вот такие вот запросы:

{
  speakerCount
  speakerList(limit: 10, offset: 0, orderBy: {name: ASC}) {
    id
    name
    email
  }
}

Заключение

Как только вы создали модель данных в Jmix (используя в основном JPA аннотации), вы сразу получаете готовое приложение. Благодаря метамодели, вы можете использовать готовые сервисы практически без написания дополнительного кода:

  • Доступ к данным на уровне строк таблицы
  • Логическое удаление
  • CRUD REST API
  • GraphQL API

В итоге: создание простых приложений, ориентированных на работу с данными и с продвинутой функциональностью, а также с админкой “из коробки” - это очень простая задача в случае, если используется Jmix.

Андрей Беляев