【译】Spring I&O 社区专家聊 Jimmer ORM

今日,Spring I&O 社区专家 Polivakha Mikhail 发布了一篇关于 Jimmer ORM 框架的文章,由 Baeldung 发表。

这是专门为 SpringIO 社区准备的修订和扩展版本。在其中,Mikhail 揭示了 Jimmer 的主要特征: 没有 JPA 传统、声明式 DTO 和灵活的 DSL,以及与 Spring 的潜在集成。

原文地址:Baeldung - Introduction to Jimmer ORM

下文为原文翻译,如有错译漏译,欢迎指正!Jimmer 也是我非常喜欢的 ORM 框架,希望能借由 Mikhail 之笔,带你领会 Jimmer 的魅力~

1. 简介

在本教程中,我们将回顾 Jimmer ORM 框架。 这个 ORM 在本文撰写时相对较新,但它具有一些很有前途的特性。 我们将回顾 Jimmer 的理念,然后用它编写一些示例。

2. 整体架构

首先,Jimmer 不是 JPA 的实现。这意味着 Jimmer 并不实现所有 JPA 功能。 例如,Jimmer 没有脏检查机制。 然而,值得一提的是,Jimmer 与 Hibernate 一样,有许多相似的概念。 这是有意为之,目的是使从 Hibernate 过渡更加顺畅。 因此,总的来说,JPA 知识对于理解 Jimmer 会很有帮助。

举个例子,Jimmer 有实体的概念,尽管其形状和设计与 Hibernate 有很大不同。 然而,像懒加载或级联这样的概念在 Jimmer 中并不存在。 原因是由于 Jimmer 的设计方式,这些概念在 Jimmer 中并没有太大意义。我们很快就会看到这一点。

本节的最后一点:Jimmer 支持多种数据库, 包括 MySQL、Oracle、PostgreSQL、SQL Server、SQLite 和 H2。

3. 实体示例

如前所述,Jimmer 与 Hibernate 和许多其他 ORM 框架有很多不同之处; 它有几个关键的设计原则。 第一个是我们的实体只有一个目的 - 表示底层数据库的模式。 但是,这里重要的一点是,我们不通过注解指定与之交互的方式。 相反,Jimmer 要求开发者提供所有必要的信息,以便在调用点派生要执行的查询。

那么,这意味着什么?为了理解,让我们回顾以下 Jimmer 实体:

Java 复制代码
import org.babyfish.jimmer.client.TNullable;
import org.babyfish.jimmer.sql.Column;
import org.babyfish.jimmer.sql.Entity;
import org.babyfish.jimmer.sql.GeneratedValue;
import org.babyfish.jimmer.sql.GenerationType;
import org.babyfish.jimmer.sql.Id;
import org.babyfish.jimmer.sql.JoinColumn;
import org.babyfish.jimmer.sql.ManyToOne;
import org.babyfish.jimmer.sql.OneToMany;

@Entity
public interface Book {
    @Id
    @GeneratedValue(strategy = GenerationType.USER)
    long id();

    @Column(name = "title")
    String title();

    @Column(name = "created_at")
    Instant createdAt();

    @ManyToOne
    @JoinColumn(name = "author_id")
    Author author();

    @TNullable
    @Column(name = "rating")
    Long rating();

    @OneToMany(mappedBy = "book")
    List<Page> pages();
}

如你所见,它有类似于 JPA 的注解。 但缺少一件事 - 我们没有为关系指定任何级联,比如我们例子中的 pages。 类似地,对于获取类型(懒加载或急加载) - 在声明端 - 没有指定。 我们也不能指定 @Column 注解的 insertable 或 updatable 属性,就像我们在 JPA 中可能会做的那样,等等。

我们不这样做是因为 Jimmer 期望我们在尝试执行相应操作时明确提供它。 我们将在下面的章节中详细了解这一点。

4. DTO 语言

另一件立即引起我们注意的事情是 Book 是一个接口,而不是一个类。 这是有意为之的,因为在 Jimmer 中,我们不应该直接使用实体, 也就是说,我们不应该实例化它们。相反, 假设我们将通过 DTO 读取和写入数据。 这些 DTO 应该具有我们想要从数据库写入或读取的确切形状。 让我们看一个例子(现在不要关注我们所做的确切 API 调用):

