How to map Idempiere models to DTO with MapStruct
TL;DR
MapStruct is a code generator simplifying mapping between Idempiere Objects and DTOs.
Before using MapStruct you need to install it manually. You can find instructions here.
@Mapper annotation
@Mapper
annotation is used to define a mapper interface. It is used to define mappings between different types of objects.
@Mapper
annotation has a parameter: componentModel = "spring"
. It is used to add @Component
annotation to generated mapper.
@Component
annotation is used to register the mapper interface as a Spring bean.
@Mapper
annotation has a parameter: uses = {PaymentService.class}
. It is used to define a service we need to use in the mapper.
@Mapper
annotation has a parameter: imports = {MBPartner.class}
. It is used to define a class that will be used in the expression.
@Mapper(componentModel = "spring", uses = {PaymentService.class},
imports = {MBPartner.class})
public interface InvoiceMapper {
...
}
Basic mapping
Let’s take a look at an exemplary mapping between MInvoice
and InvoiceDTO
objects.
MInvoice object is a model from Idempiere. It is a representation of C_Invoice
table in the database.
InvoiceDTO is a data transfer object created by OpenApi. It is used to transfer data between backend and frontend.
@Schema(name = "Invoice", description = "Object Invoice")
@JsonTypeName("Invoice")
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen")
public class InvoiceDto {
@JsonProperty("id")
private Integer id;
@JsonProperty("documentNo")
private String documentNo;
@JsonProperty("poReference")
private String poReference;
@JsonProperty("bpartnerName")
private String bpartnerName;
...
}
Mappings between these objects are defined in co.icreated.portal.mapper.InvoiceMapper
class.
@Mapping(target = "id", source = "c_Invoice_ID")
@Mapping(target = "bpartnerName",
expression = "java(MBPartner.get(invoice.getCtx(),
invoice.getC_BPartner_ID()).getName())")
@Mapping(target = "date", source = "dateInvoiced")
@Mapping(target = "poReference", source = "POReference")
@Mapping(target = "currency", source = "currencyISO")
public abstract InvoiceDto toDto(MInvoice invoice);
MapStruct maps fields with the same name by default. So, you don’t need to define mapping for fields with the same name.
If you want to map fields with different names you need to use @Mapping annotation:
@Mapping annotation is used to define mapping between two fields.
@Mapping annotation has two parameters: target
and source
:
target
is a field in the target object. In our case it isInvoiceDTO
.source
is a field in the source object. In our case it isMInvoice
.
For example:
@Mapping(target = "id", source = "c_Invoice_ID")
means thatid
field inInvoiceDTO
will be mapped fromc_Invoice_ID
field inMInvoice
.
expression
is used to define a custom mapping. In our case we use it to get bpartnerName
from C_BPartner_ID
field.
Java code is used to define custom mapping.
Don’t forget to add MBPartner
class to the list of imports in @Mapper annotation.
Special cases
For exemple we need to do additional actions when mapping MInvoice
to InvoiceDTO
.
It can be done with @AfterMapping annotation. This annotation means that the method will be called after the mapping is done.
In the parameters of the method we can use the source and target objects.
Here InvoiceDto
annotated with @MappingTarget
is already populated with data from MInvoice
. We can use it to get additional data.
@AfterMapping
protected void setBPartnerName(MInvoice invoice, @MappingTarget InvoiceDto invoiceDto) {
invoiceDto.setBpartnerName(MBPartner.get(invoice.getCtx(), invoice.getC_BPartner_ID())
.getName());
invoiceDto.setPayments(paymentService.findInvoicePayments(invoice.getC_Invoice_ID()));
}
Thanks to all these features we can easily map Idempiere models to DTOs. \
Advice: Escaping custom mapping is possible if you give the same name to the field in the source and target objects. Think about it when working with OpenApi definitions.