编程

JAVA SQL 查询结果映射之复杂映射

459 2024-07-18 05:14:00

这是 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

由于 AuthorBook 表都有 idversion 字段,我们需要在 SQL 语句中对其进行冲命名。此处将 Authoridversion 字段重命名为 authorIdauthorVersion。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 实体直接的映射。如果我们将此提供给 EntityManagercreateNativeQuery(String sqlString, String resultSetMapping) 方法,我们将获得一个列表(List)。这可能不像是我们最初想要实现的。我们想去掉这些 Object[]。如果我们更详细地查看数组中的对象,我们会发现这些对象不再是查询的不同字段,而是 BookAuthor 实体。由于 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>

如果我们在 EntityManagercreateNativeQuery(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 特有的功能。