实体模型

假设我们有以下 Post 实体:

Post Domain 如果你需要修改实体,则需要抓取整个实体;但是如果你只是对其中的几列感兴趣,则使用 DTO 会更有效。

如果我们只是想选择 Postidtitle,如果抓取整个实体会比较浪费资源,接下来我们看下 JPA 和 Hibernate 怎么实现我们的目标。

使用 JPA 映射 DTO

在使用 JPA 或 Hibernate 查询实体的时候,你可以通过执行 JPQL 或着 Criteria API 以及原生的 SQL 查询。

  • 使用 Tuple 和 JPQL 映射 DTO

如果你不想将映射应用到 DTO,你可以使用 JPA 的 Tuple, 如果使用 Tuple 映射,你的 JPQL 查询看起来是这样的:

List<Tuple> postDTOs = entityManager
.createQuery(
   "select " +
   "       p.id as id, " +
   "       p.title as title " +
   "from Post p " +
   "where p.createdOn > :fromTimestamp", Tuple.class)
.setParameter( "fromTimestamp", Timestamp.from(
   LocalDateTime.of( 2016, 1, 1, 0, 0, 0 )
       .toInstant( ZoneOffset.UTC ) ))
.getResultList();

assertFalse( postDTOs.isEmpty() );

Tuple postDTO = postDTOs.get( 0 );
assertEquals( 
   1L, 
   postDTO.get( "id" ) 
);

assertEquals( 
   "High-Performance Java Persistence", 
   postDTO.get( "title" ) 
);

如您所见,tuple 是一种获取 DTO 投影的便捷方式,因为您不需要为需要支持的每种类型的投影指定 DTO 类。

  • 使用构造函数和 JPQL 映射 DTO

如果你想使用特定的类来映射 DTO,你可以使用构造函数来 New 一个你想要的参数列表的对象。

DTO 类必须提供一个全参的构造函数来映射结果

DTO 映射如下:

public class PostDTO {

    private Long id;

    private String title;

    public PostDTO(Number id, String title) {
        this.id = id.longValue();
        this.title = title;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }
}

因此,使用构造函数的 JPQL 查询如下:

List<PostDTO> postDTOs = entityManager
.createQuery(
   "select new " +
   "   com.vladmihalcea.book.hpjp.hibernate.query.dto.projection.jpa.PostDTO(" +
   "       p.id, " +
   "       p.title " +
   "   ) " +
   "from Post p " +
   "where p.createdOn > :fromTimestamp", PostDTO.class)
.setParameter( "fromTimestamp", Timestamp.from(
   LocalDateTime.of( 2016, 1, 1, 0, 0, 0 )
       .toInstant( ZoneOffset.UTC ) ))
.getResultList();
  • 使用 Tuple 和原生 SQL 查询 映射 DTO

从 Hibernate ORM 5.2.11开始,由于 HHH-11897 Jira 问题得到修复,您可以使用 Tuple 进行原生 SQL 查询。

List<Tuple> postDTOs = entityManager
.createNativeQuery(
   "SELECT " +
   "       p.id AS id, " +
   "       p.title AS title " +
   "FROM Post p " +
   "WHERE p.created_on > :fromTimestamp", Tuple.class)
.setParameter( "fromTimestamp", Timestamp.from(
   LocalDateTime.of( 2016, 1, 1, 0, 0, 0 )
       .toInstant( ZoneOffset.UTC ) ))
.getResultList();

assertFalse( postDTOs.isEmpty() );

Tuple postDTO = postDTOs.get( 0 );
assertEquals( 
   1L, 
   ((Number) postDTO.get( "id" )).longValue() 
);

assertEquals( 
   "High-Performance Java Persistence", 
   postDTO.get( "title" ) 
);
  • 使用 ConstructorResult 映射 DTO

对于原生的 SQl 查询,你不能使用构造函数,所以你需要使用一个命名原生查询(NamedNativeQuery)和配置一个 SqlResultSetMapping,这样你就可以通过构造函数或字段来填充 DTO 类:

@NamedNativeQuery(
    name = "PostDTO",
    query =
        "SELECT " +
        "       p.id AS id, " +
        "       p.title AS title " +
        "FROM Post p " +
        "WHERE p.created_on > :fromTimestamp",
    resultSetMapping = "PostDTO"
)
@SqlResultSetMapping(
    name = "PostDTO",
    classes = @ConstructorResult(
        targetClass = PostDTO.class,
        columns = {
            @ColumnResult(name = "id"),
            @ColumnResult(name = "title")
        }
    )
)

使用下面的代码执行 SQL 映射:

List<PostDTO> postDTOs = entityManager
.createNamedQuery("PostDTO")
.setParameter( "fromTimestamp", Timestamp.from(
    LocalDateTime.of( 2016, 1, 1, 0, 0, 0 )
        .toInstant( ZoneOffset.UTC ) ))
.getResultList();

使用 Hibernate 映射 DTO

当然您可以将所有 JPA 特性在 Hibernate 上使用,因为 Hibernate 提供的特性比标准 Java Persistence 规范要多得多。

  • 使用 ResultTransformer 和 JPQL 来映射 DTO

如前所述,ResultTransformer 允许您以任何方式自定义结果集,以便您可以使用它将典型的 Object [] 数组投影转换为 DTO 结果集。

这次,您不需要提供构造函数来匹配查询选择的实体属性。

虽然你甚至不需要在你的DTO类中提供 setter,但是我们需要 setter,因为 id 列在数据库映射时会返回 BigInteger,而我们需要将它强制转换为 Long。

Hibernate 可以使用 Reflection 设置适当的字段,因此它比以前的 JPA 构造函数替代方案更灵活。

考虑下面的 DTO 类:

public class PostDTO {

   private Long id;

   private String title;

   public Long getId() {
       return id;
   }

   public void setId(Number id) {
       this.id = id.longValue();
   }

   public String getTitle() {
       return title;
   }

   public void setTitle(String title) {
       this.title = title;
   }
}

我们可以使用 Hibernate 特定 org.hibernate.query.Query 接口的 setResultTransformer 方法转换结果集,该接口可以从 JPA Query 解析。

List<PostDTO> postDTOs = entityManager
.createQuery(
   "select " +
   "       p.id as id, " +
   "       p.title as title " +
   "from Post p " +
   "where p.createdOn > :fromTimestamp")
.setParameter( "fromTimestamp", Timestamp.from(
   LocalDateTime.of( 2016, 1, 1, 0, 0, 0 ).toInstant( ZoneOffset.UTC ) ))
.unwrap( org.hibernate.query.Query.class )
.setResultTransformer( Transformers.aliasToBean( PostDTO.class ) )
.getResultList();
  • 使用 ResultTransformer 和 原生 SQL 查询映射 DTO

如果你想用原生 SQL 查询,你不需要经历声明SqlResultSetMapping 的所有麻烦,因为你可以使用 AliasToBeanResultTransformer,就像前面提到的 JPQL 示例的情况一样。

List postDTOs = entityManager
.createNativeQuery(
    "select " +
    "       p.id as id, " +
    "       p.title as title " +
    "from Post p " +
    "where p.created_on > :fromTimestamp")
.setParameter( "fromTimestamp", Timestamp.from(
    LocalDateTime.of( 2016, 1, 1, 0, 0, 0 ).toInstant( ZoneOffset.UTC ) ))
.unwrap( org.hibernate.query.NativeQuery.class )
.setResultTransformer( Transformers.aliasToBean( PostDTO.class ) )
.getResultList();