Столбцы, определенные во время выполнения, с помощью asentinel-orm

Asentinel-orm – это легкий инструмент ORM, построенный на основе Spring JDBC, в частности JdbcTemplate. Таким образом, он обладает большинством функций, которые можно ожидать от базового ORM, таких как генерация SQL, ленивая загрузка и т. д.

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

С 2015 года asentinel-orm успешно использовался в нескольких приложениях и постоянно совершенствовался в соответствии с бизнес-потребностями. Летом 2024 года он официально стал проектом с открытым исходным кодом, что, как мы считаем, ускорит его развитие и увеличит число участников.

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

  • Простая конфигурация
  • Прямолинейное моделирование сущностей домена с помощью пользовательских аннотаций
  • Легкая запись и безопасное выполнение простых SQL-запросов
  • Автоматическая генерация SQL-запросов
  • Динамическая схема (сущности обогащаются дополнительными атрибутами времени выполнения, сохраняются и читаются без изменений в коде)

Приложение

Настройка

  • Java 21
  • Spring Boot 3.4.0
  • asentinel-orm 1.70.0
  • База данных H2

Конфигурация

Для взаимодействия с asentinel-orm и использования его функционала требуется экземпляр OrmOperations.

Как указано в JavaDoc, это центральный интерфейс для выполнения операций ORM и не предполагается, чтобы он был специально реализован в клиентском коде.

Пример приложения включает код конфигурации для создания бина этого типа.

Java

 

OrmOperations имеет два суперинтерфейса:

  • SqlBuilderFactory – создает экземпляры SqlBuilder, которые могут быть дальше использованы для создания SQL-запросов. SqlBuilder способен автоматически генерировать части запроса, например, ту, которая выбирает столбцы. Кроме того, часть where, часть order by, другие условия и фактические столбцы могут быть добавлены с использованием методов из класса SqlBuilder. В следующей части этого раздела показан пример запроса, сгенерированного с помощью SqlBuilder.
  • Updater – используется для сохранения сущностей в соответствующие таблицы базы данных. Он может выполнять вставки или обновления в зависимости от того, является ли сущность новой или уже существующей. Существует стратегический интерфейс под названием NewEntityDetector, который используется для определения, является ли сущность новой. По умолчанию используется SimpleNewEntityDetector.

Все запросы, генерируемые ORM, выполняются с использованием экземпляра SqlQueryTemplate , которому дополнительно необходим Spring JdbcOperations/JdbcTemplate для работы. В конечном итоге все запросы проходят через добрый старый JdbcTemplate, через который они выполняются в рамках транзакций Spring, как и любое JdbcTemplate прямое выполнение.

Специфические для базы данных SQL конструкции и логика предоставляются через реализации интерфейса JdbcFlavor, которые далее внедряются в большинство упомянутых выше бинов. В данной статье используется база данных H2, для которой сконфигурирована реализация H2JdbcFlavor.

Полная конфигурация ORM в качестве части примерного приложения представлена в OrmConfig.

Реализация

Экспериментальная доменная модель, представленная примерным приложением, проста и состоит из двух сущностей – производителей автомобилей и моделей автомобилей. Представляя именно то, что обозначают их названия, связь между ними очевидна: один производитель автомобилей может владеть несколькими моделями автомобилей.

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

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

Начальные сущности сопоставляются с использованием таблиц баз данных ниже:

SQL

 

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

Java

 

Java

 

Несколько соображений:

  • @Table – сопоставляет (ассоциирует) класс с таблицей баз данных
  • @PkColumn – сопоставляет id (уникальный идентификатор) с первичным ключом таблицы
  • @Column – сопоставляет член класса с столбцом таблицы
  • @Child – определяет отношение с другой сущностью
  • Члены, аннотированные с помощью @Child, настроены на ленивую загрузку
  • Столбец таблицы type сопоставлен с полем enumCarType

Чтобы классу CarManufacturer поддерживать атрибуты, определенные во время выполнения (сопоставленные с столбцами таблиц, определенными во время выполнения), определен подкласс, как показано ниже:

Java

 

Этот класс хранит атрибуты (поля), определенные во время выполнения, в Map. Взаимодействие между значениями полей во время выполнения и ORM выполняется через реализацию интерфейса DynamicColumnEntity .

Java

 

  • setValue() – используется для установки значения столбца, определенного во время выполнения, когда оно считывается из таблицы
  • getValue() – используется для извлечения значения столбца, определенного во время выполнения, когда оно сохраняется в таблицу

Класс DynamicColumn отображает атрибуты, определенные во время выполнения, на соответствующие столбцы аналогично тому, как аннотация @Column отображает известные на этапе компиляции члены.

При запуске приложения выполняется класс CfRunner. Пользователя просят ввести имена и типы желаемых динамических пользовательских атрибутов, которые обогащают сущность CarManufacturer (для упрощения поддерживаются только типы int и varchar ). 

Для каждой пары имя-тип выполняется DML-команда, чтобы новые столбцы можно было добавить в таблицу базы данных CarManufacturer . Операцию выполняет следующий метод (объявленный в CarService).

Java

 

Каждый входной атрибут записывается как DefaultDynamicColumn, реализация ссылки на DynamicColumn .

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

Java

 

Нижеприведенный метод (объявленный в CarService) фактически создает сущность с помощью ORM.

Java

 

Версия метода OrmOperations update() с двумя параметрами вызывается, что позволяет передать экземпляр UpdateSettings и сообщить ORM при выполнении, что есть значения, определенные во время выполнения, которые должны быть сохранены.

В конце концов создаются две модели автомобилей, соответствующие одному из ранее добавленных производителей автомобилей.

Java

 

Ниже представленный метод (объявленный в CarService) фактически создает сущности через ORM, в этот раз используя метод OrmOperations update() для сохранения сущностей без динамических атрибутов. Для удобства несколько сущностей создаются за один вызов.

Java

 

На последнем этапе один из созданных производителей загружается по имени с использованием сгенерированного ORM запроса.

Java

 

Несколько объяснений относительно вышеуказанного метода стоит сделать.

Метод OrmOperations newSqlBuilder() создает экземпляр SqlBuilder, и, как и следует из названия, он может быть использован для генерации SQL-запросов. Метод SqlBuilder select() генерирует часть select from table запроса, в то время как остальная часть (where, order by) должна быть добавлена. Часть запроса select может быть настроена путем передачи экземпляров EntityDescriptorNodeCallback (подробности о EntityDescriptorNodeCallback могут стать предметом будущей статьи).

Для того чтобы ORM знал, что план заключается в выборе и сопоставлении столбцов, определенных во время выполнения, необходимо передать DynamicColumnsEntityNodeCallback . Вместе с ним предоставляется AutoEagerLoader , чтобы ORM понимал, что необходимо предварительно загружать список CarModel, связанных с производителем. Тем не менее, это не имеет отношения к атрибутам, определенным во время выполнения, но это демонстрирует, как можно предварительно загружать дочерние элементы.

Вывод

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

Не было редкостью, когда у нас была возможность обсуждать в “сообществе” asentinel-orm, причины, по которым нам пришлось разрабатывать такой инструмент. Обычно, на первый взгляд, разработчики оказывались скептически настроенными и осторожными, когда речь заходила о собственной ORM, спрашивая, почему бы не использовать Hibernate или другие реализации JPA.

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

Поскольку проект теперь является открытым исходным кодом, любому заинтересованному лицу легко посмотреть на него, сформировать объективное мнение и, возможно, сделать форк, открыть PR и внести свой вклад.

Ресурсы

  • Открытый проект ORM находится здесь.
  • Исходный код образцового приложения находится здесь.

Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm