Spring boot mapstruct mapper using other mapper - java

Is it possible to use different mappers in mapstruct?
e.g. I have this mapper
#Component
#RequiredArgsConstructor
public class ObjMapperImpl implements ObjMapper{
private final UserMapper userMapper;
#Override
public ObjDto daoToDto(Obj obj) {
return ObjDto.builder()
.meetLink(obj.getMeetLink())
.date(obj.getFromDate().toLocalDate())
.user(userMapper.daoToDto(obj.getUser()))
.build();
}
}
Is it possible to change this implementation to mapstruct?

The below code should work for you, the generated implementation of daoToDto() of ObjMapper will invoke daoToDto() of UserMapper, you only need to specify the mapping from fromDate to date due to property name difference.
#Mapper(component Model = "spring")
public class ObjMapper{
#Mapping(source = "fromDate", target = "date")
ObjDto daoToDto(Obj obj);
}

MapStruct has an entire section about Invoking other mappers in the documentation.
When you want to use other mappers in your mapper you need to use Mapper#uses.
e.g.
#Mapper(componentModel = "spring", uses = UserMapper.class)
public class ObjMapper{
#Mapping(source = "fromDate", target = "date")
ObjDto daoToDto(Obj obj);
}

Related

How to save java object in postgres jsonb column

Here is my code:
I need to save java object value as jsonb in database (r2dbc).
#Getter
#Setter
#ToString
#NoArgsConstructor
#AllArgsConstructor
#Table("scoring")
public class ScoringModel extends BaseModel {
#Column("client_id")
#SerializedName(value = "clientId", alternate = {"client_id"})
private String clientId;
//othres
#Column("languages")
#SerializedName(value = "languages", alternate = {"languages"})
private String languages;
#SerializedName(value = "response", alternate = {"response"})
//Need to save as JsonB
private Object response;
}
Please help me resolve the issue
You need to implement ReadingConverter and WritingConverter and then register them in R2dbcCustomConversions in configuration.
#Bean
public R2dbcCustomConversions myConverters(ConnectionFactory connectionFactory){
var dialect = DialectResolver.getDialect(connectionFactory);
var converters = List.of(…);
return R2dbcCustomConversions.of(dialect, converters);
}
Converters itself should produce JSONs.
If you using Postgres, there are two approaches you can use to map to Postgres JSON/JSONB fields.
use Postgres R2dbc Json type directly in Java entity class.
use any Java type and convert it between Json via registering custom converters.
The first one is simple and stupid.
Declare a json db type field, eg.
metadata JSON default '{}'
Declare the Json type field in your entity class.
class Post{
#Column("metadata")
private Json metadata;
}
For the second the solution, similarly
Declare json/jsonb db type in the schema.sql.
Declare the field type as your custom type.eg.
class Post{
#Column("statistics")
private Statistics statistics;
record Statistics(
Integer viewed,
Integer bookmarked
) {}
}
Declare a R2dbcCustomConversions bean.
#Configuration
class DataR2dbcConfig {
#Bean
R2dbcCustomConversions r2dbcCustomConversions(ConnectionFactory factory, ObjectMapper objectMapper) {
R2dbcDialect dialect = DialectResolver.getDialect(factory);
return R2dbcCustomConversions
.of(
dialect,
List.of(
new JsonToStatisticsConverter(objectMapper),
new StatisticsToJsonConverter(objectMapper)
)
);
}
}
#ReadingConverter
#RequiredArgsConstructor
class JsonToStatisticsConverter implements Converter<Json, Post.Statistics> {
private final ObjectMapper objectMapper;
#SneakyThrows
#Override
public Post.Statistics convert(Json source) {
return objectMapper.readValue(source.asString(), Post.Statistics.class);
}
}
#WritingConverter
#RequiredArgsConstructor
class StatisticsToJsonConverter implements Converter<Post.Statistics, Json> {
private final ObjectMapper objectMapper;
#SneakyThrows
#Override
public Json convert(Post.Statistics source) {
return Json.of(objectMapper.writeValueAsString(source));
}
}
The example codes is here.
Finally verify it with a #DataR2dbcTest test.
#Test
public void testInsertAndQuery() {
var data = Post.builder()
.title("test title")
.content("content of test")
.metadata(Json.of("{\"tags\":[\"spring\",\"r2dbc\"]}"))
.statistics(new Post.Statistics(1000, 200))
.build();
this.template.insert(data)
.thenMany(
this.posts.findByTitleContains("test%")
)
.log()
.as(StepVerifier::create)
.consumeNextWith(p -> {
log.info("saved post: {}", p);
assertThat(p.getTitle()).isEqualTo("test title");
}
)
.verifyComplete();
}

