将NativeQuery结果映射到POJO

mhl*_*dry 34 java hibernate jpa pojo

我试图使用带@ConstructorResult的@SqlResultSetMapping将Native查询的结果映射到POJO.这是我的代码:

@SqlResultSetMapping(name="foo",
    classes = {
        @ConstructorResult(
                targetClass = Bar.class,
                columns = {
                    @ColumnResult(name = "barId", type = Long.class),
                    @ColumnResult(name = "barName", type = String.class),
                    @ColumnResult(name = "barTotal", type = Long.class)
                })
    })

public class Bar {

private Long barId;
private String barName;
private Long barTotal;

...
Run Code Online (Sandbox Code Playgroud)

然后在我的DAO中:

Query query = em.createNativeQueryBar(QUERY, "foo");
... set some parameters ...
List<Bar> list = (List<Bar>) query.getResultList();
Run Code Online (Sandbox Code Playgroud)

我已经读过这个功能仅在JPA 2.1中受支持,但这正是我正在使用的.这是我的依赖:

        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.1-api</artifactId>
            <version>1.0.0.Final</version>
        </dependency>
Run Code Online (Sandbox Code Playgroud)

我发现了一些资源,包括这一个:jpa 2.1中的@ConstructorResult映射.但我仍然没有运气.

我错过了什么?为什么不能找到SqlResultSetMapping?

javax.persistence.PersistenceException: org.hibernate.MappingException: Unknown SqlResultSetMapping [foo]
Run Code Online (Sandbox Code Playgroud)

zbi*_*big 32

@SqlResultSetMapping注释不应该放在POJO上.把它放在(任何)@Entity课堂上."Unknown SqlResultSetMapping [foo]"告诉您,JPA提供程序在名称'foo'下没有看到任何映射.请参阅我的另一个答案,以获得正确的示例

  • 您要映射到的对象(`@ ConstructorResult`的`targetClass`)不必是实体.但是`@ SqlResultSetMapping`必须注释一个Entity类.原因是JPA提供程序扫描所有实体类,并从那里获取`@ SqlResultSetMapping`配置.不扫描非实体类. (11认同)
  • 我不明白为什么Object需要是一个实体.你自己说它不需要:"在JPA 2.1版本中添加了使用@ConstructorResult映射到POJO类." (3认同)
  • 值得一提的是,您可以使用“@MappedSuperClass”注释 POJO,然后“@SqlResultSetMapping”将被考虑在内。这不是最优雅的解决方案,但对我来说比注释另一个随机实体更好。 (3认同)

wil*_*oop 11

简短的工作示例:

  • DTO POJO课程

    @lombok.Getter
    @lombok.AllArgsConstructor
    public class StatementDto {
        private String authorName;
        private Date createTime;
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 存储库bean:

    @Repository
    public class StatementNativeRepository {      
        @PersistenceContext private EntityManager em;
    
        static final String STATEMENT_SQLMAP = "Statement-SQL-Mapping";
    
        public List<StatementDto> findPipelinedStatements() {
            Query query = em.createNativeQuery(
                "select author_name, create_time from TABLE(SomePipelinedFun('xxx'))",
                STATEMENT_SQLMAP);
            return query.getResultList();
        }
    
        @SqlResultSetMapping(name= STATEMENT_SQLMAP, classes = {
            @ConstructorResult(targetClass = StatementDto.class,
                columns = {
                    @ColumnResult(name="author_name",type = String.class),
                    @ColumnResult(name="create_time",type = Date.class)
                }
            )
        }) @Entity class SQLMappingCfgEntity{@Id int id;} // <- workaround
    
    }
    
    Run Code Online (Sandbox Code Playgroud)


Vla*_*cea 10

领域模型

让我们考虑一下我们的数据库中有以下内容post和表格:post_comment

JPA SqlResultSet映射 post 和 post_comment 表

日本PASqlResultSetMapping

JPA注释SqlResultSetMapping如下所示:

@Repeatable(SqlResultSetMappings.class)
@Target({TYPE}) 
@Retention(RUNTIME)
public @interface SqlResultSetMapping { 

