当前位置: 首页 > 工具软件 > Jimmer > 使用案例 >

一个革命性的Java ORM: Jimmer

沈骞仕
2023-12-01

现在,有很多ORM框架可供选择,比如:JPA、myBatis、JOOQ、Exposed、Ktorm。 它们够用吗?

或许,一个实现层面更轻量,但功能层面却足够强大的革命性ORM能颠覆人们对ORM的传统认知。于是我创作这个ORM

项目地址:https://github.com/babyfish-ct/jimmer
文档(中英双语)地址:https://babyfish-ct.github.io/jimmer-doc/
视频地址: https://www.bilibili.com/video/BV1dA4y1R7pV/
JDK要求:8

项目分为两个部分:jimmer-core和jimmer-sql。

  1. jimmer-core, 定义了一套全新的不可变对象体系,用于定义实体类型,作为ORM的基石。jimmer-sql的强大,有一部分是基于jimmer-core的强大。

    作为对kotlin data class的抄袭回应,java14加入了record类型,用于创建不可变对象。即便去观察一些JVM平台的语言,也可以看见越来越多的现代编程语言(如C#9.0)对不可变对象进行了内置支持,不可变对象代表着未来编程语言的发展发向。

    因为不可变对象可以在语义层面混淆基于引用的共享和基于值的复制,不用担心数据内部细节被其它人意外修改,尤其是深层次的细节和多人开发的项目。只有数据库和缓存才有足够的分量成为可多方修改的共享数据,让开发人员去思考多方修改的副作用或利用这种副作用。普通的Java对象应该彻底消除这种顾虑,这就是现代编程语言的发展确实越来越喜欢不可变对象原因。

    不幸的是,不可变对象有它的问题,由于对象是不可变的,我们不能直接修改它,而需要基于旧对象创建新对象。如果对象只是一个简单对象,开发人员面对的复杂性还很一般,但是,只要对象具有一定的关联深度,“修改”深层次的关联对象将会变成噩梦。为了节约篇幅,本文不讨论这个痛点都多痛,请参考这个链接

    然而,java record(或kotlin data class)并没有回答这个痛点该如何解决。为此jimmer把JS/TS领域一个叫immer的功能移植给了Java。这是目前已知不可变对象最强方案,也是这个项目叫做jimmer的根本原因。

    jimmer可以基于已有的不可变对象创建可变的临时代理。由于代理是可变的,你当然可以随意修改,尤其是很深的关联对象。整个过程完成后,临时代理消亡前会利用其收集到的所有用户修改行为去创建另外一个不可变对象。这个过程看起来,就如同直接修改传统的可变对象一样简单,而幕后是基于对象树的copy-on-write策略,只有发生变化的部分会被拷贝,没有变化的部分子树永远在新旧对象之间共享。

    为了和ORM配合,不可变对象具备动态性。并不是对象的所有属性都需要初始化,它允许缺少一些属性,这个特性在传统ORM中称为延迟加载,并非所有对象属性,尤其是关联属性,都需要从数据库中查询。

    这里的未指定的属性并不是null,而是未知。在直接被代码访问时会导致异常,而在JSON序列化中会被自动忽略,不会异常。

    在传统的ORM中,这种信息不完整的对象仅仅可以在ORM内部被产生,返回给用户(比如,Hibernate中lazy的多对一属性返回代理对象,改代理只有id);而在jimmer中,无论是ORM还是用户代码,双方都可以随意可以构建任意形状的信息不完整的动态对象树给给对方用。这是jimmer能提供远强于其他ORM的功能的根本原因所在。

    值得一提的是,对象动态性还有一个妙用。定义实体类型时,类型之间允许双向关联,没有设计限制。但是具体业务实现需要创建对象时,对象之间只允许单方关联,保证不出现环形引用,以方便微服务之间的交流和和前端的交流。类型定义允许双向关联+对象实例之间仅允许单项关联的目的是为了让开发人员实现业务聚合根设计的滞后化。

  2. jimmer-sql,基于jimmer-core动态不可变对象的ORM。

    从实现层面讲,jimmer-sql轻量得让人难以置信,除了JDBC外没有任何依赖,甚至连myBatis中那种对数据库连接的SqlSession轻量级封装都没有。

    与QueryDsl, JOOQ, JPA Criteria类似,强类型的SQL DSL,绝大部分SQL错误都在编译时刻被报告,而非表现为运行时异常。

    然而强类型SQL DSL和Native SQL不冲突,通过优雅的API,在强类型的SQL DSL中混入Native SQL,鼓励开发人员使用特定数据库产品特有的功能,比如分析函数,正则。

    除了所有ORM的必须有的功能外,jimmer-sql提供了4个其它远超其他ORM的功能:Save指令、对象抓取器、动态表连接、更智能的分页查询。这4个明显区别于其它ORM的强大的功能,是本文要重点讨论的内容。

本文内容提纲

  1. jimmer-core: 让User Bean足够强大

    • 使用不可变数据,但支持临时可变代理
    • 动态对象
  2. jimmer-sql: 基于不可变对象的ORM

    • Save指令:将任意复杂的对象【】保存到数据库中。

      无论复杂业务对数据库的更新有多复杂,只要能通过一颗对象树来表达,都可以一个API调用存入数据库。

    • 对象抓取器:从数据库中查询任意复杂的对象【】。

      非GraphQL,但胜似GraphQL。

    • 动态表连接

      一个myBatis难以实现的高级SQL拼接功能,特别实用。

    • 更智能的分页查询

      自动根据data-query生成并优化count-query,从此告别复杂分页查询要构建两个查询的问题。

1. jimer-core: 让User Bean足够强大

1.1 使用不可变数据,但支持临时可变代理

@Immutable
public interface TreeNode {
    String name();
    TreeNode parent();
    List<TreeNode> childNodes();
}

Annotation processor自动生成一个接口: TreeNodeDraft. 该接口是可变的,且从TreeNode派生,使用方式如下。

// 第一步: 从头创建全新对象
TreeNode oldTreeNode = TreeNodeDraft.$.produce(root ->  
    root
        .setName("Root")
        .addIntoChildNodes(child ->
            child.setName("Drinks")        
        )
        .addIntoChildNodes(child ->
            child.setName("Breads")        
        )
);

// 第二步: 基于已有对象,创建新对象
TreeNode newTreeNode = TreeNodeDraft.$.produce(
    oldTreeNode, // 现有旧对象
    root ->
      root // 根代理
          .childNodes(false).get(0) // 得到子代理
          .setName("Dranks+"); // 修改子代理
);

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("我")
        .setParent(parent -> parent.setName("父亲"))
        .addIntoChildNodes(child -> child.setName("儿子"))
);

