سؤال

I have a class Plan in which there is a list of Activity. The Activity class has a reference to a single Plan. Hence there is a OneToMany relationship like this:

@Entity
public class Plan {

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

@Entity
public class Activity {

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

I need to convert them to DTOs to be sent to presentation layer. So I have an assembler class to simply convert domain objects to 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

    }

As you can see, in PlanAssembler, I need to iterate over all activities and convert them to ActivityDTO but the trouble is, in ActivityAssembler I also need the PlanDTO to construct the ActivityDTO. It's gonna be an infinite loop. How can I sort this out?

Please help.

هل كانت مفيدة؟

المحلول 2

It won't be an infinite loop because you have to use the PlanDTO object result which you have just created before the loop. See the code below.

Note : Still I suggest to go for a framework which will do this stuff for you.

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);
    }

نصائح أخرى

Table relationships

Let's assume we have the following post and post_comment tables, which form a one-to-many relationship via the post_id Foreign Key column in the post_comment table.

The post and post_comment tables used for the JPA DTO projection

Fetching a one-to-many DTO projection with JPA and Hibernate

Considering we have a use case that only requires fetching the id and title columns from the post table, as well as the id and review columns from the post_comment tables, we could use the following JPQL query to fetch the required projection:

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

When running the projection query above, we get the following results:

| 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! |

However, we don't want to use a tabular-based ResultSet or the default List<Object[]>JPA or Hibernate query projection. We want to transform the aforementioned query result set to a List of PostDTO objects, each such object having a comments collection containing all the associated PostCommentDTO objects:

The PostDTO and PostCommentDTO used for DTO projection

We can use a Hibernate ResultTransformer, as illustrated by the following example:

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());

The PostDTOResultTransformer is going to define the mapping between the Object[] projection and the PostDTO object containing the PostCommentDTO child DTO objects:

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());
    }
}

The aliasToIndexMap is just a small utility that allows us to build a Map structure that associates the column aliases and the index where the column value is located in the Object[] tuple array:

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;
}

The postDTOMap is where we are going to store all PostDTO entities that, in the end, will be returned by the query execution. The reason we are using the postDTOMap is that the parent rows are duplicated in the SQL query result set for each child record.

The computeIfAbsent method allows us to create a PostDTO object only if there is no existing PostDTO reference already stored in the postDTOMap.

The PostDTO class has a constructor that can set the id and title properties using the dedicated column aliases:

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
}

The PostCommentDTO is built in a similar fashion:

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
}

That's it!

Using the PostDTOResultTransformer, the SQL result set can be transformed into a hierarchical DTO projection, which is much convenient to work with, especially if it needs to be marshalled as a JSON response:

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!"

Now if you really want to sort things out on your own:

1) In the mapper class you could define implement mappers resolving this issue by making them unidirectional. With methods like MapPlanWithActivities(), MapPlan(), MapActivitiesWithPlan() and MapActivities(). this way you could know what data you need and according to what function you use you know when to stop the recursion.

2) The other (much) more complex solution would be to solve the issue by logic and detect the loop. You can for instance define an annotation for that case as Jackson Library does. for that you will have to use some java reflection. See Java Reflection here

3) the easiest way would be to use Dozer as said in my comment:Dozer

This is a perfect use case for Blaze-Persistence Entity Views.

I created the library to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.

A mapping for your model could look as simple as the following

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

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

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

But the Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/#spring-data-features

The biggest benefit of this is, that this approach will only fetch what you define through the getter definitions in your entity views, whereas other approaches usually fetch too much data and/or require a lot of boilerplate.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top