【问题标题】:How to convert a JPA OneToMany relationship to DTO如何将 JPA OneToMany 关系转换为 DTO
【发布时间】:2014-02-05 15:19:21
【问题描述】:

我有一个班级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。这将是一个无限循环。我该如何解决这个问题?

请帮忙。

【问题讨论】:

标签: java jpa dto


【解决方案1】:

表关系

假设我们有以下postpost_comment 表,它们通过post_comment 表中的post_id 外键列形成一对多关系。

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

考虑到我们的用例只需要从post 表中获取idtitle 列,以及从post_comment 表中获取idreview 列,我们可以使用以下 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&lt;Object[]&gt;JPA 或 Hibernate 查询投影。我们希望将上述查询结果集转换为PostDTO 对象中的List,每个这样的对象都有一个comments 集合,其中包含所有关联的PostCommentDTO 对象:

我们可以使用 Hibernate 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[] 投影和包含 PostCommentDTO 子 DTO 对象的 PostDTO 对象之间的映射:

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[] 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 实体的位置,这些实体最终将由查询执行返回。我们使用postDTOMap 的原因是父行在每个子记录的 SQL 查询结果集中都有重复。

computeIfAbsent 方法允许我们创建一个 PostDTO 对象,前提是 postDTOMap 中已经没有现有的 PostDTO 引用。

PostDTO 类有一个构造函数,可以使用专用列别名设置idtitle 属性:

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
}

就是这样!

使用PostDTOResultTransformer,可以将 SQL 结果集转换为分层 DTO 投影,使用起来非常方便,尤其是在需要编组为 JSON 响应时:

postDTOs = {ArrayList}, size = 2
  0 = {PostDTO} 
    id = 1L
    title = "High-Performance Java Persistence"
    comments = {ArrayList}, size = 2
      0 = {PostCommentDTO} 
        id = 1L
        review = "Best book on JPA and Hibernate!"
      1 = {PostCommentDTO} 
        id = 2L
        review = "A must read for every Java developer!"
  1 = {PostDTO} 
    id = 2L
    title = "Hypersistence Optimizer"
    comments = {ArrayList}, size = 1
      0 = {PostCommentDTO} 
       id = 3L
       review = "It's like pair programming with Vlad!"

【讨论】:

    【解决方案2】:

    这不会是一个无限循环,因为您必须使用您刚刚在循环之前创建的 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);
        }
    

    【讨论】:

    • 谢谢。但是这里的reaultPlanDTO,而Activity 只能有Plan 与之关联。不是PlanDTO
    • 我已经更新了答案中的代码,实际上 Activity 也应该被转换为 DTO 并且 DTO 应该指向 PlanDTO。
    • 有一件事,我确实有另一个专门用于Activity 的汇编程序。它被称为ActivityAssembler。您认为将刚刚创建的PlanDTO 作为参数发送到ActivityAssembler 并将其设置在那里更好吗?
    • 是的,您必须将其作为参数发送到 ActivityAssembler,但是一旦您的域模型增加,它就会变得混乱,这就是我建议使用库/框架来执行此操作的原因。
    • 再次感谢。这里没有使用库的选项。至少现在是这样,但我很感激你的回答。
    【解决方案3】:

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

    1) 在映射器类中,您可以定义实现映射器,通过使它们单向来解决这个问题。使用类似的方法 MapPlanWithActivities()MapPlan()MapActivitiesWithPlan()MapActivities()。这样你就可以知道你需要什么数据,根据你使用的函数你知道什么时候停止递归。

    2) 另一个(更)复杂的解决方案是通过逻辑解决问题并检测循环。例如,您可以像杰克逊图书馆那样为这种情况定义一个注释。为此,您将不得不使用一些 java 反射。 See Java Reflection here

    3) 最简单的方法是使用推土机,如我在评论中所说:Dozer

    【讨论】:

      【解决方案4】:

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

      我创建了该库以允许在 JPA 模型和自定义接口定义模型之间轻松映射,例如 Spring Data Projections on steroids。这个想法是您以您喜欢的方式定义您的目标结构,并通过 JPQL 表达式将属性(getter)映射到实体模型。由于属性名称用作默认映射,因此您通常不需要显式映射,因为 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 定义定义的内容,而其他方法通常会获取太多数据和/或需要大量样板文件。

      【讨论】:

        猜你喜欢
        • 2014-08-20
        • 1970-01-01
        • 2017-01-23
        • 1970-01-01
        • 2021-02-26
        • 1970-01-01
        • 2022-09-30
        • 1970-01-01
        • 2012-04-22
        相关资源
        最近更新 更多