一款以 Jimmer 为核心的 Java 脚手架
https://github.com/ccmjga/mjga-scaffold/tree/model-first
1. 简介
在本教程中,我们将回顾 Jimmer ORM 框架。在撰写本文时,这个 ORM 相对较新,但它有一些有前途的功能。我们将回顾 Jimmer 的哲学,然后用它写一些例子。
2. 整体架构
首先,Jimmer 不是 JPA 实现。这意味着 Jimmer 并没有实现每个 JPA 功能。例如,Jimmer 本身没有脏检查机制 。不过,值得一提的是,Jimmer 和 Hibernate 一样,有很多类似的概念。这是有意为之的,以便使从 Hibernate 的过渡更加顺畅。因此,一般来说,JPA 知识将有助于理解 Jimmer。
例如,Jimmer 有一个实体的概念,尽管它的形状和设计与 Hibernate 有很大不同。但是,延迟加载或级联等概念在 Jimmer 中并不存在。原因是由于其设计方式,它们在 Jimmer 中并没有多大意义。我们很快就会看到这一点。
本节的最后说明:Jimmer 支持多种数据库,包括 MySQL、Oracle、PostgreSQL、SQL Server、SQLite 和 H2。
3. 实体样本
如前所述,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 的注释。但是缺少一件事——我们没有为关系指定任何级联,例如我们的例子中的_页面_ 。与 fetch 类型(lazy 或 eager) 类似——在声明端——它没有指定。我们也不能像在 JPA 等中那样指定 @Column 注释的_可插入_或_可更新_属性。
我们不这样做,因为 Jimmer 希望我们在尝试执行适当的作时显式提供它。我们将在下面的部分中详细介绍。
4. DTO 语言
另一件立即打动我们的事情是, 这本书_是一个_接口 ,而不是_一个类_ 。这是有意为之的,因为在 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 接口创建临时 DTO。Jimmer 为我们生成了 BookDraft 界面和 AuthorDraft,它不是手写代码。如果我们使用的是 Java,则通过 Java 注释处理工具生成本身,如果我们使用的是 Kotlin,则通过 Kotlin 符号处理进行生成。
这两个生成的接口允许构造任意形状的 DTO 对象,Jimmer 稍后在内部将其转换为 Book 实体。 所以,我们确实在拯救一个实体,只是我们不是自己实例化它,而是 Jimmer 为我们做的。
5. 空处理
此外,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 如下所示:
INSERT INTO BOOK(ID, author_id) VALUES(?, ?)
如果我们显式地将标量属性设置为 null,则 Jimmer 会将此属性包含在底层 _INSERT/_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);
}
生成的 INSERT 语句如下所示:
INSERT INTO BOOK(ID, author_id, rating) VALUES(?, ?, ?)
请注意,INSERT 包括 rating 属性。此_评级_属性的绑定值将在基础 JDBC _语句_中设置为 null。
最后,对于表示关系的属性(非标量属性),行为更复杂,值得单独撰写一篇文章。
6. 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 来描述每种情况下所需的行为。但我们可以创建临时版本,也可以依赖编译器插件在构建过程中为我们生成的 POJO。
7. 阅读路径
到目前为止,我们只讨论了将数据保存到数据库中的方法。让我们回顾一下阅读路径。为了读取数据,我们还需要准确指定需要通过 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 的临时 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 非常相似。
7. 事务管理
最后,我们将快速回顾一下 Jimmer 管理事务的方式。一般来说,Jimmer 本身没有内置的事务管理机制。因此,Jimmer 严重依赖 Spring Framework 的事务管理基础设施。例如,让我们回顾一下本地事务管理用法(非分布式),这是最常见的方案。在这种情况下,Jimmer 依赖于 Spring 的 TransactionSynchronizationManager 功能和要绑定到当前线程的事务连接。
综上所述,Spring @Transactional 的传统用法对 Jimmer 来说是有效的。Jimmer 也可以通过 Spring 的 TransactionTemplate 进行命令式事务管理。
8. 结论
在本文中,我们讨论了 Jimmer ORM。正如我们所看到的,Jimmer 在数据作方面采取了一种独特的方法。虽然 JPA 和 Hibernate 主要通过注释来表达与数据库交互的方式,但 Jimmer 要求开发人员在调用站点动态提供所有信息。为此,Jimmer 使用 DTO,我们通常会通过 Jimmer 本身使用其 DTO 语言生成 DTO。但是,我们也可以临时创建它们。在事务管理方面,Jimmer 依赖于 Spring Framework 的基础设施。
与往常一样,本文的源代码可在 GitHub 上找到 。
原文:https://www.baeldung.com/jimmer-orm-intro