Java 复制代码
public void saveAdHocBookDraft(String title) {
    Book book = BookDraft.$.produce(bookDraft -> {
        bookDraft.setCreatedAt(Instant.now());
        bookDraft.setTitle(title);
        bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
            authorDraft.setId(1L);
        }));
        bookDraft.setId(1L);
    });
    sqlClient.save(book);
}

一般来说,在大多数交互中,我们需要使用 SqlClient 与数据库交互。

在上面的示例中,我们通过 BookDraft 接口创建了一个临时 DTO。 Jimmer 为我们生成了 BookDraft 接口以及 AuthorDraft,这不是手写代码。 如果我们使用 Java,生成本身是在编译时通过 Java 注解处理工具(APT)完成的, 或者如果我们使用 Kotlin,则通过 Kotlin Symbol Processing(KSP)。

这两个生成的接口允许构建任意形状的 DTO 对象, Jimmer 内部稍后将其转换为 Book 实体。 所以,我们确实在保存一个实体,只是我们不是自己实例化它,而是 Jimmer 为我们做的。

5. Null 处理

此外,Jimmer 只会保存 DTO 中存在的组件。 这是因为 Jimmer 严格区分最初未设置的属性和明确设置为 null 的属性。 换句话说,如果我们不想在生成的 SQL 中包含给定的标量属性, 我们只需创建一个没有明确设置它的 DTO。所谓标量, 我们指的是不表示关系属性的字段:

Java 复制代码
public void insertOnlyIdAndAuthorId() {
    Book book = BookDraft.$.produce(
        bookDraft -> {
            bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
                authorDraft.setId(1L);
            }));
            bookDraft.setId(1L);
        });
    sqlClient.insert(book);
}

上述情况下为 Book 生成的 INSERT 语句将如下所示:

Java 复制代码
INSERT INTO BOOK(ID, author_id) VALUES(?, ?)

如果我们明确将标量属性设置为 null, 那么 Jimmer 会在底层 INSERT/UPDATE 语句中包含此属性,并为其分配 null 值:

Java 复制代码
public void insertExplicitlySetRatingToNull() {
    Book book = BookDraft.$.produce(bookDraft -> {
        bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
            authorDraft.setId(1L);
        }));
        bookDraft.setRating(null);
        bookDraft.setId(1L);
    });
    sqlClient.insert(book);
}

生成的 INSERT 语句将如下所示:

Java 复制代码
INSERT INTO BOOK(ID, author_id, rating) VALUES(?, ?, ?)

注意 INSERT 包含了 rating 属性。 在底层 JDBC Statement 中,这个 rating 属性的绑定值将被设置为 null。

最后一点,对于表示关系的属性(非标量属性), 行为更为复杂,值得单独写一篇文章。

6. DTO 爆炸问题

现在,有经验的开发人员可能会注意到一个问题。 Jimmer 的数据库交互方法可能意味着需要创建数十个 DTO,每个用于某个独特的操作。 答案是 - 不完全是。虽然我们确实需要很多 DTO, 但我们可以显著减少手动编写它们的开销。 原因是 Jimmer 有一种专用的 DTO 语言。这里有一个例子:

Java 复制代码
export com.baeldung.jimmer.models.Book
    -> package com.baeldung.jimmer.dto

BookView {
   #allScalars(Book)
   author {
     id
   }
   pages {
    #allScalars(Page)
   }
}

上面的例子代表了用 Jimmer DTO 语言编写的标记。 从这种标记语言生成 POJO 的过程发生在编译时, 与前面章节中的例子一样。

在上面的标记中,例如,我们要求 Jimmer 通过使用 #allScalars 指令在生成的 DTO 中包含所有标量字段。 除此之外,我们还提到 DTO 只会有 Author 的 ID,而不是 Author 本身。 pages 集合将在 DTO 中完整呈现(仅标量字段)。

所以,总的来说,使用 Jimmer,我们确实需要很多 DTO 来描述每种情况下的所需行为。 但我们可以创建临时版本,或者依赖编译器插件在构建过程中为我们生成的 POJO。

7. 读取路径

到目前为止,我们只讨论了将数据保存到数据库的方法。让我们回顾一下读取路径。 为了读取数据,我们还需要通过 DTO 精确指定需要获取哪些数据。 DTO 的形状本身就指示 Jimmer 需要获取哪些字段。 如果字段在 DTO 中不存在,它将不会被获取:

Java 复制代码
public List<BookView> findAllByTitleLike(String title) {
    List<BookView> values = sqlClient.createQuery(BookTable.$)
      .where(BookTable.$.title()
        .like(title))
      .select(BookTable.$.fetch(BookView.class))
      .execute();
        
    return values;
}

这里,我们使用上一节中的 BookView DTO。 我们还可以通过 Fetcher 的临时 API 指定需要读取的列。 它与我们在写入数据库时使用的非常相似:

Java 复制代码
public List<BookView> findAllByTitleLikeProjection(String title) {
    List<Book> books = sqlClient.createQuery(BookTable.$)
      .where(BookTable.$.title()
        .like(title))
      .select(BookTable.$.fetch(Fetchers.BOOK_FETCHER.title()
        .createdAt()
        .author()))
      .execute();

    return books.stream()
      .map(BookView::new)
      .collect(Collectors.toList());
}

在这里,我们使用 Object Fetcher API 构建表示我们想要读取的结构形状的 DTO。 但我们仍然在调用点而不是声明点上指示我们想要读取的列。 这种方法与临时创建用于保存的 DTO 非常相似。

8. 事务管理

最后,我们将快速回顾 Jimmer 管理事务的方式。总的来说, Jimmer 本身没有内置的事务管理机制。因此, Jimmer 严重依赖 Spring Framework 的事务管理基础设施。 例如,让我们回顾本地事务管理的使用(非分布式),这是最常见的场景。 在这种情况下,Jimmer 依赖于 Spring 的 TransactionSynchronizationManager 功能和绑定到当前线程的事务连接。

总结上述内容,Spring 的传统 @Transactional 用法将适用于 Jimmer。 通过 Spring 的 TransactionTemplate 进行命令式事务管理对 Jimmer 也是可能的。

9. 结论

在本文中,我们讨论了 Jimmer ORM。正如我们所看到的,Jimmer 在数据操作方面采取了独特的方法。 虽然 JPA 和(特别是) Hibernate 主要通过注解表达与数据库的交互方式, 但 Jimmer 要求开发者在调用点动态提供所有信息。为此,Jimmer 使用 DTO, 我们通常会通过 Jimmer 本身使用其 DTO 语言生成。然而,我们也可以临时创建它们 (Jimmer 的 Entity)。 在事务管理方面,Jimmer 可以依赖于 Spring Framework 的基础设施。 (译者注:其实在非Spring环境下也可用事务,就像操作JDBC事务那样,Jimmer自带一个简单的额外的事务管理实现。 但是当然了,肯定还是集成使用成熟框架(比如Spring)的事务管理更为方便)

一如既往,本文的源代码可在 GitHub 上获取

结束

好啦!以上就是 Spring I&O 社区专家 Polivakha Mikhail 为大家带来的 Jimmer ORM 介绍了,不知道各位感觉如何? 如果感兴趣的话,不妨也去尝试一下我心目中的 ORM NO.1 Jimmer 吧!

相关推荐
paopaokaka_luck1 分钟前
基于Spring Boot+Vue的吉他社团系统设计和实现(协同过滤算法)
java·vue.js·spring boot·后端·spring
Flobby52936 分钟前
Go语言新手村:轻松理解变量、常量和枚举用法
开发语言·后端·golang
曹朋羽1 小时前
spring cloud sentinel 动态规则配置
spring·spring cloud·sentinel
Warren982 小时前
Java Stream流的使用
java·开发语言·windows·spring boot·后端·python·硬件工程
程序视点3 小时前
IObit Uninstaller Pro专业卸载,免激活版本,卸载清理注册表,彻底告别软件残留
前端·windows·后端
xidianhuihui3 小时前
go install报错: should be v0 or v1, not v2问题解决
开发语言·后端·golang
爱分享的程序员4 小时前
前端面试专栏-前沿技术:30.跨端开发技术(React Native、Flutter)
前端·javascript·面试
进击的铁甲小宝4 小时前
Django-environ 入门教程
后端·python·django·django-environ
掘金码甲哥4 小时前
Go动态感知资源变更的技术实践,你指定用过!
后端
做一位快乐的码农5 小时前
基于Spring Boot和Vue电脑维修平台整合系统的设计与实现
java·struts·spring·tomcat·电脑·maven