转载自 Substack.
雅加达数据是Java持久化的一种新规范,计划作为EE 11平台的一部分发布。雅加达持久化提供了成熟且功能丰富的对象/关系映射解决方案(如Hibernate)的基础,而雅加达数据旨在提供一种较为简化的编程模型,但同时也适用于非关系型数据库。
“存储库”的概念
雅加达数据是一种存储库的编程模型。规范早期版本包含了我称之为“DAO式”存储库,类似于Spring Data等既定解决方案。在这种方法中,每个实体都有自己的存储库接口,提供针对特定实体的持久化操作。所以对于 Book
,有一个 BookRepository
,对于 Author
,有一个 AuthorRepository
,依此类推。
关注我在 Twitter 上的朋友们知道,我一直对这个方法持怀疑态度。我不想在这里重提所有论点,但让我快速提及以下几点,因为它们帮助推动了(新的)雅加达数据的设计
-
对于关系型数据,按实体划分操作是不自然的。大多数有趣的查询都跨越多个实体,并且经常跨越许多实体,这些实体——就像我最喜欢的
Book
/Author
示例一样——参与多对多关联。将这些操作分配给一个实体是不自然的。请注意,这一点特定于关系型数据。对于文档数据库,DAO式存储库是完全自然的。我真的不想深入这个兔子洞,但让我只给你一个提示,这是O/R映射问题的一个被低估的方面,也是社区某一特定部分完全忽视的一个方面。 -
在一个典型的成熟系统中,查询比实体多得多,这导致DAO充满了许多低内聚的操作。
-
旧框架提供的额外类型安全性很少或没有,代码量最多只能减少到极小的程度,并且在应用程序程序和数据库之间插入了一个完整的额外框架代码层,这使得获取Hibernate的高级和性能相关功能变得更加困难。
-
基于JPA的旧实现提供了一个CRUD风格的API,这是对JPA的状态持久上下文的完全不适当的抽象。在状态持久上下文中,更新是隐式的,所以
update()
方法没有意义。
鉴于这一切,Jakarta Data提供了不与特定实体绑定的存储库。相反,持久化操作可以根据您作为软件设计者所想象的任何分类进行分类。您可能不只有BookRepository
和AuthorRepository
,还可能有Bookstore
和Library
。实际上,这个想法是您可以有多个细粒度的存储库接口,按照客户端或主题进行分类,每个都有很大的内聚性,每个都操作多个实体。
但这并不是改变我对存储库看法的原因,这也不是您开始使用它们而不是仅仅调用EntityManager
的充分理由。对我来说,真正不同的是认识到极大的类型安全潜力,这是旧框架未能正确利用的潜力。这种认识源于我在Hibernate 6.3中开始的工作,但它达到了Jakarta Data规范的高峰。但现在我们跑题了。
在Hibernate上开始使用Jakarta Data
不再含糊其辞。如果您今天想尝试Jakarta Data,您需要M4版本,并且需要Hibernate 6.6的快照构建。
-
这里有一个您可以使用Gradle构建,或者
-
如果您想在Quarkus中尝试它,请查看这个Maven POM。
请注意,启用HibernateProcessor
至关重要,这是现在不恰当地命名的hibernate-jpamodelgen
模块中的注释处理器。该注释处理器包含Hibernate的Jakarta Data实现。
如果您想知道我们为什么使用注释处理器实现Jakarta Data,而不是使用反射、字节码生成或其他类似运行时技术,答案是类型安全。HibernateProcessor
能够在编译时检测到各种不同类型的错误,并以易于理解的消息报告这些问题,就像我们稍后将要看到的那样。这种编译时错误报告是使用Hibernate与Jakarta Data一起使用的主要原因。
当然,我们还需要一些JPA实体类
@Entity
public class Book {
@Id
String isbn;
@Basic(optional = false)
String title;
LocalDate publicationDate;
@Basic(optional = false)
String text;
@Enumerated(STRING)
@Basic(optional = false)
Type type = Type.Book;
@ManyToOne(optional = false, fetch = LAZY)
Publisher publisher;
@ManyToMany(mappedBy = Author_.BOOKS)
Set<Author> authors;
...
}
@Entity
public class Author {
@Id
String ssn;
@Basic(optional = false)
String name;
Address address;
@ManyToMany
Set<Book> books;
}
Jakarta Data还与使用Jakarta NoSQL注释定义的实体一起工作,但在这里我们使用Hibernate ORM,因此这些注释与您从jakarta.persistence
中已经知道的相同。这里没有新的东西。
接下来,让我们创建我们的第一个存储库,为了好玩,我们称之为Library
@Repository
public interface Library {}
@Repository
注释告诉HibernateProcessor
生成该接口的实现。当我们编译代码时,我们获得以下生成的类
@RequestScoped
@Generated("org.hibernate.processor.HibernateProcessor")
public class Library_ implements Library {
protected @Nonnull StatelessSession session;
public Library_(@Nonnull StatelessSession session) {
this.session = session;
}
public @Nonnull StatelessSession session() {
return session;
}
@PersistenceUnit
private EntityManagerFactory sessionFactory;
@PostConstruct
private void openSession() {
session = sessionFactory.unwrap(SessionFactory.class).openStatelessSession();
}
@PreDestroy
private void closeSession() {
session.close();
}
@Inject
Library_() {
}
}
如您所见,这是一个可注入的CDI Bean,它使用Hibernate的StatelessSession
与数据库交互。您可以使用CDI从中获取实例
@Inject Library library;
另一方面,如果您在环境中没有CDI,没有问题,HibernateProcessor
将生成没有CDI依赖的简单代码。您总是可以选择使用new实例化存储库
Library library = new Library_(sessionFactory.openStatelessSession());
如果我们需要直接访问Library
下层的StatelessSession
,我们只需添加一个像这样的方法
@Repository
public interface Library {
StatelessSession session();
}
此方法特别适用于我们的存储库接口具有我们将手动实现的默认方法的情况。
我应该提到的是,我们在编译代码时生成的不仅仅是Library_
类。我们还获得了
-
Jakarta Persistence静态元模型类
Author_
和Book_
,以及 -
雅加达数据静态元模型类
_Author
和_Book
。
让我们希望雅加达平台永远不会需要引入第三个持久化API,因为我们已经用完了我们可以放置下划线的地方。
到目前为止,我们的存储库没有任何有用的操作。
生命周期方法
生命周期方法通过注解指示。在雅加达数据1.0中,有四个生命周期注解,前三个的名称与 StatelessSession
的操作匹配
@Repository
public interface Library {
@Insert
void addToCollection(Book book);
@Delete
void removeFromCollection(Book book);
@Insert
void newAuthor(Author author);
@Update
void updateAuthor(Author author);
}
编译此代码将在 Library_
中生成实现,包括以下方法
@Override
public void addToCollection(Book book) {
if (book == null) throw new IllegalArgumentException("Null book");
try {
session.insert(book);
}
catch (EntityExistsException exception) {
throw new jakarta.data.ex.EntityExistsException(exception);
}
catch (PersistenceException exception) {
throw new DataException(exception);
}
}
我想你会同意这段代码易于理解和调试。
我说雅加达数据1.0中有四个生命周期注解,但我只向你展示了三个。这是第四个
@Save
void addOrUpdate(Book book);
现在,“保存”在这里与Hibernate的Session接口上具有相似名称的旧版(并已弃用)方法绝对无关。它实际上导致了一个SQL merge
语句。以下是生成的实现。
@Override
public void addOrUpdate(Book book) {
if (book == null) throw new IllegalArgumentException("Null book");
try {
session.upsert(book);
}
catch (OptimisticLockException exception) {
throw new OptimisticLockingFailureException(exception);
}
catch (PersistenceException exception) {
throw new DataException(exception);
}
}
没错,@Save
映射到 upsert()
。不错,对吧?
好吧,现在是时候说些坏消息了,至少如果你是JPA风格的状态化持久化上下文粉丝的话。你可能已经在屏幕上大声喊叫,想知道为什么没有 @Persist 、@Merge 、@Refresh 、@Lock 和 @Remove 注解映射到JPA EntityManager 的标准操作。答案很简单,它们不在雅加达数据1.0中,但几乎可以肯定会在以后出现。雅加达数据中的存储库是无状态的,至少现在是这样。 |
一个敏锐的观察者已经注意到了,与直接调用 StatelessSession
的等效方法相比,雅加达数据生命周期方法没有任何优势。但这是因为生命周期方法是无聊的。我们真正关心的是查询。
自动查询方法
自动查询方法是一切表达实体查询的最简单方法。方法的参数表达查询条件。让我们考虑一个最简单的例子
@Find
Book book(String isbn);
这就是查询。
这个查询检索具有给定 isbn
的 Book
。雅加达数据使用方法参数的名称来识别我们用于限制查询结果的字段。
此时,你肯定在骗我,这种荒谬的“查询”不可能类型安全。但你错了
这比你现在使用的任何DAO风格存储库框架都要好,甚至好得多。相当快,IntelliJ自己就会在您输入时报告这些错误,甚至不需要调用注解处理器。
自动查询可以变得更加有趣。这个有点中等有趣
@Find
List<Book> booksByTitle(@Pattern String title, Type type,
Order<Book> order, Limit limit);
今天我们不会走得更远,因为我有更好的东西要告诉你。然而,我应该向你展示我们刚才看到的方法的生成代码。
对于我们的第一个例子,HibernateProcessor
识别出 isbn
是 Book
的主键,并生成了以下代码
@Override
public Book book(String isbn) {
if (isbn == null) throw new IllegalArgumentException("Null isbn");
try {
return session.get(Book.class, isbn);
}
catch (NoResultException exception) {
throw new EmptyResultException(exception);
}
catch (NonUniqueResultException exception) {
throw new jakarta.data.ex.NonUniqueResultException(exception);
}
catch (PersistenceException exception) {
throw new DataException(exception);
}
}
对于第二个“中等有趣”的例子,我们得到
@Override
public List<Book> booksByTitle(String title, Type type, Order<Book> order, Limit limit) {
if (type == null) throw new IllegalArgumentException("Null type");
var _builder = session.getFactory().getCriteriaBuilder();
var _query = _builder.createQuery(Book.class);
var _entity = _query.from(Book.class);
_query.where(
title==null
? _entity.get(Book_.title).isNull()
: _builder.like(_entity.get(Book_.title), title),
_builder.equal(_entity.get(Book_.type), type)
);
var _orders = new ArrayList<Order<? super Book>>();
for (var _sort : order.sorts()) {
_orders.add(by(Book.class, _sort.property(),
_sort.isAscending() ? ASCENDING : DESCENDING,
_sort.ignoreCase()));
}
try {
return session.createSelectionQuery(_query)
.setFirstResult((int) limit.startAt() - 1)
.setMaxResults(limit.maxResults())
.setOrder(_orders)
.getResultList();
}
catch (PersistenceException exception) {
throw new DataException(exception);
}
}
乍一看可能有点可怕,但如果你了解JPA CriteriaQuery
API,你会很快理解它。注意,即使是这个方法的生成实现也是完全静态类型安全的。
注解查询方法
注解查询方法是指查询以Jakarta Data查询语言(JDQL)、Jakarta持久化查询语言(JPQL)或原生SQL之类的语言表达的方法。JDQL是JPQL的严格子集,因此Hibernate支持这三个选项。
@Query
注解允许我们指定用JDQL或JPQL编写的查询
@Query("select b " +
"from Book b join b.authors a " +
"where a.name = :authorName " +
"order by a.ssn, b.isbn")
List<Book> booksBy(String authorName);
好吧,这次你肯定把我困住了。不可能这个糟糕的字符串是类型安全的!每个人都知道嵌入在Java字符串中的查询是如何工作的。
错了
是的,这是正确的:`HibernateProcessor` 不仅在编译时检查您的查询语法,还检查整个查询的类型!这背后有一些相当复杂的机制,我们花费了数年时间来构建这些机制。
但是,JetBrains的人们也有一些非常棒的机制,IntelliJ 也很快就能做到这一点。
为了实现更高的类型安全,有些人提倡使用复杂、几乎无法阅读的内部 DSL,比如 `CriteriaQuery`(我参与设计了它,所以请不要对那些形容词感到不满),或者 JOOQ,或者其他。我现在认为这种方法已经过时了。JPQL 比条件查询更容易阅读。
当然,当我们真正需要动态构建查询时,内部 DSL 仍然很受欢迎。(Jakarta Data 的未来版本也将提供一种方法来部分解决这个问题!)所以 `CriteriaQuery` 还没有完全过时。
哦,对了,差点忘了:生成的代码。到现在你可能已经猜到了。
@Override
public List<Book> booksBy(String authorName) {
try {
return session.createSelectionQuery(BOOKS_BY_String, Book.class)
.setParameter("authorName", authorName)
.getResultList();
}
catch (PersistenceException exception) {
throw new DataException(exception);
}
}
当然,我还可以说更多关于 JDQL 和一般查询方法的事情。一个特别有趣的话题——我将留到未来的文章中讨论——是基于偏移量和基于键(或“键集”)的分页。
当前状态
Jakarta Data 1.0 和 Jakarta Persistence 3.2 现在非常接近发布,并且它们是十多年来 Java 持久性领域的最大新闻。我们已经在 Hibernate 7.0 中完成 Persistence 3.2 的实现。我们的 Jakarta Data 实现在 Hibernate 6 分支上完成,并将首先在 6.5 或 6.6 中提供。
继续阅读 第二部分。