    String name(); 

    EntityResult[] entities() default {};

    ConstructorResult[] classes() default {};

    ColumnResult[] columns() default {};
}
Run Code Online (Sandbox Code Playgroud)

SqlResultSetMapping 注释是可重复的,并且应用于实体类级别。除了使用 Hibernate 用来注册映射的唯一名称之外,还有三种映射选项:

  • EntityResult
  • ConstructorResult
  • ColumnResult

接下来,我们将了解这三个映射选项如何工作,以及需要使用它们的用例。

JPA SqlResultSetMapping - EntityResult

EntityResult选项允许您将 JDBCResultSet列映射到一个或多个 JPA 实体。

假设我们想要获取前 5 个Post实体以及与PostComment给定模式匹配的所有关联实体title

正如我在本文中所解释的,我们可以使用DENSE_RANK SQL 窗口函数来了解如何过滤postpost_comment连接记录,如以下 SQL 查询所示:

SELECT *
FROM (
  SELECT
    *,
    DENSE_RANK() OVER (
    ORDER BY
      "p.created_on",
      "p.id"
    ) rank
  FROM (
    SELECT
      p.id AS "p.id", p.created_on AS "p.created_on",
      p.title AS "p.title", pc.post_id AS "pc.post_id",
      pc.id as "pc.id", pc.created_on AS "pc.created_on",
      pc.review AS "pc.review"
    FROM post p
    LEFT JOIN post_comment pc ON p.id = pc.post_id
    WHERE p.title LIKE :titlePattern
    ORDER BY p.created_on
  ) p_pc
) p_pc_r
WHERE p_pc_r.rank <= :rank
Run Code Online (Sandbox Code Playgroud)

但是,我们不想返回标量列值的列表。我们希望从此查询返回 JPA 实体,因此我们需要配置注释entities的属性@SqlResultSetMapping,如下所示:

@NamedNativeQuery(
    name = "PostWithCommentByRank",
    query = """
        SELECT *
        FROM (
          SELECT
            *,
            DENSE_RANK() OVER (
            ORDER BY
              "p.created_on",
              "p.id"
            ) rank
          FROM (
            SELECT
              p.id AS "p.id", p.created_on AS "p.created_on",
              p.title AS "p.title", pc.post_id AS "pc.post_id",
              pc.id as "pc.id", pc.created_on AS "pc.created_on",
              pc.review AS "pc.review"
            FROM post p
            LEFT JOIN post_comment pc ON p.id = pc.post_id
            WHERE p.title LIKE :titlePattern
            ORDER BY p.created_on
          ) p_pc
        ) p_pc_r
        WHERE p_pc_r.rank <= :rank
        """,
    resultSetMapping = "PostWithCommentByRankMapping"
)
@SqlResultSetMapping(
    name = "PostWithCommentByRankMapping",
    entities = {
        @EntityResult(
            entityClass = Post.class,
            fields = {
                @FieldResult(name = "id", column = "p.id"),
                @FieldResult(name = "createdOn", column = "p.created_on"),
                @FieldResult(name = "title", column = "p.title"),
            }
        ),
        @EntityResult(
            entityClass = PostComment.class,
            fields = {
                @FieldResult(name = "id", column = "pc.id"),
                @FieldResult(name = "createdOn", column = "pc.created_on"),
                @FieldResult(name = "review", column = "pc.review"),
                @FieldResult(name = "post", column = "pc.post_id"),
            }
        )
    }
)
Run Code Online (Sandbox Code Playgroud)

就位后,我们可以像这样SqlResultSetMapping获取Post和实体:PostComment

List<Object[]> postAndCommentList = entityManager
    .createNamedQuery("PostWithCommentByRank")
    .setParameter("titlePattern", "High-Performance Java Persistence %")
    .setParameter("rank", POST_RESULT_COUNT)
    .getResultList();
Run Code Online (Sandbox Code Playgroud)

并且,我们可以验证实体是否已正确获取:

assertEquals(
    POST_RESULT_COUNT * COMMENT_COUNT, 
    postAndCommentList.size()
);

for (int i = 0; i < COMMENT_COUNT; i++) {
    Post post = (Post) postAndCommentList.get(i)[0];
    PostComment comment = (PostComment) postAndCommentList.get(i)[1];

    assertTrue(entityManager.contains(post));
    assertTrue(entityManager.contains(comment));

    assertEquals(
        "High-Performance Java Persistence - Chapter 1",
        post.getTitle()
    );

    assertEquals(
        String.format(
            "Comment nr. %d - A must read!",
            i + 1
        ),
        comment.getReview()
    );
}
Run Code Online (Sandbox Code Playgroud)

@EntityResult当通过 SQL 存储过程获取 JPA 实体时,这也很有用。查看这篇文章了解更多详细信息。

JPA SqlResultSetMapping - 构造函数结果

假设我们要执行一个聚合查询,计算post_coment每个记录的数量post并返回post title用于报告目的。我们可以使用下面的SQL查询来实现这个目标:

SELECT
  p.id AS "p.id",
  p.title AS "p.title",
  COUNT(pc.*) AS "comment_count"
FROM post_comment pc
LEFT JOIN post p ON p.id = pc.post_id
GROUP BY p.id, p.title
ORDER BY p.id
Run Code Online (Sandbox Code Playgroud)

我们还希望将帖子标题和评论计数封装在以下 DTO 中:

public class PostTitleWithCommentCount {

