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 и не предполагается, чтобы он был специально реализован в клиентском коде.
Пример приложения включает код конфигурации для создания бина этого типа.
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
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
.
Реализация
Экспериментальная доменная модель, представленная примерным приложением, проста и состоит из двух сущностей – производителей автомобилей и моделей автомобилей. Представляя именно то, что обозначают их названия, связь между ними очевидна: один производитель автомобилей может владеть несколькими моделями автомобилей.
Кроме имени, производитель автомобилей обогащен атрибутами (столбцами), которые вводятся пользователем приложения динамически во время выполнения. Пример использования является прямолинейным:
- Пользователю предлагается предоставить целевые имена и типы для динамических атрибутов
- Создаются несколько производителей автомобилей с конкретными значениями для ранее добавленных динамических атрибутов, а затем
- Сущности загружаются обратно, описанные как начальными, так и атрибутами, определенными во время выполнения
Начальные сущности сопоставляются с использованием таблиц баз данных ниже:
CREATE TABLE CarManufacturers (
ID INT auto_increment PRIMARY KEY,
NAME VARCHAR(255)
);
CREATE TABLE CarModels(
ID INT auto_increment PRIMARY KEY,
CarManufacturer int,
NAME VARCHAR(255),
TYPE VARCHAR(15),
foreign key (CarManufacturer) references CarManufacturers(id)
);
Соответствующие доменные классы декорированы аннотациями, специфичными для ORM, для настройки сопоставлений с указанными таблицами баз данных.
"CarManufacturers") (
public class CarManufacturer {
"id") (
private int id;
"name") (
private String name;
parentRelationType = RelationType.MANY_TO_ONE, (
fkName = CarModel.COL_CAR_MANUFACTURER,
fetchType = FetchType.LAZY)
private List<CarModel> models = Collections.emptyList();
...
}
"CarModels") (
public class CarModel {
public static final String COL_CAR_MANUFACTURER = "CarManufacturer";
"id") (
private int id;
"name") (
private String name;
"type") (
private CarType type;
fkName = COL_CAR_MANUFACTURER, fetchType = FetchType.LAZY) (
private CarManufacturer carManufacturer;
...
}
public enum CarType {
CAR, SUV, TRUCK
}
Несколько соображений:
@Table
– сопоставляет (ассоциирует) класс с таблицей баз данных@PkColumn
– сопоставляетid
(уникальный идентификатор) с первичным ключом таблицы@Column
– сопоставляет член класса с столбцом таблицы@Child
– определяет отношение с другой сущностью- Члены, аннотированные с помощью
@Child
, настроены на ленивую загрузку - Столбец таблицы
type
сопоставлен с полемenum
–CarType
Чтобы классу CarManufacturer
поддерживать атрибуты, определенные во время выполнения (сопоставленные с столбцами таблиц, определенными во время выполнения), определен подкласс, как показано ниже:
public class CustomFieldsCarManufacturer extends CarManufacturer
implements DynamicColumnsEntity<DynamicColumn> {
private final Map<DynamicColumn, Object> customFields = new HashMap<>();
...
public void setValue(DynamicColumn column, Object value) {
customFields.put(column, value);
}
public Object getValue(DynamicColumn column) {
return customFields.get(column);
}
...
}
Этот класс хранит атрибуты (поля), определенные во время выполнения, в Map
. Взаимодействие между значениями полей во время выполнения и ORM выполняется через реализацию интерфейса DynamicColumnEntity
.
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– используется для установки значения столбца, определенного во время выполнения, когда оно считывается из таблицыgetValue()
– используется для извлечения значения столбца, определенного во время выполнения, когда оно сохраняется в таблицу
Класс DynamicColumn
отображает атрибуты, определенные во время выполнения, на соответствующие столбцы аналогично тому, как аннотация @Column
отображает известные на этапе компиляции члены.
При запуске приложения выполняется класс CfRunner
. Пользователя просят ввести имена и типы желаемых динамических пользовательских атрибутов, которые обогащают сущность CarManufacturer
(для упрощения поддерживаются только типы int
и varchar
).
Для каждой пары имя-тип выполняется DML-команда, чтобы новые столбцы можно было добавить в таблицу базы данных CarManufacturer
. Операцию выполняет следующий метод (объявленный в CarService
).
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
Каждый входной атрибут записывается как DefaultDynamicColumn
, реализация ссылки на DynamicColumn
.
После определения всех атрибутов в базу данных добавляются два производителя автомобилей, поскольку пользователь предоставляет значения для каждого такого атрибута.
Map<DynamicColumn, Object> dynamicColumnsValues = new HashMap<>();
for (DynamicColumn dynamicColumn : dynamicColumns) {
// read values for each dynamic attribute
...
}
CustomFieldsCarManufacturer mazda = new CustomFieldsCarManufacturer("Mazda", dynamicColumnsValues);
carService.createManufacturer(mazda, dynamicColumns);
Нижеприведенный метод (объявленный в CarService
) фактически создает сущность с помощью ORM.
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
Версия метода OrmOperations
update()
с двумя параметрами вызывается, что позволяет передать экземпляр UpdateSettings
и сообщить ORM при выполнении, что есть значения, определенные во время выполнения, которые должны быть сохранены.
В конце концов создаются две модели автомобилей, соответствующие одному из ранее добавленных производителей автомобилей.
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
Ниже представленный метод (объявленный в CarService
) фактически создает сущности через ORM, в этот раз используя метод OrmOperations
update() для сохранения сущностей без динамических атрибутов. Для удобства несколько сущностей создаются за один вызов.
public void createModels(CarModel... models) {
orm.update(models);
}
На последнем этапе один из созданных производителей загружается по имени с использованием сгенерированного ORM запроса.
CarManufacturer mazda1 = carService.findManufacturerByName("Mazda", dynamicColumns);
readOnly = true) (
public CarManufacturer findManufacturerByName(String name, List<DynamicColumn> attributes) {
return orm.newSqlBuilder(CustomFieldsCarManufacturer.class)
.select(
AutoEagerLoader.forPath(CarManufacturer.class, CarModel.class),
new DynamicColumnsEntityNodeCallback<>(
new DefaultObjectFactory<>(CustomFieldsCarManufacturer.class),
attributes
)
)
.where().column("name").eq(name)
.execForEntity();
}
Несколько объяснений относительно вышеуказанного метода стоит сделать.
Метод 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 и внести свой вклад.
Ресурсы
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm