JAVA SQL 查询结果映射之复杂映射
这是 SQL 查询结果映射的第二篇。我们在结果集映射的第一篇文章《基础篇》中了解了一些基本的结果类型映射。本文中,我们将定义更复杂的映射,这些映射可以将查询结果映射到多个实体,并处理无法映射到特定实体的其他字段。
示例
在我们深入研究更复杂的映射之前,让我们先看看将用于示例的实体模型。我们在本系列的第一篇文章中使用了 Author 实体,该实体具有 id、版本、名字和姓氏。对于更复杂的映射,我们需要额外的 Book
实体,该实体具有 id、版本、标题和对作者的引用。简单地说,每本书只由一位作者写作。
如何映射实体
在实际应用中,我们经常使用一个查询检索多个实体,以避免需要初始化懒加载的额外的查询。如果我们使用原生查询或者存储的过程调用完成此操作,我们将获得一个 List 而非实体。然后我们需要提供一个自定义映射,该映射告诉 EntityManager
这些 Object[]
应该映射到哪些实体以及如何完成映射。
下例中,我们定义了一个查询,使之在一个查询中返回 book 及其 author。
SELECT b.id, b.title, b.author_id, b.version, a.id as authorId, a.firstName, a.lastName, a.version as authorVersion FROM Book b JOIN Author a ON b.author_id = a.id
由于 Author
和 Book
表都有 id
和 version
字段,我们需要在 SQL 语句中对其进行冲命名。此处将 Author
的 id
和 version
字段重命名为 authorId
和 authorVersion
。Book 的字段不做改变。那么,我们如何定义一个 SQL 结果集映射,将返回的 Object[]
的列表(List
)转换为完全初始化的 Book 和 Author 实体的列表(List
)呢?该映射定义类似于我们在基础篇中的自定义映射。就像前文讨论的映射,@SqlResultMapping
定义了映射名,稍后我们将用此名称来引用该映射。此处主要的不同是,我们提供了两个 @EntityResult
注释,一个用于 Book 实体,一个用于 Author 实体。@EntityResult
看起来与前面的映射相似,它定义了实体类以及 @FieldResult
映射的列表。
@SqlResultSetMapping(
name = "BookAuthorMapping",
entities = {
@EntityResult(
entityClass = Book.class,
fields = {
@FieldResult(name = "id", column = "id"),
@FieldResult(name = "title", column = "title"),
@FieldResult(name = "author", column = "author_id"),
@FieldResult(name = "version", column = "version")}),
@EntityResult(
entityClass = Author.class,
fields = {
@FieldResult(name = "id", column = "authorId"),
@FieldResult(name = "firstName", column = "firstName"),
@FieldResult(name = "lastName", column = "lastName"),
@FieldResult(name = "version", column = "authorVersion")})})
如果你不想将这样大的注释块添加到实体中,你也可以在 XML 文件中定义该映射。如前所述,默认映射文件叫做 orm.xml
,如果将其添加到 jar 文件的 META-INFO
目录将会被自动使用。
该映射定义本身看起来类似于已经描述的基于注释的映射定义。
<sql-result-set-mapping name="BookAuthorMappingXml">
<entity-result entity-class="org.thoughts.on.java.jpa.model.Author">
<field-result name="id" column="authorId"/>
<field-result name="firstName" column="firstName"/>
<field-result name="lastName" column="lastName"/>
<field-result name="version" column="authorVersion"/>
</entity-result>
<entity-result entity-class="org.thoughts.on.java.jpa.model.Book">
<field-result name="id" column="id"/>
<field-result name="title" column="title"/>
<field-result name="author" column="author_id"/>
<field-result name="version" column="version"/>
</entity-result>
</sql-result-set-mapping>
现在我们有了自定义结果集的映射定义,它定义了查询结果与 Book 和 Author 实体直接的映射。如果我们将此提供给 EntityManager
的 createNativeQuery(String sqlString, String resultSetMapping)
方法,我们将获得一个列表(List)。这可能不像是我们最初想要实现的。我们想去掉这些 Object[]
。如果我们更详细地查看数组中的对象,我们会发现这些对象不再是查询的不同字段,而是 Book
和 Author
实体。由于 EntityManager
知道这两个实体是相互关联的,因此 Book
实体上的关系已经初始化。
List<Object[]> results = this.em.createNativeQuery("SELECT b.id, b.title, b.author_id, b.version, a.id as authorId, a.firstName, a.lastName, a.version as authorVersion FROM Book b JOIN Author a ON b.author_id = a.id", "BookAuthorMapping").getResultList();
results.stream().forEach((record) -> {
Book book = (Book)record[0];
Author author = (Author)record[1];
// do something useful
});
如何匹配其他字段
另一个非常方便的功能是在查询结果中映射其他字段。如果我们想检索所有作者(Author)及其图书(Book)数量,我们可以定义以下查询。
SELECT a.id, a.firstName, a.lastName, a.version, count(b.id) as bookCount FROM Book b JOIN Author a ON b.author_id = a.id GROUP BY a.id, a.firstName, a.lastName, a.version
那么如何将此查询结果映射到 Author
实体和另一个 Long
值?这很简单,我们只需要将 Author
实体的映射与额外的@ColumnResult
定义结合起来。Author
实体的映射必须定义所有字段的映射,即使我们没有像下面的示例中那样更改任何内容。@ColumnResult
定义了要映射的字段的名称,并可以选择指定要转换成的 Java 类型。我用它将查询默认返回的 BigInteger
转换为 Long
。
@SqlResultSetMapping(
name = "AuthorBookCountMapping",
entities = @EntityResult(
entityClass = Author.class,
fields = {
@FieldResult(name = "id", column = "id"),
@FieldResult(name = "firstName", column = "firstName"),
@FieldResult(name = "lastName", column = "lastName"),
@FieldResult(name = "version", column = "version")}),
columns = @ColumnResult(name = "bookCount", type = Long.class))
如前,该映射也可以使用 XML 配置来定义。
<sql-result-set-mapping name="AuthorBookCountMappingXml">
<entity-result entity-class="org.thoughts.on.java.jpa.model.Author">
<field-result name="id" column="id"/>
<field-result name="firstName" column="firstName"/>
<field-result name="lastName" column="lastName"/>
<field-result name="version" column="version"/>
</entity-result>
<column-result name="bookCount" class="java.lang.Long" />
</sql-result-set-mapping>
如果我们在 EntityManager
的 createNativeQuery(String sqlString, String resultSetMapping)
中使用此映射,我们将获得一个包含初始化 Author
实体的列表(List)以及其书(Book)的数量作为 Long 值。
List<Object[]> results = this.em.createNativeQuery("SELECT a.id, a.firstName, a.lastName, a.version, count(b.id) as bookCount FROM Book b JOIN Author a ON b.author_id = a.id GROUP BY a.id, a.firstName, a.lastName, a.version", "AuthorBookCountMapping").getResultList();
results.stream().forEach((record) -> {
Author author = (Author)record[0];
Long bookCount = (Long)record[1];
System.out.println("Author: ID ["+author.getId()+"] firstName ["+author.getFirstName()+"] lastName ["+author.getLastName()+"] number of books ["+bookCount+"]");
});
如果你的查询变得复杂,且结果不能确切地映射到实体模型,那么这类映射相当方便。原因可能是数据库计算的其他属性,如我们在上面的示例中所做的那样,或者是仅从关联表中检索某些特定字段的查询。
结论
本系列的第一篇文章中,我们查看了将查询结果映射到实体的一些基本方法。不过这对于真实的应用是不够的。因此本文中我们创建了一些更复杂的映射:
- 通过注释多个 @EntityResult 注释将查询结果映射到多个实体,以及
- 使用 @ColumnResult 注释处理不属于实体的字段。
后续的文章,我们将使用 JPA 2.1 中引入的构造函数结果映射,并查看一些 Hibernate 特有的功能。