    private final String postTitle;
    
    private final int commentCount;

    public PostTitleWithCommentCount(
            String postTitle,
            int commentCount) {
        this.postTitle = postTitle;
        this.commentCount = commentCount;
    }

    public String getPostTitle() {
        return postTitle;
    }

    public int getCommentCount() {
        return commentCount;
    }
}
Run Code Online (Sandbox Code Playgroud)

为了将上述SQL查询的结果集映射到DTO PostTitleWithCommentCount,我们可以使用注释classes的属性@SqlResultSetMapping,如下所示:

@NamedNativeQuery(
    name = "PostTitleWithCommentCount",
    query = """
        SELECT
          p.id AS "p.id",
          p.title AS "p.title",
          COUNT(pc.*) AS "comment_count"
        FROM post_comment pc
        LEFT JOIN post p ON p.id = pc.post_id
        GROUP BY p.id, p.title
        ORDER BY p.id
        """,
    resultSetMapping = "PostTitleWithCommentCountMapping"
)
@SqlResultSetMapping(
    name = "PostTitleWithCommentCountMapping",
    classes = {
        @ConstructorResult(
            columns = {
                @ColumnResult(name = "p.title"),
                @ColumnResult(name = "comment_count", type = int.class)
            },
            targetClass = PostTitleWithCommentCount.class
        )
    }
)
Run Code Online (Sandbox Code Playgroud)

ConstructorResult注释允许我们指示 Hibernate 使用什么 DTO 类以及在实例化 DTO 对象时调用哪个构造函数。

请注意,我们使用注释type的属性@ColumnResult来指定comment_count应该转换为 Java int。这是必需的,因为某些 JDBC 驱动程序使用LongBigInteger作为 SQL 聚合函数结果。

这是PostTitleWithCommentCount使用 JPA 调用命名本机查询的方法:

List<PostTitleWithCommentCount> postTitleAndCommentCountList = entityManager
    .createNamedQuery("PostTitleWithCommentCount")
    .setMaxResults(POST_RESULT_COUNT)
    .getResultList();
Run Code Online (Sandbox Code Playgroud)

并且,我们可以看到返回的PostTitleWithCommentCountDTO 已被正确获取:

assertEquals(POST_RESULT_COUNT, postTitleAndCommentCountList.size());

for (int i = 0; i < POST_RESULT_COUNT; i++) {
    PostTitleWithCommentCount postTitleWithCommentCount = 
        postTitleAndCommentCountList.get(i);

    assertEquals(
        String.format(
            "High-Performance Java Persistence - Chapter %d",
            i + 1
        ),
        postTitleWithCommentCount.getPostTitle()
    );

    assertEquals(COMMENT_COUNT, postTitleWithCommentCount.getCommentCount());
}
Run Code Online (Sandbox Code Playgroud)

有关使用 JPA 和 Hibernate 获取 DTO 投影的最佳方法的更多详细信息,请查看这篇文章

JPA SqlResultSetMapping - ColumnResult

前面的示例展示了如何将 SQL 聚合结果集映射到 DTO。但是,如果我们想要返回正在计算评论的 JPA 实体,该怎么办?

为了实现这个目标,我们可以使用属性entities来定义Post我们要获取的实体,并使用注释classes的属性来映射标量值,在我们的例子中是关联记录@SqlResultSetMapping的数量:post_comment

@NamedNativeQuery(
    name = "PostWithCommentCount",
    query = """
        SELECT
          p.id AS "p.id",
          p.title AS "p.title",
          p.created_on AS "p.created_on",
          COUNT(pc.*) AS "comment_count"
        FROM post_comment pc
        LEFT JOIN post p ON p.id = pc.post_id
        GROUP BY p.id, p.title
        ORDER BY p.id
        """,
    resultSetMapping = "PostWithCommentCountMapping"
)
@SqlResultSetMapping(
    name = "PostWithCommentCountMapping",
    entities = @EntityResult(
        entityClass = Post.class,
        fields = {
            @FieldResult(name = "id", column = "p.id"),
            @FieldResult(name = "createdOn", column = "p.created_on"),
            @FieldResult(name = "title", column = "p.title"),
        }
    ),
    columns = @ColumnResult(
        name = "comment_count",
        type = int.class
    )
)
Run Code Online (Sandbox Code Playgroud)

执行PostWithCommentCount指定的本机查询时:

List<Object[]> postWithCommentCountList = entityManager
    .createNamedQuery("PostWithCommentCount")
    .setMaxResults(POST_RESULT_COUNT)
    .getResultList();
Run Code Online (Sandbox Code Playgroud)

我们将获得Post实体和commentCount标量列值:

assertEquals(POST_RESULT_COUNT, postWithCommentCountList.size());

for (int i = 0; i < POST_RESULT_COUNT; i++) {
    Post post = (Post) postWithCommentCountList.get(i)[0];
    int commentCount = (int) postWithCommentCountList.get(i)[1];

    assertTrue(entityManager.contains(post));

    assertEquals(i + 1, post.getId().intValue());
    assertEquals(
        String.format(
            "High-Performance Java Persistence - Chapter %d",
            i + 1
        ),
        post.getTitle()
    );

    assertEquals(COMMENT_COUNT, commentCount);
}
Run Code Online (Sandbox Code Playgroud)


小智 6

我能够这样做:

Session session = em().unwrap(Session.class);
SQLQuery q = session.createSQLQuery("YOUR SQL HERE");
q.setResultTransformer( Transformers.aliasToBean( MyNotMappedPojoClassHere.class) );
List<MyNotMappedPojoClassHere> postList = q.list();
Run Code Online (Sandbox Code Playgroud)

  • 结果转换器可以工作,但它是一个特定于hibernate的解决方案而不是JPA. (2认同)

Vla*_*nik 5

添加@Entity到您的 DTO POJO的问题在于它会在您的数据库中创建一个您不需要的表。必须在@Id必要的字段中添加一个临时关键字也很麻烦。一个简单的解决方案是将您移动@SqlResultSetMapping到抽象类。

@MappedSuperclass
@SqlResultSetMapping(name="foo",
    classes = {
        @ConstructorResult(
                targetClass = Bar.class,
                columns = {
                    @ColumnResult(name = "barId", type = Long.class),
                    @ColumnResult(name = "barName", type = String.class),
                    @ColumnResult(name = "barTotal", type = Long.class)
                })
    })

public abstract class sqlMappingCode {}
Run Code Online (Sandbox Code Playgroud)

不要忘记添加@MappedSuperclass. 这将确保 Hibernate 自动连接您的映射。

更新:此功能在 Hibernate 5.4.30.Final 中被破坏,Jira 票创建了HHH-14572