JPA 的类型安全查询 API

发布者    |       Hibernate ORM JPA

JPA 2.0 规范的公开草案已经发布,并包括了一个备受期待的功能:一个允许您通过调用 Java 对象的方法来创建查询的 API,而不是通过将 JPA-QL 嵌入由 JPA 实现解析的字符串中。您可以在 Linda 的博客 上了解更多关于公开草案中提出的 API 的信息。

有几种原因更喜欢基于 API 的方法

  • 动态构建查询更容易,可以处理查询结构根据运行时条件而变化的案例。
  • 由于查询是由 Java 编译器解析的,因此无需特殊的工具即可获得语法验证、自动完成和重构支持。

(请注意,JPA-QL 语法验证和自动完成在有些 IDE 中是可用的 - 例如在 JBoss Tools 中。)

Java 语言中关于标准查询 API 的两个主要问题

  • 查询更冗长,可读性较差。
  • 必须使用基于字符串的名称指定属性。

第一个问题实际上不能在没有重大新语言功能的情况下解决(通常称为 DSL 支持)。第二个问题可以通过为 Java 添加类型安全的字面量语法来解决方法字段轻松解决。这是语言迫切需要的特性,它特别适用于与注解结合使用。

曾有人尝试解决缺少方法和字段字面量的不足。一个最近的例子是 LIQUidFORM。不幸的是,该特定方法迫使您将每个持久属性表示为公共 getter 方法,这在 JPA 规范中不是可以接受的限制。

我向 JPA EG 提出了一种不同的方法。该方法分为三个层次

  • JPA 的元模型 API
  • 查询 API,其中类型和属性根据元模型 API 对象指定
  • 支持第三方工具,这些工具可以从实体类生成类型安全的元模型

让我们一层层地来看。

元模型

元模型API有点类似于Java反射API,但它是JPA持久化提供者提供的,了解JPA元数据,并巧妙地使用泛型。(它也使用未检查的异常。)

例如,要获取表示实体的对象,我们调用MetaModel对象

import javax.jpa.metamodel.Entity;
...
Entity<Order> order = metaModel.entity(Order.class);
Entity<Item> item = metaModel.entity(Item.class);
Entity<Product> item = metaModel.entity(Product.class);

要获取实体的属性,我们需要使用基于字符串的名称,就像通常一样

import javax.jpa.metamodel.Attribute;
import javax.jpa.metamodel.Set;
...
Set<Order, Item> orderItems = order.set("items", Item.class);
Attribute<Item, Integer> itemQuantity = item.att("quantity", Integer.class);
Attribute<Item, Product> itemProduct = item.att("product", Product.class);
Attribute<Product, BigDecimal> productPrice = product.att("price", BigDecimal.class)

请注意,代表属性的元模型类型不仅参数化它们所表示的属性类型,而且也参数化它们所属的类型。

还要注意,这段代码是非类型安全的,如果在实体类中不存在具有给定类型和名称的持久属性,则会在运行时失败。这是我们唯一看到的不安全代码 - 我们的目标是使API的其余部分完全类型安全。这有什么帮助呢?嗯,这里的技巧是要注意到元模型对象代表的是持久类的完全静态信息,这种状态在运行时不会改变。因此,我们可以

  • 在系统初始化时获取并缓存这些对象,强制任何错误提前发生,或者甚至
  • 让一个可以访问我们的持久类的工具生成获取和缓存元模型对象的代码。

这比在查询执行时间发生这些错误要好得多,如之前的标准查询提议中那样。

元模型API通常很有用,甚至独立于查询API。目前,由于JPA元数据可能分散在注解和各种XML文档之间,因此很难编写与JPA交互的泛型代码。

但是,当然,元模型最流行的用途是构建查询。

查询

要构建查询,我们将元模型对象传递给QueryBuilderAPI

Query query = queryBuilder.create();

Root<Order> orderRoot = query.addRoot(order);
Join<Order, Item> orderItemJoin = orderRoot.join(orderItems);
Join<Item, Product> itemProductJoin = orderItemJoin.join(itemProduct);

Expression<Integer> quantity = orderItemJoin.get(itemQuantity);
Expression<BigDecimal> price = itemProductJoin.get(productPrice);

Expression<Number> itemTotal = queryBuilder.prod(quantity, price);
Expression<Boolean> largeItem = queryBuilder.gt(itemTotal, 100);

query.restrict(largeItem)
     .select(order)
     .distinct(true);

为了比较,这里是用公开草案中提出的API表达的同查询

Query query = queryBuilder.createQueryDefinition();

DomainObject orderRoot = query.addRoot(Order.class);
DomainObject orderItemJoin = orderRoot.join("items");
DomainObject itemProductJoin = orderItemJoin.join("product");

Expression quantity = orderItemJoin.get("quantity");
Expression price = itemProductJoin.get("price");

Expression itemTotal = quantity.times(price);
Predicate largeItem = queryBuilder.greaterThan(100);

query.where(largeItem);
     .selectDistinct(order);

当然,这个查询可以用这两种API中的任何一种更紧凑地编写,但我在尝试引起人们对构成查询的对象的泛型类型的注意。类型参数阻止我编写这样的内容

orderItemJoin.get(productPrice); //compiler error

泛型的使用意味着编译器可以在我们尝试通过组合一个查询实体和一个其他类型的属性来创建路径表达式时检测到错误。元模型对象productPrice的类型是Attribute<Product, BigDecimal>因此不能传递给get()方法,该方法仅接受orderItemJoin. get()只接受Attribute<Item, ?>,因为orderItemJoin是类型Join<Order, Item>.

Expression表达式也参数化表达式类型,因此编译器可以检测到错误,如

queryBuilder.gt(stringExpression, numericExpression); //error

确实,API具有足够的安全性,几乎不可能构建不可执行的查询。

生成类型安全的元模型

完全可以用元模型API和查询API构建查询。但要真正充分利用这些API,最后一块拼图是一个小的代码生成工具。这种工具不需要由JPA规范定义,不同的工具不需要生成完全相同的代码。然而,生成的代码将在所有JPA实现之间始终是可移植的。这个工具所做的只是反射持久化实体,创建一个或多个类来静态缓存对元模型实体属性对象的引用。

为什么我们需要这个代码生成器?因为编写Attribute<Item, Integer> itemQuantity = item.att("quantity", Integer.class);手动操作既费时又容易出错,而且由于你的重构工具可能不够智能,无法在重构持久化类属性名称时更改基于字符串的名称。代码生成工具不会犯这种错误,并且它们不介意每次让你重新开始工作。

简单来说:该工具使用非类型安全的元模型API构建类型安全的元模型。

最令人兴奋的可能性是,这个代码生成工具可以成为javac的APT插件。你不需要显式运行代码生成器,因为APT现在已完全集成到Java编译器中。(或者,它也可以是一个IDE插件。)

但是代码生成工具不是最近已经过时了吗?Hibernate和JPA等ORM解决方案的伟大特性之一不是它们不依赖于代码生成吗?嗯,我坚信使用任何适合解决当前问题的工具。代码生成当然被应用于它不是最佳解决方案的问题上。另一方面,我并没有看到有人因为ANTLR或JavaCC使用代码生成来解决他们所面对的问题而对其进行抨击。在这种情况下,我们正在解决Java类型系统中的一个具体问题:缺乏类型安全的元模型(反射是设计最糟糕的语言特性之一)。而代码生成只是唯一可行的解决方案。事实上,对于这个问题,它工作得很好。

别担心,生成的代码不会难懂……例如,它可能看起来像这样

public class persistent {
	static Metamodel metaModel;

	public static Entity<model.Order> order = metaModel.entity(model.Order.class);
	public static class Order {
		public static Attribute<model.Order, Long> id = order.id(Long.class);
		public static Set<model.Order, model.Item> items = order.set("items", model.Item.class);
		public static Attribute<model.Order, Boolean> filled = order.att("filled", Boolean.class);
		public static Attribute<model.Order, Date> date = order.att("date", Date.class);
	}

	public static Entity<model.Item> item = metaModel.entity(model.Item.class);
	public static class Item {
		public static Attribute<model.Item, Long> id = item.id(Long.class);
		public static Attribute<model.Item, model.Product> product = item.att("product", model.Product.class);
		public static Attribute<model.Item, model.Order> order = item.att("order", model.Order.class);
		public static Attribute<model.Item, Integer> quantity = item.att("quantity", Integer.class);
	}

	public static Entity<model.Product> product = metaModel.entity(model.Product.class);
	public static class Product {
		public static Attribute<model.Product, Long> id = product.id(Long.class);
		public static Set<model.Product, model.Item> items = product.set("items", model.Item.class);
		public static Attribute<model.Product, String> description = product.att("description", String.class);
		public static Attribute<model.Product, BigDecimal> price = product.att("price", BigDecimal.class);
	}

}

这个类只是让我们可以轻松地引用实体属性。例如,我们可以输入persistent.Order.id来引用id属性,它是Order类的。或者persistent.Product.description来引用descriptionProduct.


返回顶部