How can I use spring injection, A repository class in a mapstruct mapper?

#Data
public class FilesDTO {
private int issue;
private String uniqueStr;
private StorageDomain xml;
private StorageDomain pdf;
private StorageDomain stop;
}
#Data
public class BackHalfDomain {
private int articleId;
private String uniqueStrr;
private long xmlContentId;
private long pdfContentId;
private long stopId;
private int issueNumber;
}
Using a repository class I have to fetch a StorageDomain object from the ID in BackHalfDomain. So I have to map StorageDomain object with respective fields.
like StorgeDomain sd = repo.findById(id).get(); and set this sd object in FilesDTO's xml field and so on.
This is my mapper
#Mapper(componentModel = "spring")
public interface FilesDTOMapper {
public static final FilesDTOMapper fileDTOMapper = Mappers.getMapper(FilesDTOMapper.class);
#Mapping(target = "issue", source = "domain.issueNumber")
#Mapping(target = "DOI", source = "domain.doi")
public FilesDTO map(BackHalfDomain domain);
}
I have used uses but wasn't successful.
I have used #Mapping(target="xyz", expression="java(repo.findById(id))")"
but all I got was NullPointerException
Spring injection isin't working.
Can someone have a solution for this? I am just started with mapstruct
Since mapstruct 1.2 you can use a combination of #AfterMapping and #Context.
#Mapper(componentModel="spring")
public interface FilesDTOMapper {
#Mapping(target = "xyz", ignore = true)
#Mapping(target = "issue", source = "domain.issueNumber")
#Mapping(target = "DOI", source = "domain.doi")
FilesDTO map( BackHalfDomain domain, #Context MyRepo repo);
#AfterMapping
default void map( #MappingTarget FilesDTO target, BackHalfDomain domain, #Context MyRepo repo) {
target.setXYZ(repo.findById(domain.getId()));
}
}
In 1.1 you would need to transform the mapper to a abstract class
#Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE,
componentModel = "spring",
uses = {})
public abstract class FilesDTOMapper {
#Autowired
protected MyRepo repo;
#Mapping(target = "issue", source = "domain.issueNumber")
#Mapping(target = "DOI", source = "domain.doi")
#Mapping(target="xyz", expression="java(repo.findById(domain.getId())))")
public FilesDTO map(BackHalfDomain domain);
}
I ran into this same problem. The solution was to use a Decorator as suggested in this answer. Following your code, the solution would be something like the following.
First, we have to specifiy the Decorator in the Mapper:
#Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
#Primary
#DecoratedWith(FilesDTOMapperDecorator.class)
public interface FilesDTOMapper {
// basic mapping here
public FilesDTO map(BackHalfDomain domain);
}
Then, implement the Decorator:
public abstract class FilesDTOMapperDecorator implements FilesDTOMapper {
#Autowired
#Qualifier("delegate")
private FilesDTOMapper delegate;
#Autowired
private SomeRepository someRepository;
#Override
public FilesDTO map(BackHalfDomain domain) {
// Delegate basic mapping
FilesDTO filesDTO = delegate.map(domain);
// Use the repository as needed to set additional mapping
filesDTO.setSomeValue(repo.findById(id).getSomeValue());
return filesDTO;
}
}

Mapping objects with bi-directional relations with Mapstruct

