Новая Java ORM и почему она нам нужна

Я создал фреймворк о неизменяемых данных и ORM для неизменяемых данных.

Имя проекта: Jimmer

Дом проекта: https://babyfish-ct.github.io/jimmer-doc/

Обзор

Jimmer разделен на две части, jimmer-core и jimmer-sql.

  1. jimmer-core: неизменяемые данные
  2. jimmer-sql: ORM на основе jimmer-core

Их соответствующие обязанности заключаются в следующем

  1. jimmer-core

    Портирование известного проекта immer для Java, изменяющего неизменяемые объекты на манер изменяемых объектов.

    Jimmer может использоваться в любом контексте, где требуются неизменяемые структуры данных для замены записей java. Неизменяемые структуры данных позволяют (эффективно) обнаруживать изменения: если ссылка на объект не изменилась, то и сам объект не изменился. Кроме того, это делает клонирование относительно дешевым: неизменяемые части дерева данных не нужно копировать, они делятся в памяти со старыми версиями того же состояния.

    В общем случае эти преимущества достигаются за счет того, что вы никогда не изменяете никаких свойств объекта или списка, а всегда создаете измененную копию. На практике это может привести к написанию очень громоздкого кода, и эти ограничения легко случайно нарушить. Jimmer поможет вам следовать парадигме неизменяемых данных, устранив следующие болевые точки:

    1. Jimmer обнаружит неожиданную мутацию и выдаст ошибку.
    2. Jimmer избавит вас от необходимости создавать типичный код, необходимый при глубоком обновлении неизменяемых объектов: без Jimmer вам пришлось бы вручную создавать копии объектов на каждом уровне. Обычно с использованием большого количества конструкций копирования.
    3. При использовании JImmer изменения вносятся в объект draft, который записывает изменения и заботится о создании необходимых копий, не затрагивая оригинал.

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

    Кроме того, для поддержки ORM Jimmer добавляет динамику объектов в immer. Любое свойство объекта может отсутствовать.

    • Отсутствующие свойства вызывают исключения при прямом обращении к ним кода.
    • Отсутствующие свойства автоматически игнорируются при сериализации Jackson и не вызывают исключений.
  2. jimmer-sql

    ORM, основанный на динамических неизменяемых объектах jimmer-core.

    С точки зрения реализации, jimmer-sql невероятно легкий, без каких-либо зависимостей, кроме JDBC, даже без какой-либо легковесной инкапсуляции для подключения к базе данных, как SqlSession из myBatis.

    Подобно QueryDsl, JOOQ, JPA Criteria, в сильно типизированных SQL DSL большинство ошибок SQL сообщается во время компиляции, а не как исключения во время выполнения.

    Однако сильно типизированные SQL DSL не конфликтуют с нативным SQL. Благодаря элегантному API, Native SQL смешивается с сильно типизированным SQL DSL, и разработчикам предлагается использовать возможности, характерные для конкретных продуктов баз данных, такие как аналитические функции и регуляризация.

    В дополнение ко всем обязательным функциям ORM, jimmer-sql предоставляет еще 4 функции, которые значительно превосходят другие ORM:

 - [Save command](https://babyfish-ct.github.io/jimmer-doc/docs/jimmer-sql/mutation/save-command)
 - [Object fetcher](https://babyfish-ct.github.io/jimmer-doc/docs/jimmer-sql/query/fetcher)
 - [Dynamic table joins](https://babyfish-ct.github.io/jimmer-doc/docs/jimmer-sql/table-join#dynamic-join)
 - [Smarter pagination queries](https://babyfish-ct.github.io/jimmer-doc/docs/jimmer-sql/query/pagination).

 These four powerful functions that are clearly different from other ORMs are the goals pursued by the ORM part of this framework.
Вход в полноэкранный режим Выход из полноэкранного режима

1. Сделать User Bean достаточно мощным

1.1 Использовать неизменяемые данные, но поддерживать временные изменяемые прокси.

@Immutable
public interface TreeNode {
    String name();
    TreeNode parent();
    List<TreeNode> childNodes();
}
Войти в полноэкранный режим Выход из полноэкранного режима

Процессор аннотаций сгенерирует для пользователя производный мутабельный интерфейс: TreeNodeDraft. Пользователь может использовать его следующим образом

// Step1: Create object from scratch
TreeNode oldTreeNode = TreeNodeDraft.$.produce(root ->  
    root
        .setName("Root")
        .addIntoChildNodes(child ->
            child.setName("Drinks")        
        )
        .addIntoChildNodes(child ->
            child.setName("Breads")        
        )
);

// Step2: Create object based on existing object
TreeNode newTreeNode = TreeNodeDraft.$.produce(
    oldTreeNode, // existing object
    root ->
      root.childNodes(false).get(0) // Get child proxy
          .setName("Dranks+"); // Change child proxy
);

System.out.println("Old tree node: ");
System.out.println(oldTreeNode);

System.out.println("New tree node: ");
System.out.println(newTreeNode);
Войти в полноэкранный режим Выйти из полноэкранного режима

Окончательный результат печати выглядит следующим образом

Old tree node: 
{"name": "Root", childNodes: [{"name": "Drinks"}, {"name": "Breads"}]}
New tree node: 
{"name": "Root", childNodes: [{"name": "`Drinks+`"}, {"name": "Breads"}]}
Вход в полноэкранный режим Выход из полноэкранного режима

1.2 Динамический объект.

Любое свойство объекта данных может быть неопределенным.

  1. Прямой доступ к неопределенным свойствам вызывает исключение.
  2. При использовании сериализации Jackson неопределенные свойства будут игнорироваться, без выброса исключения.
TreeNode current = TreeNodeDraft.$.produce(current ->
    node
        .setName("Current")
        .setParent(parent -> parent.setName("Father"))
        .addIntoChildNodes(child -> child.setName("Son"))
);

// You can access specified properties
System.out.println(current.name());
System.out.println(current.parent());
System.out.println(current.childNodes());

/*
 * But you cannot access unspecified fields, like this
 *
 * System.out.println(current.parent().parent());
 * System.out.println(
 *     current.childNodes().get(0).childNodes()
 * );
 *
 * , because direct access to unspecified 
 * properties causes an exception.
 */

/*
 * Finally You will get JSON string like this
 * 
 * {
 *     "name": "Current", 
 *     parent: {"name": "Father"},
 *     childNodes:[
 *         {"name": "Son"}
 *     ]
 * }
 *
 * , because unspecified will be ignored by 
 * jackson serialization, without exception throwing.
 */
String json = new ObjectMapper()
    .registerModule(new ImmutableModule())
    .writeValueAsString(current);

System.out.println(json);
Вход в полноэкранный режим Выход из полноэкранного режима

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

  1. Одинокий объект, например

    TreeNode lonelyObject = TreeNodeDraft.$.produce(draft ->
        draft.setName("Lonely object")
    );
    
  2. Дерево мелких объектов, например

    TreeNode shallowTree = TreeNodeDraft.$.produce(draft ->
        draft
            .setName("Shallow Tree")
            .setParent(parent -> parent.setName("Father"))
            .addIntoChildNodes(child -> parent.setName("Son"))
    );
    
  3. Глубокое дерево объектов, например

    TreeNode deepTree = TreeNodeDraft.$.produce(draft ->
        draft
            .setName("Deep Tree")
            .setParent(parent -> 
                 parent
                     .setName("Father")
                     .setParent(deeperParent ->
                         deeperParent.setName("Grandfather")
                     )
            )
            .addIntoChildNodes(child -> 
                child
                    .setName("Son")
                    .addIntoChildNodes(deeperChild -> 
                        deeperChild.setName("Grandson");
                    )
            )
    );
    

Этот динамизм объектов, включающий бесчисленные возможности, является основной причиной, по которой ORM от jimmer может предоставлять более мощные возможности.

2. ORM на основе неизменяемых объектов.

В jimmer’s ORM объекты также являются неизменяемыми интерфейсами.

@Entity
public interface TreeNode {

    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "sequence:TREE_NODE_ID_SEQ"
    )
    long id();

    @Key // jimmer annotation, `name()` is business key,
    // business key will be used when `id` property is not specified
    String name();

    @Key // Business key too
    @ManyToOne
    @OnDelete(DeleteAction.DELETE)
    TreeNode parent();

    @OneToMany(mappedBy = "parent")
    List<TreeNode> childNodes();
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Примечание!

Хотя jimmer использует некоторые аннотации JPA для завершения связки между сущностями и таблицами, jimmer не является JPA.

2.1 Сохранение произвольно сложного дерева объектов в базу данных

  1. Сохранить одинокую сущность

    sqlClient.getEntities().save(
        TreeNode lonelyObject = TreeNodeDraft.$.produce(draft ->
            draft
                .setName("RootNode")
                .setParent((TreeNode)null)
        )
    );
    
  2. Сохранить неглубокое дерево сущностей

    sqlClient.getEntities().save(
        TreeNode lonelyObject = TreeNodeDraft.$.produce(draft ->
            draft
                .setName("RootNode")
                .setParent(parent ->
                    parent.setId(100L)
                )
                .addIntoChildNodes(child ->
                    child.setId(101L)
                )
                .addIntoChildNodes(child ->
                    child.setId(102L)
                )
        )
    );
    
  3. Сохранить глубокое дерево сущностей

    sqlClient.getEntities().saveCommand(
        TreeNode lonelyObject = TreeNodeDraft.$.produce(draft ->
            draft
                .setName("RootNode")
                .setParent(parent ->
                    parent
                        .setName("Parent")
                        .setParent(grandParent ->
                            grandParent.setName("Grand parent")
                        )
                )
                .addIntoChildNodes(child ->
                    child
                        .setName("Child-1")
                        .addIntoChildNodes(grandChild ->
                            grandChild.setName("Child-1-1")
                        )
                       .addIntoChildNodes(grandChild ->
                            grandChild.setName("Child-1-2")
                        )
                )
                .addIntoChildNodes(child ->
                    child
                        .setName("Child-2")
                        .addIntoChildNodes(grandChild ->
                            grandChild.setName("Child-2-1")
                        )
                        .addIntoChildNodes(grandChild ->
                            grandChild.setName("Child-2-2")
                        )
                )
        )
    ).configure(it ->
        // Auto insert associated objects 
        // if they do not exists in database
        it.setAutoAttachingAll()
    ).execute();
    

2.2 Запрос произвольно сложного дерева объектов из базы данных

  1. Выберите корневые узлы из базы данных (TreeNodeTable – java-класс, созданный процессором аннотаций)

    List<TreeNode> rootNodes = sqlClient
        .createQuery(TreeNodeTable.class, (q, treeNode) -> {
            q.where(treeNode.parent().isNull()) // filter roots
            return q.select(treeNode);
        })
        .execute();
    
  2. Выбрать корневые узлы и их дочерние узлы из базы данных (TreeNodeFetcher – java-класс, созданный процессором аннотаций).

    List<TreeNode> rootNodes = sqlClient
        .createQuery(TreeNodeTable.class, (q, treeNode) -> {
            q.where(treeNode.parent().isNull()) // filter roots
            return q.select(
                treeNode.fetch(
                    TreeNodeFetcher.$
                        .allScalarFields()
                        .childNodes(
                            TreeNodeFetcher.$
                                .allScalarFields()
                        )
                )
            );
        })
        .execute();
    
  3. Запросить корневые узлы с двумя уровнями дочерних узлов.

    У вас есть два способа сделать это

-   Specify a deeper tree format
Войти в полноэкранный режим Выход из полноэкранного режима
    ```java
    List<TreeNode> rootNodes = sqlClient
    .createQuery(TreeNodeTable.class, (q, treeNode) -> {
        q.where(treeNode.parent().isNull()) // filter roots
        return q.select(
            treeNode.fetch(
                TreeNodeFetcher.$
                    .allScalarFields()
                    .childNodes( // level-1 child nodes
                        TreeNodeFetcher.$
                            .allScalarFields()
                            .childNodes( // level-2 child nodes
                                TreeNodeFetcher.$
                                    .allScalarFields()
                            )
                    )
            )
        );
    })
    .execute();
    ```
Вход в полноэкранный режим Выход из полноэкранного режима
-   You can also specify depth for self-associative property, this is better way
Войти в полноэкранный режим Выход из полноэкранного режима
    ```java
    List<TreeNode> rootNodes = sqlClient
    .createQuery(TreeNodeTable.class, (q, treeNode) -> {
        q.where(treeNode.parent().isNull()) // filter roots
        return q.select(
            treeNode.fetch(
                TreeNodeFetcher.$
                    .allScalarFields()
                    .childNodes(
                        TreeNodeFetcher.$
                            .allScalarFields(),
                        it -> it.depth(2) // Fetch 2 levels
                    )
            )
        );
    })
    .execute();
    ```
Войти в полноэкранный режим Выход из полноэкранного режима
  1. Запросить все корневые узлы, рекурсивно получить все дочерние узлы, независимо от их глубины

    List<TreeNode> rootNodes = sqlClient
    .createQuery(TreeNodeTable.class, (q, treeNode) -> {
        q.where(treeNode.parent().isNull()) // filter roots
        return q.select(
            treeNode.fetch(
                TreeNodeFetcher.$
                    .allScalarFields()
                    .childNodes(
                        TreeNodeFetcher.$
                            .allScalarFields(),
    
                        // Recursively fetch all, 
                        // no matter how deep
                        it -> it.recursive() 
                    )
            )
        );
    })
    .execute();
    
  2. Запросить все корневые узлы, разработчик сам определяет, должен ли каждый узел рекурсивно запрашивать дочерние узлы.

    List<TreeNode> rootNodes = sqlClient
    .createQuery(TreeNodeTable.class, (q, treeNode) -> {
        q.where(treeNode.parent().isNull()) // filter roots
        return q.select(
            treeNode.fetch(
                TreeNodeFetcher.$
                    .allScalarFields()
                    .childNodes(
                        TreeNodeFetcher.$
                            .allScalarFields(),
                        it -> it.recursive(args ->
                            // - If the node name starts with `Tmp_`, 
                            // do not recursively query child nodes.
                            //
                            // - Otherwise, 
                            // recursively query child nodes.
                            !args.getEntity().name().startsWith("Tmp_")
                        )
                    )
            )
        );
    })
    .execute();
    

2.3 Динамические соединения таблиц.

Для разработки мощных динамических запросов недостаточно поддерживать динамические предикаты where, необходимы динамические соединения таблиц.

@Repository
public class TreeNodeRepository {

    private final SqlClient sqlClient;

    public TreeNodeRepository(SqlClient sqlClient) {
        this.sqlClient = sqlClient;
    }

    public List<TreeNode> findTreeNodes(
        @Nullable String name,
        @Nullable String parentName,
        @Nullable String grandParentName
    ) {
        return sqlClient
            .createQuery(TreeNodeTable.class, (q, treeNode) -> {
               if (name != null && !name.isEmpty()) {
                   q.where(treeNode.name().eq(name));
               }
               if (parentName != null && !parentName.isEmpty()) {
                   q.where(
                       treeNode
                       .parent() // Join: current -> parent
                       .name()
                       .eq(parentName)
                   );
               }
               if (grandParentName != null && !grandParentName.isEmpty()) {
                   q.where(
                       treeNode
                           .parent() // Join: current -> parent
                           .parent() // Join: parent -> grand parent
                           .name()
                           .eq(grandParentName)
                   );
               }
               return q.select(treeNode);
            })
            .execute();
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Этот динамический запрос поддерживает три нулевых параметра.

  1. Когда параметр parentName не равен нулю, требуется объединение таблиц current -> parent.
  2. Когда параметр grandParentName не является нулевым, необходимо присоединить таблицу current -> parent -> grandParent.

Когда параметры parentName и grandParent оба указаны, пути присоединения таблицы current -> parent и current -> parent -> grandParent добавляются к условиям запроса. Если среди них current->parent появится дважды, jimmer автоматически объединит дублирующие соединения таблиц.

Это означает

`current -> parent` 
+ 
`current -> parent -> grandParent` 
= 
--+-current
  |
  --+-parent
     |
     ----grandParent
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Окончательный вариант SQL

select 
    tb_1_.ID, tb_1_.NAME, tb_1_.PARENT_ID
from TREE_NODE as tb_1_

/* Two java joins are merged to one sql join*/
inner join TREE_NODE as tb_2_ 
    on tb_1_.PARENT_ID = tb_2_.ID

inner join TREE_NODE as tb_3_ 
    on tb_2_.PARENT_ID = tb_3_.ID
where
    tb_2_.NAME = ? /* parentName */
and
    tb_3_.NAME = ? /* grandParentName */
Войти в полноэкранный режим Выйти из полноэкранного режима

2.4 Автоматическая генерация count-запроса по data-запросу.

Запрос пагинации требует двух SQL операторов, один для запроса общего количества строк данных, а другой для запроса данных на одной странице, назовем их count-query и data-query.

Разработчикам нужно сосредоточиться только на data-count, а count-query может быть сгенерирован автоматически.


// Developer create data-query
ConfigurableTypedRootQuery<TreeNodeTable, TreeNode> dataQuery = 
    sqlClient
        .createQuery(TreeNodeTable.class, (q, treeNode) -> {
            q
                .where(treeNode.parent().isNull())
                .orderBy(treeNode.name());
            return q.select(book);
        });

// Framework generates count-query
TypedRootQuery<Long> countQuery = dataQuery
    .reselect((oldQuery, book) ->
        oldQuery.select(book.count())
    )
    .withoutSortingAndPaging();

// Execute count-query
int rowCount = countQuery.execute().get(0).intValue();

// Execute data-query
List<TreeNode> someRootNodes = 
    dataQuery
        // limit(limit, offset), from 1/3 to 2/3
        .limit(rowCount / 3, rowCount / 3)
        .execute();
Вход в полноэкранный режим Выход из полноэкранного режима

Оцените статью
Procodings.ru
Добавить комментарий