Mapstruct - Send nested entity having (one-to-many relation) in the response

25.5k views Asked by At

I have 2 entities CallRecords and CallRecordOperators with one-to-many relation as given below

 public class CallRecords {

    @Id
    @Column(name = "id", unique = true)
    private String id;

    @Column(columnDefinition = "varchar(255) default ''")
    private String callerNumber = "";

    @OneToMany(mappedBy="callrecord")
    private List<CallRecordOperators> callRecordOperators = new ArrayList<CallRecordOperators>();


   //getter setters
}

public class CallRecordOperators {

    @Id
    @Column(name = "id", length = 50, unique = true, nullable = false, insertable = false, updatable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "callRecordId")
    private CallRecords callrecord;

    @ManyToOne
    @JoinColumn(name = "operatorId")
    private Operator operator;

    @Formats.DateTime(pattern = "yyyy-MM-dd HH:mm:yy")
    @Column(columnDefinition = "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP")
    private Date startTime = new Date();

    @Column(columnDefinition = "varchar(100) default ''")
    private String dialStatus;

   //getter setter
}

So if the user ask for all "CallRecords" data I also have to give "CallRecordOperators" as they are related.

Current code for Mapper and DTOs

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CallRecordsMapper {

    CallRecordsMapper INSTANCE = Mappers.getMapper(CallRecordsMapper.class);

    @Mapping(source="callRecordOperators",target = "operators")
    CallRecordsDto callRecordsToCallRecordsDto(CallRecords callRecords);

    public abstract CallRecordOperatorsDto toTarget(CallRecordOperators source);

    List<CallRecordsDto> callRecordsToCallRecordsDtos(List<CallRecords> callRecords);

}

public class CallRecordsDto {

    private String callerNumber;

    private List<CallRecordOperatorsDto> operators;

    //getter setters
}

public class CallRecordOperatorsDto {

    private String callRecordsId;

    private String operatorId;
    private String operatorName;

    private String currentTime;

   // getter setter

}

But for above code I am getting

{
    "callerNumber": "9898989898",
    "operators": [{
        "callRecordsId": null,
        "operatorId": null,
        "operatorName": null,
        "currentTime": null
    }, {
        "callRecordsId": null,
        "operatorId": null,
        "operatorName": null,
        "currentTime": null
    }]
}

the values of operator array are null. what could be he issue?

3

There are 3 answers

0
Gunnar On BEST ANSWER

It seems your are lacking the mappings from CallRecordOperators to CallRecordOperatorsDto:

@Mapper
public interface CallRecordsMapper {

    CallRecordsMapper INSTANCE = Mappers.getMapper(CallRecordsMapper.class);

    @Mapping(source="callRecordOperators",target = "operators")
    CallRecordsDto callRecordsToCallRecordsDto(CallRecords callRecords);

    @Mapping(target = "callRecordsId", source = "callrecord.id")
    @Mapping(target = "operatorId", source = "operator.id")
    @Mapping(target = "operatorName", source = "operator.name")
    @Mapping(target = "currentTime", source = "startTime")
    CallRecordOperatorsDto callRecordOperatorsToDto(CallRecordOperators source);
}
0
JMSilla On

When you do a Hibernate query of A elements, you can fetch the related B elements of the bs collection using different strategies. Some of them are:

  1. If you use HQL to construct your queries, you can do a JOIN FETCH or LEFT JOIN FETCH to populate the bs collection:

    String hql = "SELECT DISTINCT a FROM " + A.class.getName() 
        + " a LEFT JOIN FETCH a.bs WHERE ...";
    

    This query will load all data using a single SQL query.

  2. Use eager fetching of the bs collection, changing the @OneToMany annotation:

    @OneToMany(fetch=FetchType.EAGER)
    private List<B> bs;
    

    In this case, when you run a query of A elements, a SQL query will be launched to retrieve the A data, and for each A object in the result, a SQL query will be executed to load the corresponding bs collection.

  3. If you use Criteria to build the query, you can change the fetch mode of the bs collection in a way similar to the HQL JOIN FETCH:

    Criteria c = session.createCriteria(A.class);
    c.setFetchMode("bs", FetchMode.JOIN);
    c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
    
0
Christian Beikov On

How about switching to a slightly different approach that also performs better? By using Blaze-Persistence Entity Views you can define your mapping directly on the DTO classes and apply that onto a query builder to generate efficient queries that perfectly fit your DTO structure.

@EntityView(CallRecords.class)
public interface CallRecordsDto {
    // The id of the CallRecords entity
    @JsonIgnore
    @IdMapping("id") String getId();

    String getCallerNumber();

    @Mapping("callRecordOperators")
    List<CallRecordOperatorsDto> getOperators();
}

@EntityView(CallRecordOperators.class)
public interface CallRecordOperatorsDto {

    // The id of the CallRecordOperators entity
    @JsonIgnore
    @IdMapping("id") Long getId();

    @Mapping("callrecord.id")
    String getCallRecordId();

    @Mapping("operator.id")
    String getOperatorId();

    @Mapping("operator.name")
    String getOperatorName();

    @Mapping("startTime")
    String getCurrentTime();

    // Whatever properties you want
}

See how you can map the entity attributes right in your DTOs? And here comes the code for querying

EntityManager entityManager = // jpa entity manager
CriteriaBuilderFactory cbf = // query builder factory from Blaze-Persistence
EntityViewManager evm = // manager that can apply entity views to query builders

CriteriaBuilder<User> builder = cbf.create(entityManager, CallRecords.class)
    .where("callerNumber").eq("123456789");
List<CallRecordsDto> result = evm.applySetting(
    builder, 
    EntityViewSetting.create(CallRecordsDto.class)
).getResultList();

Note that this will roughly generate the following optimized query

SELECT 
    c.id, 
    c.callerNumber, 
    o.callrecord.id, 
    o.id,
    o.startTime,
    op.id,
    op.name
FROM CallRecords c
LEFT JOIN c.callRecordOperators o
LEFT JOIN o.operator op
WHERE c.callerNumber = :param_1