// 你可以访问被指定的属性
System.out.println(current.name());
System.out.println(current.parent());
System.out.println(current.childNodes());

/*
 * 但是你无法访问未被指定的属性,比如
 *
 * current.parent().parent();
 * current.childNodes().get(0).childNodes()
 * 因为直接访问未指定的属性会导致异常。
 */

/*
 * 最终你会得到这样的JSON
 * 
 * {
 *     "name": "我", 
 *     parent: {"name": "父亲"},
 *     childNodes:[
 *         {"name": "儿子"}
 *     ]
 * }
 *
 * 因为使用Jackson序列化,未指定的属性将被忽略,不会抛异常。
 */
String json = new ObjectMapper()
    .registerModule(new ImmutableModule())
    .writeValueAsString(current);

System.out.println(json);

因为实体对象是动态的,所以用户可以构建任意复杂的数据结构。 有无数种可能,比如

  1. 孤单的对象,例如

    TreeNode lonelyObject = TreeNodeDraft.$.produce(draft ->
        draft.setName("孤单的对象")
    );
    
  2. 较浅的对象树,例如

    TreeNode shallowTree = TreeNodeDraft.$.produce(draft ->
        draft
            .setName("较浅的对象树")
            .setParent(parent -> parent.setName("父亲"))
            .addIntoChildNodes(child -> parent.setName("儿子"))
    );
    
  3. 较深的对象树,例如

    TreeNode deepTree = TreeNodeDraft.$.produce(draft ->
        draft
            .setName("较深的对象树")
            .setParent(parent -> 
                 parent
                     .setName("父亲")
                     .setParent(deeperParent ->
                         deeperParent.setName("祖父")
                     )
            )
            .addIntoChildNodes(child -> 
                child
                    .setName("儿子")
                    .addIntoChildNodes(deeperChild -> 
                        deeperChild.setName("孙子");
                    )
            )
    );
    

