将JPA OneToMany关系转换为DTO

时间:2014-02-05 15:19:21

标签: java jpa dto

我有一个班级Plan,其中有一个Activity列表。 Activity类引用了单个Plan。因此,有一个像这样的 OneToMany 关系:

@Entity
public class Plan {

    @OneToMany(mappedBy = "Plan")
    private List<Activity> activities;
}

@Entity
public class Activity {

    @ManyToOne
    @JoinColumn(name= "PLAN_ID")
    private Plan plan;
}

我需要将它们转换为DTO才能发送到表示层。所以我有一个汇编程序类来简单地将域对象转换为POJO。

public class PlanAssembler {

    public static PlanDTO makeDTO(Plan p) {

    PlanDTO result = new PlanDTO();
    result.setProperty(p.getProperty);
    ... 

    for (Activity a: p.getActivity()) {

     // Here I need to iterate over each activity to convert it to DTO
     // But in ActivityAssembler, I also need PlanDTO

    }

正如您所看到的,在PlanAssembler中,我需要迭代所有活动并将其转换为ActivityDTO,但麻烦的是,ActivityAssembler我还需要PlanDTO构造ActivityDTO。这将是一个无限循环。我该如何解决这个问题?

请帮忙。

4 个答案:

答案 0 :(得分:2)

现在,如果你真的想自己解决问题:

1)在mapper类中,您可以通过使它们单向来定义实现映射器来解决此问题。用像这样的方法 MapPlanWithActivities()MapPlan()MapActivitiesWithPlan()MapActivities()。通过这种方式,您可以知道您需要哪些数据,并根据您使用的函数知道何时停止递归。

2)另一个(更多)更复杂的解决方案是通过逻辑解决问题并检测循环。例如,您可以像Jackson Library那样为该案例定义注释。为此,你将不得不使用一些java反射。 See Java Reflection here

3)最简单的方法是使用Dozer,如我的评论所述:Dozer

答案 1 :(得分:1)

它不会是一个无限循环,因为你必须使用刚刚在循环之前创建的PlanDTO对象结果。请参阅下面的代码。

注意:我仍然建议您选择一个能够为您完成这些工作的框架。

public class PlanAssembler {

    public static PlanDTO makeDTO(Plan p) {

    PlanDTO result = new PlanDTO();
    result.setProperty(p.getProperty);
    ... 

    for (Activity a: p.getActivity()) {

      ActivityDTO activityDTO = new ActivityDTO();
     // Here I need to iterate over each activity to convert it to DTO
     // But in ActivityAssembler, I also need PlanDTO

     //Code to convert Activity to ActivityDTO.

      activityDTO.setPlan(result);
    }

答案 2 :(得分:1)

这是一个非常常见的问题,因此此答案基于我在博客上写的this post

表关系

假设我们具有以下postpost_comment表,它们通过post_id表中的post_comment外键列构成one-to-many relationship。 / p>

The post and post_comment tables used for the JPA DTO projection

使用JPA和Hibernate获取一对多的DTO投影

考虑到我们有一个用例,只需要从id表以及titlepost中提取idreviewpost_comment表中的列中,我们可以使用以下JPQL查询来获取所需的投影:

select p.id as p_id, 
       p.title as p_title,
       pc.id as pc_id, 
       pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id

运行上面的投影查询时,我们得到以下结果:

| p.id | p.title                           | pc.id | pc.review                             |
|------|-----------------------------------|-------|---------------------------------------|
| 1    | High-Performance Java Persistence | 1     | Best book on JPA and Hibernate!       |
| 1    | High-Performance Java Persistence | 2     | A must-read for every Java developer! |
| 2    | Hypersistence Optimizer           | 3     | It's like pair programming with Vlad! |

但是,我们不想使用基于表格的ResultSet或默认的List<Object[]> JPA或Hibernate查询投影。我们想将上述查询结果集转换为List个对象的PostDTO,每个这样的对象都有一个comments集合,其中包含所有关联的PostCommentDTO对象:

The PostDTO and PostCommentDTO used for DTO projection

正如我在this article中所述,我们可以使用休眠ResultTransformer,如以下示例所示:

List<PostDTO> postDTOs = entityManager.createQuery("""
    select p.id as p_id, 
           p.title as p_title,
           pc.id as pc_id, 
           pc.review as pc_review
    from PostComment pc
    join pc.post p
    order by pc.id
    """)
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new PostDTOResultTransformer())
.getResultList();

assertEquals(2, postDTOs.size());
assertEquals(2, postDTOs.get(0).getComments().size());
assertEquals(1, postDTOs.get(1).getComments().size());

PostDTOResultTransformer将定义Object[]投影与包含PostDTO子DTO对象的PostCommentDTO对象之间的映射:

public class PostDTOResultTransformer 
        implements ResultTransformer {

    private Map<Long, PostDTO> postDTOMap = new LinkedHashMap<>();

    @Override
    public Object transformTuple(
            Object[] tuple, 
            String[] aliases) {

        Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases);

        Long postId = longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]);

        PostDTO postDTO = postDTOMap.computeIfAbsent(
            postId, 
            id -> new PostDTO(tuple, aliasToIndexMap)
        );

        postDTO.getComments().add(
            new PostCommentDTO(tuple, aliasToIndexMap)
        );

        return postDTO;
    }

    @Override
    public List transformList(List collection) {
        return new ArrayList<>(postDTOMap.values());
    }
}