I wonder if and how Mapstruct could help with mapping ojects with bi-directional relations (in my case one to many):
public class A{
private Set<B> listB;
}
public class B{
private A a;
}
Mapping from/to an entity yields a StackOverflowError. (i would expect this to happen).
On the other hand the closed Mapstruct issues 469 and 1163 seem to imply that mapstruct will not directly support it.
I tried this example:
https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles
But this is just not working. With or without applying the "CycleAvoidingMappingContext" the stackoverflowerror keeps the same.
So how to map objects with cycles and leveraging mapstruct? (i am want to avoid complete manual mapping)
In order for the mapping to work you can try with the following mapper:
#Mapper
public interface MyMapper {
A map(A a, #Context CycleAvoidingMappingContext context);
Set<B> map(Set<B> b, #Context CycleAvoidingMappingContext context);
B map(B b, #Context CycleAvoidingMappingContext context);
}
If the methods for mapping from Set<B> into Set<B> is not there then the set in A won't be mapped.
In meantime i found a solution: First i ignore the field in question and map it in "AfterMapping". In my case i want to map also the child independently. In my case the #Context only contains a Boolean (boolean is not working) that tells where i entered the cycle (startedFromPru pru is the parent):
Mapper for parent (contains set of licenses):
#Mapper(componentModel = "spring", uses = {DateTimeMapper.class, LicenseMapper.class, CompanyCodeMapper.class, ProductHierarchyMapper.class})
public abstract class ProductResponsibleUnitMapper {
#Autowired
private LicenseMapper licenseMapper;
#Mappings({#Mapping(target = "licenses", ignore = true)})
public abstract ProductResponsibleUnit fromEntity(ProductResponsibleUnitEntity entity, #Context Boolean startedFromPru);
#Mappings({#Mapping(target = "licenses", ignore = true)})
public abstract ProductResponsibleUnitEntity toEntity(ProductResponsibleUnit domain, #Context Boolean startedFromPru);
#AfterMapping
protected void mapLicenseEntities(ProductResponsibleUnit pru, #MappingTarget ProductResponsibleUnitEntity pruEntity, #Context Boolean startedFromPru){
if(startedFromPru) {
pruEntity.getLicenses().clear(); //probably not necessary
pru.getLicenses().stream().map(license -> licenseMapper.toEntity(license, startedFromPru)).forEach(pruEntity::addLicense);
}
}
#AfterMapping
protected void mapLicenseEntities(ProductResponsibleUnitEntity pruEntity, #MappingTarget ProductResponsibleUnit pru, #Context Boolean startedFromPru){
if(startedFromPru) {
pru.getLicenses().clear(); //probably not necessary
pruEntity.getLicenses().stream().map(licenseEntity -> licenseMapper.fromEntity(licenseEntity, startedFromPru)).forEach(pru::addLicense);
}
}
}
Mapper for child:
#Mapper(componentModel = "spring", uses = { DateTimeMapper.class, CompanyCodeMapper.class, ProductHierarchyMapper.class,
CountryCodeMapper.class })
public abstract class LicenseMapper {
#Autowired
private ProductResponsibleUnitMapper pruMapper;
#Mappings({ #Mapping(source = "licenseeId", target = "licensee"), #Mapping(target = "licensor", ignore = true) })
public abstract License fromEntity(LicenseEntity entity, #Context Boolean startedFromPru);
#Mappings({ #Mapping(source = "licensee", target = "licenseeId"), #Mapping(target = "licensor", ignore = true) })
public abstract LicenseEntity toEntity(License domain, #Context Boolean startedFromPru);
#AfterMapping
protected void mapPru(LicenseEntity licenseEntity, #MappingTarget License license,
#Context Boolean startedFromPru) {
//only call ProductResponsibleUnitMapper if mapping did not start from PRU -> startedFromPru is false
if (!startedFromPru) {
license.setLicensor(pruMapper.fromEntity(licenseEntity.getLicensor(), startedFromPru));
}
}
#AfterMapping
protected void mapPru(License license, #MappingTarget LicenseEntity licenseEntity,
#Context Boolean startedFromPru) {
//only call ProductResponsibleUnitMapper if mapping did not start from PRU -> startedFromPru is false
if (!startedFromPru) {
licenseEntity.setLicensor(pruMapper.toEntity(license.getLicensor(), startedFromPru));
}
}

Passing additional parameters to MapStruct mapper

Haven't figured out how to pass in additional arguments or an alternative.
Currently I'm mapping an Order and OrderLines. Both objects are different and need #mappings. Example I have an Order and OrderRequest object both are different and need #Mappings annotation to map the values, same with OrderLines.
I've created the following mappers
OrderMapper (uses = OrderLineMappers.class)
OrderLineMappers (uses = OrderLineMapper.class)
OrderLineMapper
So my issue is the OrderLine needs the OrderId from the Order object. However in the OrderLineMapper it's passing in the OrderLine and not the Order. How can I send the OrderId to the OrderLineMapper? Currently I have the OrderMapper doing an #AfterMapper, looping through the orderLines and populating with the OrderId.
Any help would be great.
Class OrderMapper
#Mapper(componentModel = "spring", uses = {OrderLineMappers.class})
public abstract class OrderMapper {
#AfterMapping
protected void orderRequestFromOrder( Order order, #MappingTarget
OrderRequest orderRequest ) {
//Wanting to do this at the OrderLineMapper class and not here
String orderId = order.getId();
List<OrderLineRequest> lines = orderRequest.getOrderLines();
List<OrderLineRequest> updatedLines = new ArrayList<>();
for (OrderLineRequest line : lines) {
line.setOrderId(orderId);
updatedLines.add(line);
}
orderRequest.setOrderLines(updatedLines);
}
#Mappings({
#Mapping( source = "orderId", target = "id" ),
#Mapping( source = "orderNumber", target = "order_number" ),
#Mapping( source = "orderLines", target = "orderLines")
})
public abstract Order orderRequestToOrder(OrderRequest orderRequest);
Class OrderLineMappers
#Mapper(componentModel = "spring", uses = {OrderLineMapper.class})
public interface OrderLineMappers {
List<OrderLine> orderLines(List<OrderLineRequest> orderLineRequest);
#InheritInverseConfiguration
List<OrderLineRequest> orderLineRequests(List<OrderLine> orderLine);
}
Class OrderLineMapper
#Mapper(componentModel = "spring")
public abstract class OrderLineMapper {
#Mappings({
#Mapping( target = "orderId", source = "orderLineId" ),
#Mapping( target = "order_line_number", source = "orderLineNumber")
})
public abstract OrderLine orderLineRequestToOrderLine(OrderLineRequest orderLineRequest);
}
Again just trying to pass in the OrderId to the OrderLineMapper. Not sure how to do this.
You can't really do this. What you can do instead is to use the #Context and perform your logic in it. You can have #AfterMapping and #BeforeMapping in your context where you can store the Order id and use that during its execution
e.g.
public class OrderMappingContext {
protected String currentOrderId;
#BeforeMapping
public void startMappingOrder(#MappingTarget Order order, OrderRequest request) {
this.currentOrderId = request.getId();
}
#AfterMapping
public void finishOrderMapping(#MappingTarget Order order) {
this.currentOrderId = null;
}
#AfterMapping
public void afterOrderLineMapping(#MappingTarget OrderLine orderLine) {
orderLine.setOrderId(this.currentOrderId);
}
}
Your mappers will then look like:
#Mapper(componentModel = "spring", uses = {OrderLineMappers.class})
public abstract class OrderMapper {
#Mapping( source = "orderId", target = "id" ),
#Mapping( source = "orderNumber", target = "order_number" ),
#Mapping( source = "orderLines", target = "orderLines")
public abstract Order orderRequestToOrder(OrderRequest orderRequest, #Context OrderMappingContext context);
#Mapper(componentModel = "spring", uses = {OrderLineMapper.class})
public interface OrderLineMappers {
List<OrderLine> orderLines(List<OrderLineRequest> orderLineRequest, #Context OrderMappingContext context);
#InheritInverseConfiguration
List<OrderLineRequest> orderLineRequests(List<OrderLine> orderLine);
}
#Mapper(componentModel = "spring")
public abstract class OrderLineMapper {
#Mapping( target = "orderId", source = "orderLineId" ),
#Mapping( target = "order_line_number", source = "orderLineNumber")
public abstract OrderLine orderLineRequestToOrderLine(OrderLineRequest orderLineRequest, #Context OrderMappingContext context);
}
On the invocation side you will do something like
orderMapper.orderRequestToOrder(request, new OrderMappingContext());

How to use Mapstruct when target is inline object?

I have the object like this:
class User {
private String firstName;
private String secondName;
private int age;
....
get/set methods
}
And another object has User as a property:
class UserHolder {
private User user;
....
get/set methods
}
I want to convert UserHolder to User use MapStruct.
When I write this mapper I've something like this:
#Mapper(componentModel = "spring")
public interface UserHolderMapper {
#Mapping(source = "user.firstName", target = "firstName")
#Mapping(source = "user.secondName", target = "secondName")
#Mapping(source = "user.age", target = "age")
User toUser(UserHolder source);
}
But it contains a lot of boilerplate code (in #Mapping annotation), how I can say to mapper that I want to map source.user to this target without specifying fields of target?
I may be late to the party. However following should work fine.
#Mapper(componentModel = "spring")
public interface UserHolderMapper {
#Mapping(source = "source.user", target = ".")
User toUser(UserHolder source);
}
This is currently not possible. There is already a feature request #1406 which is quite similar to what you need.
In any case as a workaround your mapper can look like:
#Mapper(componentModel = "spring")
public interface UserHolderMapper {
default User toUser(UserHolder source) {
return source == null ? null : toUser(source.getUser());
}
User toUser(UserDto source);
}
I don't know what the object in UserHolder is. UserDto is just a presumption, it can be any object.
In case you don't want to expose User toUser(UserDto source) then you can create an abstract mapper and make that method protected and abstract there. MapStruct will be able to handle it

Categories