【要点】

这种包含无数可能性的对象动态性,是jimmer的ORM能够提供更强大功能的根本原因。

2. jimer-sql: 基于不可变对象的ORM

在jimmer的ORM中,实体也是不可变的接口

@Entity
public interface TreeNode {
    
    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "sequence:TREE_NODE_ID_SEQ"
    )
    long id();
    
    @Key // jimmer注解, `name()`是业务主键,
    // 当对象的id属性未被指定时,使用业务主键
    String name();

    @Key // 这也是业务主键
    @ManyToOne
    @OnDelete(DeleteAction.DELETE)
    TreeNode parent();

    @OneToMany(mappedBy = "parent")
    List<TreeNode> childNodes();
}

【注意】

虽然jimmer使用了一些JPA注解来完成实体和表之间的映射,但jimmer并不是JPA。

2.1 Save指令:将任意复杂的对象树保存到数据库中

无论复杂业务对数据库的更新有多复杂,只要能通过一颗对象树来表达,都可以一个API调用存入数据库。

  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("CurrentNode")
                .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("CurrentNode")
                .setParent(parent ->
                    parent
                        .setName("父亲")
                        .setParent(grandParent ->
                            grandParent.setName("祖父")
                        )
                )
                .addIntoChildNodes(child ->
                    child
                        .setName("子-1")
                        .addIntoChildNodes(grandChild ->
                            grandChild.setName("孙-1-1")
                        )
                       .addIntoChildNodes(grandChild ->
                            grandChild.setName("孙-1-2")
                        )
                )
                .addIntoChildNodes(child ->
                    child
                        .setName("子-2")
                        .addIntoChildNodes(grandChild ->
                            grandChild.setName("孙-2-1")
                        )
                        .addIntoChildNodes(grandChild ->
                            grandChild.setName("孙-2-2")
                        )
                )
        )
    ).configure(it ->
        // 如果关联对象在数据库中不存在,自动插入它们
        it.setAutoAttachingAll()
    ).execute();
    

2.2 对象抓取器:从数据库中查询任意复杂的对象树