aliasToIndexMap只是一个很小的实用程序,它使我们可以构建一个Map结构,该结构将列别名与列值位于Object[] {{1中的索引}}数组:

tuple

public Map<String, Integer> aliasToIndexMap( String[] aliases) { Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>(); for (int i = 0; i < aliases.length; i++) { aliasToIndexMap.put(aliases[i], i); } return aliasToIndexMap; } 是我们将存储所有postDTOMap实体的位置,最后,这些实体将由查询执行返回。我们使用PostDTO的原因是,每个子记录的SQL查询结果集中都有父行。

postDTOMap方法仅在computeIfAbsent中没有存储现有的PostDTO引用的情况下,才可以创建PostDTO对象。

postDTOMap类具有一个构造函数,可以使用专用的列别名来设置PostDTOid属性:

title

public class PostDTO { public static final String ID_ALIAS = "p_id"; public static final String TITLE_ALIAS = "p_title"; private Long id; private String title; private List<PostCommentDTO> comments = new ArrayList<>(); public PostDTO( Object[] tuples, Map<String, Integer> aliasToIndexMap) { this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); this.title = stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]); } //Getters and setters omitted for brevity } 的构建方式类似:

PostCommentDTO

就是这样!

使用public class PostCommentDTO { public static final String ID_ALIAS = "pc_id"; public static final String REVIEW_ALIAS = "pc_review"; private Long id; private String review; public PostCommentDTO( Object[] tuples, Map<String, Integer> aliasToIndexMap) { this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]); this.review = stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]); } //Getters and setters omitted for brevity } ,可以将SQL结果集转换为分层DTO投影,使用起来非常方便,尤其是当它需要作为JSON响应编组时:

PostDTOResultTransformer

答案 3 :(得分:0)

这是Blaze-Persistence Entity Views的完美用例。

我创建了该库,以允许在JPA模型和自定义接口定义的模型之间轻松映射,例如类固醇上的Spring Data Projections。这个想法是您以自己喜欢的方式定义目标结构,并通过JPQL表达式将属性(获取器)映射到实体模型。由于属性名称用作默认映射,因此大多数情况下您不需要显式映射,因为80%的用例都是将DTO作为实体模型的子集。

模型的映射看起来像下面的一样简单

@EntityView(Plan.class)
interface PlanDTO {
  @IdMapping
  Long getId();
  String getName();
  List<ActivityDTO> getActivities();
}
@EntityView(Activity.class)
interface ActivityDTO {
  @IdMapping
  Long getId();
  String getName();
}

查询是将实体视图应用于查询的问题,最简单的方法就是按ID查询。

PlanDTOdto = entityViewManager.find(entityManager, PlanDTO.class, id);

但是Spring Data集成允许您像使用Spring Data Projections一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/#spring-data-features

这样做的最大好处是,这种方法只会获取您通过实体视图中的getter定义定义的内容,而其他方法通常会获取过多的数据和/或需要大量的样板。