Java持久性查询语言(JPGL)是使用JPA从数据库中查询数据的最常见方式。它使你能够重复使用你的映射定义,并且比SQL更容易使用。但它只支持SQL标准的一个小子集,也不支持数据库的特定功能。
那么,如果你需要使用数据库特定的查询功能,或者你的DBA给你一个高度优化的查询,而你又不能将其转化为JPQL,你该怎么办?就这样忽略它,在Java代码中完成所有的工作?
当然不是!JPA有它自己的查询语言,但它被设计成一个泄密的抽象,支持本地SQL查询。你可以用类似于JPQL查询的方式来创建这些查询,如果你想的话,它们甚至可以返回托管实体。
在这篇文章中,我将向你展示如何使用本地SQL查询,将查询结果映射到DTO和实体对象的不同选项,并避免一个常见的性能隐患。
内容
- 1定义和执行一个本地查询
- 1.1创建临时性的本地查询
- 1.2创建命名的本地查询
- 2参数绑定
- 3结果处理
- 4定义查询空间以避免性能问题
- 5结论
定义和执行一个本地查询
像JPQL查询一样,你可以临时定义你的本地SQL查询,或者使用注解来定义一个命名的本地查询。
创建一个临时的本地查询是非常简单的。EntityManager接口为它提供了createNativeQuery方法。它返回一个查询接口的实现,这与你调用*createQuery*方法来创建一个JPQL查询时得到的结果相同。
下面的代码片断显示了一个使用本地查询从作者表中选择姓和名的简单例子。我知道没有必要用本地SQL查询来做这件事。我可以使用一个标准的JPQL查询,但我想把重点放在JPA部分,而不是用一些疯狂的SQL东西来打扰你😉。
持久性提供者不解析SQL语句,因此你可以使用你的数据库支持的任何SQL语句。例如,在我最近的一个项目中,我用它来用Hibernate查询PostgreSQL特定的jsonb列,并将查询结果映射到POJO和实体。
Query q = em.createNativeQuery("SELECT a.firstname, a.lastname FROM Author a");
List<Object[]> authors = q.getResultList();
for (Object[] a : authors) {
System.out.println("Author "
+ a[0]
+ " "
+ a[1]);
}
正如你所看到的,你可以以与任何JPQL查询相同的方式使用创建的查询 。我并没有为结果提供任何映射信息。因为,EntityManager返回一个List ofObject[],你需要事后处理。你也可以提供额外的映射信息,让EntityManager为你做映射,而不是自己对结果进行映射。我将在本篇文章末尾的结果处理部分中详细介绍这一点。
创建命名的本地查询
如果我告诉你,命名的本地查询的定义和用法又与命名的JPQL查询非常相似,你一定不会感到惊讶。
在前面的代码片段中,我创建了一个动态本地查询来选择所有作者的名字。在下面的代码片断中,我使用同样的语句来定义*@NamedNativeQuery*。自从Hibernate 5和JPA 2.2以来,这个注解是可重复的,你可以在你的实体类中添加多个。如果你使用的是旧的JPA或Hibernate版本,你需要用*@NamedNativeQueries*注解来包装它。
@NamedNativeQuery(name = "selectAuthorNames",
query = "SELECT a.firstname, a.lastname FROM Author a")
@Entity
public class Author { ... }
正如你所看到的,该定义看起来与命名的JPQL查询的定义非常相似。正如我将在下一节向你展示的,你甚至可以包括结果映射。但后面会有更多关于这个的内容。
你可以以与命名的JPQL查询完全相同的方式使用*@NamedNativeQuery*。你只需要提供命名的本地查询的名称作为参数给EntityManager的createNamedQuery方法。
Query q = em.createNamedQuery("selectAuthorNames");
List<Object[]> authors = q.getResultList();
for (Object[] a : authors) {
System.out.println("Author "
+ a[0]
+ " "
+ a[1]);
}
参数绑定
与JPQL查询类似,你可以而且应该为你的查询参数使用参数绑定,而不是把值直接放到查询字符串中。这提供了几个优点。
- 你不需要担心SQL注入。
- 持久性提供者将你的查询参数映射到正确的类型,并且
- 持久性提供者可以进行内部优化以提高性能。
JPQL和本地SQL查询使用相同的 查询接口,它为位置参数和命名参数绑定提供了一个*setParameter方法。但是对本地查询的命名参数绑定的支持是Hibernate特有的功能。位置参数在你的本地查询中被引用为"?"*,其编号从1开始。
下面的代码片断显示了一个带有位置绑定参数的临时本地SQL查询的例子。你可以在*@NamedNativeQuery*中以同样的方式使用绑定参数。
Query q = em.createNativeQuery("SELECT a.firstname, a.lastname FROM Author a WHERE a.id = ?");
q.setParameter(1, 1);
Object[] author = (Object[]) q.getSingleResult();
System.out.println("Author "
+ author[0]
+ " "
+ author[1]);
Hibernate也支持本地查询的命名参数绑定,但正如我已经说过的,这不是由规范定义的,可能无法移植到其他JPA实现。
通过使用命名参数绑定,你可以为每个参数定义一个名称,并将其提供给*setParameter方法,以便为其绑定一个值。这个名字是区分大小写的,而且你需要添加":*"符号作为前缀。
Query q = em.createNativeQuery("SELECT a.firstname, a.lastname FROM Author a WHERE a.id = :id");
q.setParameter("id", 1);
Object[] author = (Object[]) q.getSingleResult();
System.out.println("Author "
+ author[0]
+ " "
+ author[1]);
结果处理
正如你在前面的代码片段中所看到的,你的本地查询返回一个Object[]或一个Listof*Object[]。*如果你想以不同的数据结构来检索你的查询结果,你需要向你的持久化提供者提供额外的映射信息。有3个常用的选项。
- 你可以使用实体的映射定义将查询结果的每条记录映射到一个受管实体。
- 你可以使用JPA的*@SqlResultSetMapping*注解,将每个结果记录映射到DTO、管理实体或标量值的组合。
- 你可以使用Hibernate的ResultTransformer将每条记录或整个结果集映射到DTOs、管理实体或标量值。
应用实体映射
重用实体类的映射定义是将查询结果的每条记录映射到托管实体对象的最简单方法。当这样做时,你需要使用你的实体映射定义中使用的别名来选择实体类映射的所有列。
接下来,你需要告诉你的持久化提供者它应该把查询结果映射到哪个实体类。对于一个临时的本地SQL查询,你可以通过提供一个类的引用作为createNativeQuery方法的参数来实现。
Query q = em.createNativeQuery("SELECT a.id, a.version, a.firstname, a.lastname FROM Author a", Author.class);
List<Author> authors = (List<Author>) q.getResultList();
for (Author a : authors) {
System.out.println("Author "
+ a.getFirstName()
+ " "
+ a.getLastName());
}
你可以通过引用实体类作为*@* NamedNativeQuery的resultClass属性,使用*@NamedNative*Query做同样的事情。
@NamedNativeQuery(name = "selectAuthorEntities",
query = "SELECT a.id, a.version, a.firstname, a.lastname FROM Author a",
resultClass = Author.class)
@Entity
public class Author { ... }
当你执行该查询时,Hibernate会自动应用该映射。
使用JPA的*@SqlResultSetMapping*
JPA的*@SqlResultSetMapping*要比之前的灵活得多。你不仅可以使用它将你的查询结果映射到管理的实体对象,还可以映射到DTO、标量值,以及这些的任何组合。唯一的限制是,Hibernate将定义的映射应用于结果集的每条记录。由于这个原因,你不能轻易地将结果集的多个记录分组。
这些映射是相当强大的,但它们的定义可能会变得很复杂。这就是为什么我在这篇文章中只提供了一个快速介绍。如果你想更深入地了解*@SqlResultMappings*,请阅读以下文章。
这里你可以看到一个DTO映射的基本例子。
@SqlResultSetMapping(
name = "BookAuthorMapping",
classes = @ConstructorResult(
targetClass = BookAuthor.class,
columns = {
@ColumnResult(name = "id", type = Long.class),
@ColumnResult(name = "firstname"),
@ColumnResult(name = "lastname"),
@ColumnResult(name = "numBooks", type = Long.class)}))
每个*@SqlResultSetMapping*在持久化单元中都必须有一个唯一的名字。你将在你的代码中使用它来引用这个映射定义。
@ConstructorResult注解告诉Hibernate调用BookAuthor类的构造函数,并提供结果集的id、firstName、lastName和numBooks 字段作为参数。这使你能够实例化非管理的DTO对象,这对所有的只读操作来说是非常合适的。
定义完映射后,你可以将其名称作为第二个参数提供给createNativeQuery方法。然后,Hibernate将在当前的持久化单元中查找映射定义,并将其应用于结果集的每条记录。
Query q = em.createNativeQuery("SELECT a.id, a.firstname, a.lastname, count(b.id) as numBooks FROM Author a JOIN BookAuthor ba on a.id = ba.authorid JOIN Book b ON b.id = ba.bookid GROUP BY a.id",
"BookAuthorMapping");
List<BookAuthor> authors = (List<BookAuthor>) q.getResultList();
for (BookAuthor a : authors) {
System.out.println("Author "
+ a.getFirstName()
+ " "
+ a.getLastName()
+ " wrote "
+ a.getNumBooks()
+ " books.");
}
和前面的例子类似,你可以通过提供映射的名称作为resultSetMapping属性,将相同的映射应用到*@NamedNativeQuery*。
@NamedNativeQuery(name = "selectAuthorValue",
query = "SELECT a.id, a.firstname, a.lastname, count(b.id) as numBooks FROM Author a JOIN BookAuthor ba on a.id = ba.authorid JOIN Book b ON b.id = ba.bookid GROUP BY a.id",
resultSetMapping = "BookAuthorMapping")
@Entity
public class Author { ... }
这样做之后,你可以执行你的*@NamedNativeQuery*,Hibernate会自动应用*@SqlResultSetMapping*。
Query q = em.createNamedQuery("selectAuthorValue");
List<BookAuthor> authors = (List<BookAuthor>) q.getResultList();
for (BookAuthor a : authors) {
System.out.println("Author "
+ a.getFirstName()
+ " "
+ a.getLastName()
+ " wrote "
+ a.getNumBooks()
+ " books.");
}
使用Hibernate特有的ResultTransformer
ResultTransformers是Hibernate特有的功能,与JPA的*@SqlResultSetMapping目标相同。它们允许你定义你的本地查询的结果集的自定义映射。但与@SqlResultSetMapping*不同的是,你将该映射作为Java代码实现,你可以映射每条记录或整个结果集。
Hibernate提供了一套标准的转换器,在Hibernate 6中,自定义转换器的实现变得更加容易。我在《ResultTransformer指南》中详细地解释了所有这些,以及Hibernate版本之间的区别。
下面的代码片段显示了Hibernate 6的TupleTransformer的实现。它应用的映射与之前使用的*@SqlResultSetMapping*相同。
List<BookAuthor> authors = (List<BookAuthor>) session
.createQuery("SELECT a.id, a.firstname, a.lastname, count(b.id) as numBooks FROM Author a JOIN BookAuthor ba on a.id = ba.authorid JOIN Book b ON b.id = ba.bookid GROUP BY a.id")
.setTupleTransformer((tuple, aliases) -> {
log.info("Transform tuple");
BookAuthor a = new BookAuthor();
a.setId((Long) tuple[0]);
a.setFirstName((String) tuple[1]);
a.setLastName((String) tuple[2]);
a.setNumBooks((Integer) tuple[3]);
return a;
}).getResultList();
for (BookAuthor a : authors) {
System.out.println("Author "
+ a.getFirstName()
+ " "
+ a.getLastName()
+ " wrote "
+ a.getNumBooks()
+ " books.");
}
正如你在代码片段中看到的,我调用了setTupleTransformer方法来将转换器添加到查询中。这使得转换器独立于查询,你可以用同样的方式将其应用于*@NamedNativeQuery*。
定义查询空间以避免性能问题
在文章的开头,我提到Hibernate不会解析你的本地SQL语句。这提供了一个好处,即你不局限于Hibernate支持的功能,而是可以使用你的数据库支持的所有功能。
但这也使得我们无法确定查询空间。查询空间描述了你的查询引用了哪些实体类。Hibernate使用它来优化它在执行查询之前必须执行的脏检查和冲洗操作。我在《Hibernate查询空间--优化Flush和Cache操作》中详细解释了这一点。
在使用本地SQL查询时,你需要知道的重要事情是指定查询空间。你可以通过从JPA的查询接口中解包Hibernate的SynchronizeableQuery,并使用对实体类的引用调用addSynchronizedEntityClass 方法来实现。
Query q = em.createNamedQuery("selectAuthorEntities");
SynchronizeableQuery hq = q.unwrap(SynchronizeableQuery.class);
hq.addSynchronizedEntityClass(Author.class);
List<Author> authors = (List<Author>) q.getResultList();
for (Author a : authors) {
System.out.println("Author "
+ a.getFirstName()
+ " "
+ a.getLastName());
}
这告诉Hibernate你的查询引用了哪些实体类。然后,它可以将脏检查限制在这些实体类的对象上,并将它们刷新到数据库中。在这样做的时候,Hibernate会忽略其他实体类的实体对象上的所有变化。这就避免了不必要的数据库操作,并使Hibernate能够应用进一步的性能优化。
总结
JPQL是JPA和Hibernate中最常用的查询语言。它提供了一种简单的方法来查询数据库中的数据。但它只支持SQL标准的一小部分,而且也不支持数据库的特定功能。如果你想使用任何这些功能,你需要使用本地SQL查询。
你可以通过调用EntityManager的createNativeQuery方法并提供SQL语句作为参数来定义一个本地临时查询。或者你可以使用*@NamedNativeQuery注解来定义一个命名的查询,你可以以与JPQL的@NamedQuery*相同的方式执行。
本地查询将其结果作为Object[]或List<Object[]>返回。你可以通过多种方式进行转换。如果你选择了由实体类映射的所有列,你可以提供一个类引用作为createNativeQuery 方法的第二个参数。然后,Hibernate将该类的映射应用于结果集中的每条记录,并返回管理的实体对象。如果你想把结果映射到DTO,你需要定义一个*@SqlResultSetMapping*或者实现一个Hibernate特定的ResultTransformer。
而且你应该总是定义你的本地查询的查询空间。它可以使Hibernate优化它在执行查询之前需要执行的脏检查和刷新操作。