非GraphQL,但胜似GraphQL。

  1. 查询根节点 (TreeNodeTable是annotation processor生成的Java类)

    List<TreeNode> rootNodes = sqlClient
        .createQuery(TreeNodeTable.class, (q, treeNode) -> {
            q.where(treeNode.parent().isNull()) // 只查根节点
            return q.select(treeNode);
        })
        .execute();
    
  2. 查询根节点,并附带它们的子节点 (TreeNodeFetcher是annotation processor生成的java类)

    List<TreeNode> rootNodes = sqlClient
        .createQuery(TreeNodeTable.class, (q, treeNode) -> {
            q.where(treeNode.parent().isNull()) // 只查根节点
            return q.select(
                treeNode.fetch(
                    TreeNodeFetcher.$
                        .allScalarFields()
                        .childNodes(
                            TreeNodeFetcher.$
                                .allScalarFields()
                        )
                )
            );
        })
        .execute();
    
  3. 查询根节点,并附带两级的子节点

    有两种方法可以实现此功能

    • 指定更深的对象格式

      List<TreeNode> rootNodes = sqlClient
      .createQuery(TreeNodeTable.class, (q, treeNode) -> {
          q.where(treeNode.parent().isNull()) // 只查根节点
          return q.select(
              treeNode.fetch(
                  TreeNodeFetcher.$
                      .allScalarFields()
                      .childNodes( // 第一级子节点
                          TreeNodeFetcher.$
                              .allScalarFields()
                              .childNodes( // 第二级子节点
                                  TreeNodeFetcher.$
                                      .allScalarFields()
                              )
                      )
              )
          );
      })
      .execute();
      
    • 你也可以指定子关联属性的深度,这是更好的方法

      List<TreeNode> rootNodes = sqlClient
      .createQuery(TreeNodeTable.class, (q, treeNode) -> {
          q.where(treeNode.parent().isNull()) // 只查根节点
          return q.select(
              treeNode.fetch(
                  TreeNodeFetcher.$
                      .allScalarFields()
                      .childNodes(
                          TreeNodeFetcher.$
                              .allScalarFields(),
                          it -> it.depth(2) // 抓取两层
                      )
              )
          );
      })
      .execute();
      
  4. 查询根节点, 递归获取所有子节点,无论多深

    List<TreeNode> rootNodes = sqlClient
    .createQuery(TreeNodeTable.class, (q, treeNode) -> {
        q.where(treeNode.parent().isNull()) // 只查根节点
        return q.select(
            treeNode.fetch(
                TreeNodeFetcher.$
                    .allScalarFields()
                    .childNodes(
                        TreeNodeFetcher.$
                            .allScalarFields(),
    
                        // 递归查询,无论多深
                        it -> it.recursive() 
                    )
            )
        );
    })
    .execute();
    
  5. 查询根节点, 由开发人员控制每一个节点是否需要递归查询更深的子节点

    List<TreeNode> rootNodes = sqlClient
    .createQuery(TreeNodeTable.class, (q, treeNode) -> {
        q.where(treeNode.parent().isNull()) // 只查根节点
        return q.select(
            treeNode.fetch(
                TreeNodeFetcher.$
                    .allScalarFields()
                    .childNodes(
                        TreeNodeFetcher.$
                            .allScalarFields(),
                        it -> it.recursive(args ->
                            // - 如果节点名不以`Tmp_`开头,
                            // 不查询更深的子节点;
                            //
                            // - 否则, 
                            // 递归查询更深的子节点。
                            !args.getEntity().name().startsWith("Tmp_")
                        )
                    )
            )
        );
    })
    .execute();
    

2.3 动态表连接

一个myBatis难以实现的高级SQL拼接功能,特别实用。

为了开发强大的动态查询,仅支持动态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

当参数parentNamegrandParent都被指定时,表连接路径current -> parentcurrent -> 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_

/* Java代码中两个JOIN被合并为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 更智能的分页查询。

自动根据data-query生成并优化count-query,从此告别复杂分页查询要构建两个查询的问题。

通常,分页查询需要两条SQL语句,一条查询记录数,一条查询一页数据,我们称之为count-query和data-query。

开发者只需要编写data-query,jimmer-sql会自动创建count-query。


// 开发人员创建data-query
ConfigurableTypedRootQuery<TreeNodeTable, TreeNode> dataQuery = 
    sqlClient
        .createQuery(TreeNodeTable.class, (q, treeNode) -> {
            q
                .where(treeNode.parent().isNull())
                .orderBy(treeNode.name());
            return q.select(book);
        });

// 框架自动生成count-query
TypedRootQuery<Long> countQuery = dataQuery
    .reselect((oldQuery, book) ->
        oldQuery.select(book.count())
    )
    .withoutSortingAndPaging();

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

// 执行data-query
List<TreeNode> someRootNodes = 
    dataQuery
        // limit(limit, offset), 从1/3处到2/3处
        .limit(rowCount / 3, rowCount / 3)
        .execute();

另外,框架不但能能自动生成count-query,还会竭尽全力优化count-query。更多细节请查看文档,此处不再赘述。

 类似资料: