Jimmer ORM:提供DTO语言的新数据操作框架

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

首先,Jimmer不是一个JPA实现。这意味着Jimmer并没有实现所有JPA特性。例如,Jimmer本身没有脏检查机制。然而,值得一提的是,Jimmer和Hibernate一样,有很多类似的概念。这是为了使从Hibernate的过渡更平滑。因此,一般来说,JPA知识将有助于理解Jimmer。

例如,Jimmer有一个实体的概念,尽管它的形状和设计与Hibernate有很大的不同。然而,像延迟加载或级联这样的概念在Jimmer中并不存在。原因是它们在Jimmer中没有多大意义,因为它的设计方式。我们很快就会看到。

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

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

那是什么意思为了理解,让我们回顾以下Jimmer实体:

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();
   
// equals and hashcode implementation
}

正如您所注意到的,它具有类似于JPA的注释。但有一点是缺失的--我们没有为关系指定任何级联,例如本例中的页面。类似的获取类型(lazy或eager)-在声明端-没有指定。我们也不能像在JPA等中那样指定@Column注释的可插入或可更新属性。

我们不这样做,因为Jimmer希望我们在尝试执行适当的操作时显式地提供它。我们将在下面的章节中详细了解这一点。

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

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接口创建了一个ad-hoc DTO。Jimmer为我们生成了与AuthorDraft一起沿着的BookDraft界面,它不是手写代码。如果我们使用Java,则在编译时通过Java注释处理工具生成,如果我们使用科特林,则通过科特林符号处理生成。

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

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

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

在上面的例子中,为Book生成的代码看起来像这样:

INSERT INTO BOOK(ID, author_id) VALUES(?, ?)


如果我们显式地将标量属性设置为null,那么Jimmer会将此属性包含在底层的UPDATE/UPDATE语句中,并为其分配一个null值:

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);
}

生成的SQL语句如下所示:

INSERT INTO BOOK(ID, author_id, rating) VALUES(?, ?, ?)
请注意,“等级”包括等级属性。在基础JDBC语句中,此rating属性的绑定值将被设置为null。

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

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

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将只具有作者的ID,而不是作者本身。页面集合将完整地存在于DTO中(仅标量字段)。

因此,总的来说,对于Jimmer,我们确实需要大量DTO来描述每种情况下所需的行为。但是我们可以创建ad-hoc版本,或者依赖于编译器插件在构建过程中为我们生成的POJO。

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

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的ad-hoc API指定需要读取的列。它与我们在写入数据库时使用的方法非常相似:

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进行保存非常相似。

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

综上所述,Spring的@ transmix的传统用法将适用于Jimmer。通过Spring的TransactionTemplate进行命令式事务管理也可以用于Jimmer。

结论
在本文中,我们讨论了Jimmer ORM。正如我们所看到的,Jimmer在数据操作方面采取了独特的方法。特别是JPA和Hibernate,主要通过注释来表达与数据库的交互方式,而Jimmer要求开发人员在调用站点动态提供所有信息。为此,Jimmer使用DTO,我们通常会通过Jimmer本身使用其DTO语言生成DTO。但是,我们也可以创建它们。在事务管理方面,Jimmer依赖于Spring框架的基础设施。