Repository navigation
Does mapstruct redo the analysis of methods in parent-child mapping setups? #3737
Replies: 4 comments
|
Hey @RichMacDonald, we don't have any special caching in the moment. However, we've seen some performance issues in really large projects. What I would propose (if you are able to do), would be to run the compilation with an attached profiler and share the profiling session with me over email. I can then look into what we can do on MapStruct side to improve the performance a bit. A 3rd alternative would be to use Something else that you might want to look into would be Mapping Composition. With this approach you might not need to use |
|
Hi @filiphr, thanks for your reply. Mapping Composition does not work for me because of the 3 limitations I mentioned in my referenced comment and re-listed below:
I gave inherited MapperConfig a try and made some headway. Using the following model as an example: public static class ParentA {
public String parentA;
}
public static class ParentB {
public String parentB;
}
public static class ChildA extends ParentA {
public String childA;
}
public static class ChildB extends ParentB {
public String childB;
}
/**
* Everything in one mapper.
* Does not scale when there are very many classes to be mapped
*/
@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface CompleteMapping {
@Mapping(source = "parentA", target = "parentB")
void aToBParent(ParentA a, @MappingTarget ParentB b);
@InheritInverseConfiguration(name="aToBParent")
void bToAParent(ParentB b, @MappingTarget ParentA a);
@InheritConfiguration(name="aToBParent")
@Mapping(source = "childA", target = "childB")
void aToBChild(ChildA a, @MappingTarget ChildB b);
//@InheritConfiguration(name="aToBParent") //not needed
@InheritInverseConfiguration(name="aToBChild")
void bToAChild(ChildB b, @MappingTarget ChildA a);
}
public class CompleteMappingImpl implements CompleteMapping {
@Override
public void aToBParent(InheritanceTest2.ParentA a, InheritanceTest2.ParentB b) {
if ( a == null ) {
return;
}
b.parentB = a.parentA;
}
@Override
public void bToAParent(InheritanceTest2.ParentB b, InheritanceTest2.ParentA a) {
if ( b == null ) {
return;
}
a.parentA = b.parentB;
}
@Override
public void aToBChild(InheritanceTest2.ChildA a, InheritanceTest2.ChildB b) {
if ( a == null ) {
return;
}
b.childB = a.childA;
b.parentB = a.parentA;
}
@Override
public void bToAChild(InheritanceTest2.ChildB b, InheritanceTest2.ChildA a) {
if ( b == null ) {
return;
}
a.childA = b.childB;
a.parentA = b.parentB;
}
}Now extracting a "parent" MapperConfig. The following works for the parent: /**
* Move the mapping definitions to a config class using prototype methods
*/
@MapperConfig(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface ParentMappingConfig {
@Mapping(source = "parentA", target = "parentB")
ParentB aToBParent(ParentA a);
@InheritInverseConfiguration(name="aToBParent")
ParentA bToAParent(ParentB b);
}
@Mapper(config = ParentMappingConfig.class)
public interface ParentMapping {
void aToBParent(ParentA a, @MappingTarget ParentB b);
void bToAParent(ParentB b, @MappingTarget ParentA a);
}I got an error when I naively added the child: @MapperConfig(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface ChildMappingConfig extends ParentMappingConfig{
@InheritConfiguration(name="aToBParent")
@Mapping(source = "childA", target = "childB")
ChildB aToBChild(ChildA a);
//@InheritConfiguration(name="aToBParent") //not needed
@InheritInverseConfiguration(name="aToBChild")
ChildA bToAChild(ChildB b);
}
@Mapper(config = ChildMappingConfig.class)
public interface ChildMappingWithError{
@InheritConfiguration(name="aToBChild") //needs to be explicit because there are multiple matches
void aToBChild(ChildA a, @MappingTarget ChildB b);
//Mapstruct error: More than one configuration prototype method is applicable. Use @InheritInverseConfiguration to select one of them explicitly: ParentA bToAParent(ParentB b), ChildA bToAChild(ChildB b).
@InheritConfiguration(name="bToAChild") //needs to be explicit because there are multiple matches
void bToAChild(ChildB b, @MappingTarget ChildA a);
//Mapstruct error: More than one configuration prototype method is applicable. Use @InheritInverseConfiguration to select one of them explicitly: ParentB aToBParent(ParentA a), ChildB aToBChild(ChildA a).
}The fact that mapstruct complained even though I had explicitly named the inheriting method was a shock. But it was also a useful learning experience because it explained some of the errors I have run into in the past and never understood: Beware the power of AUTO_INHERIT_ALL_FROM_CONFIG! Switching to explicit mappingInheritanceStrategy did the trick. The following works: @Mapper(config = ChildMappingConfig.class, mappingInheritanceStrategy = MappingInheritanceStrategy.EXPLICIT)
public interface ChildMapping {
@InheritConfiguration(name="aToBChild") //needs to be explicit because there are multiple matches
void aToBChild(ChildA a, @MappingTarget ChildB b);
@InheritConfiguration(name="bToAChild") //needs to be explicit because there are multiple matches
void bToAChild(ChildB b, @MappingTarget ChildA a);
}Comment 1: I expected a named inherited configuration to override AUTO_INHERIT_ALL_FROM_CONFIG. Clearly I was wrong. Or rather, it is I've read the manual 100 times and never realized the significance of mappingInheritanceStrategy in this manner. I made assumptions and that is why I misinterpreted the error message. Comment 2: Does The answer is that it does nothing for the MapperConfig methods. The following does not work: // Use auto-inherit in the config methods. Does not work
@MapperConfig(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface ParentMappingConfig2 {
@Mapping(source = "parentA", target = "parentB")
ParentB aToBParent(ParentA a);
//@InheritInverseConfiguration(name="aToBParent")
ParentA bToAParent(ParentB b);
}
@MapperConfig(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface ChildMappingConfig2 extends ParentMappingConfig2{
//@InheritConfiguration(name="aToBParent")
@Mapping(source = "childA", target = "childB")
ChildB aToBChild(ChildA a);
//@InheritConfiguration(name="aToBParent") //not needed
//@InheritInverseConfiguration(name="aToBChild")
ChildA bToAChild(ChildB b);
}
@Mapper(config = ChildMappingConfig2.class, mappingInheritanceStrategy = MappingInheritanceStrategy.EXPLICIT)
public interface ChildMapping2 {
@InheritConfiguration(name="aToBChild")
void aToBChild(ChildA a, @MappingTarget ChildB b); // nope. mapstruct error
@InheritConfiguration(name="bToAChild")
void bToAChild(ChildB b, @MappingTarget ChildA a); // nope. mapstruct error
}Comment 3: Too bad we can't use AUTO_INHERIT_ALL_FROM_CONFIG to apply to the same methods within the Mapper classes. The following would have been a nice option: //AUTO_INHERIT_ALL_FROM_CONFIG does not method AUTO_INHERIT_ALL_FROM_SELF
@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_ALL_FROM_CONFIG)
public interface CompleteMappingWithError {
@Mapping(source = "parentA", target = "parentB")
void aToBParent(ParentA a, @MappingTarget ParentB b);
//@InheritInverseConfiguration(name="aToBParent")
void bToAParent(ParentB b, @MappingTarget ParentA a); // nope. mapstruct error
//@InheritConfiguration(name="aToBParent")
@Mapping(source = "childA", target = "childB")
void aToBChild(ChildA a, @MappingTarget ChildB b); // nope. mapstruct error
//@InheritConfiguration(name="aToBParent") //not needed
//@InheritInverseConfiguration(name="aToBChild")
void bToAChild(ChildB b, @MappingTarget ChildA a); // nope. mapstruct error
}I am having issues with ignoring fields, but I will share that in a followup post as I begin to understand it. |
|
Here is a test case that feels like a mapstruct bug. I extended the above code to include fields to be ignored. The model is now: public static class ParentA {
public String parentA;
public String parentAIgnore;
}
public static class ParentB {
public String parentB;
public String parentBIgnore;
}
public static class ChildA extends ParentA {
public String childA;
}
public static class ChildB extends ParentB {
public String childB;
}Mapping just the child classes is fine: /**
* Mapping just the child class. Everything works
*/
@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface MapperWithoutParent {
@Mapping(target = "parentB", source = "parentA")
@Mapping(target = "parentBIgnore", ignore = true)
@Mapping(target = "childB", source = "childA")
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore" })
void aToBChild(ChildA a, @MappingTarget ChildB b);
@InheritInverseConfiguration(name="aToBChild")
@Mapping(target = "parentAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore" })
void bToAChild(ChildB b, @MappingTarget ChildA a);
}But something went haywire when I mapped parent and child separately. The following works, but note that I have not completed the inverse mapping for the child. /**
* Mapping parent and child classes in one mapper.
*/
@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface MapperWithParentAndChild {
@Mapping(target = "parentB", source = "parentA")
@Mapping(target = "parentBIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore" })
void aToBParent(ParentA a, @MappingTarget ParentB b);
@InheritInverseConfiguration(name="aToBParent")
@Mapping(target = "parentAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore" })
void bToAParent(ParentB b, @MappingTarget ParentA a);
@InheritConfiguration(name="aToBParent")
@Mapping(target = "childB", source = "childA")
void aToBChild(ChildA a, @MappingTarget ChildB b);
}But when I added the inverse mapping for the CHILD, mapstruct reported an error for the PARENT! /**
* Mapping parent and child classes in one mapper. Incorrect error from mapstruct
*/
@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface MapperWithParentAndChild {
@Mapping(target = "parentB", source = "parentA")
@Mapping(target = "parentBIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore" })
void aToBParent(ParentA a, @MappingTarget ParentB b);
@InheritInverseConfiguration(name="aToBParent")
@Mapping(target = "parentAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore" })
void bToAParent(ParentB b, @MappingTarget ParentA a);
// mapstruct error: Unknown property "parentBIgnore" in result type ChildA. Did you mean "parentAIgnore"?
@InheritConfiguration(name="aToBParent")
@Mapping(target = "childB", source = "childA")
void aToBChild(ChildA a, @MappingTarget ChildB b);
@InheritConfiguration(name="bToAParent")
@InheritInverseConfiguration(name="aToBChild")
void bToAChild(ChildB b, @MappingTarget ChildA a); //Adding this method caused a compile error in the PARENT method
}Removing one of the parent inheritance removes the invalid error: @Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface MapperWithParentAndChild {
@Mapping(target = "parentB", source = "parentA")
@Mapping(target = "parentBIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore" })
void aToBParent(ParentA a, @MappingTarget ParentB b);
@InheritInverseConfiguration(name="aToBParent")
@Mapping(target = "parentAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore" })
void bToAParent(ParentB b, @MappingTarget ParentA a);
@InheritConfiguration(name="aToBParent")
@Mapping(target = "childB", source = "childA")
void aToBChild(ChildA a, @MappingTarget ChildB b);
//@InheritConfiguration(name="bToAParent")
//@InheritInverseConfiguration(name="aToBChild")
//void bToAChild(ChildB b, @MappingTarget ChildA a);
//push the parent mapping down to the child. This works.
//@InheritConfiguration(name="bToAParent")
@InheritInverseConfiguration(name="aToBChild")
@Mapping(target = "parentAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore" })
void bToAChild_works(ChildB b, @MappingTarget ChildA a);
}At the very least, the mapstruct reported problem makes no sense. And surely there is no legitimate way for an error in a child mapping to affect a parent? |
|
When I split the code into multiple MapperConfig classes, it worked. So the previous comment does feel like a bug. I went ahead and worked up a parent-child-grandchild problem for testing. I also added Before/AfterMapping. That was interesting because I wrote those methods in an interface class and Mapstruct tried to instantiate it. This obviously doesn't work. So another minor complication with a 3rd hierarchy of classes to make this work. I show the complete code example below, as it currently stands. The only real complaint is that it is fairly complicated and everything has to be "just right". And when things go wrong, good luck figuring out where you made a typo. Also, the lack of inheriting ignoreUnmappedSourceProperties (separate issue) is a real management concern as we scale. public class InheritanceTestWithIgnore {
public static class ParentA {
public String parentA;
public String parentAIgnore;
}
public static class ParentB {
public String parentB;
public String parentBIgnore;
}
public static class ChildA extends ParentA {
public String childA;
public String childAIgnore;
}
public static class ChildB extends ParentB {
public String childB;
public String childBIgnore;
}
public static class GrandA extends ChildA {
public String grandA;
public String grandAIgnore;
}
public static class GrandB extends ChildB {
public String grandB;
public String grandBIgnore;
}
/**
* Move the mapping definitions to a config class using prototype methods
*/
@MapperConfig(uses = ParentMappingAfterConfig.class)
public interface ParentMappingConfig {
@Mapping(target = "parentB", source = "parentA")
@Mapping(target = "parentBIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore" })
ParentB aToBParent(ParentA a);
@InheritInverseConfiguration(name="aToBParent")
@Mapping(target = "parentAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore" })
ParentA bToAParent(ParentB b);
@AfterMapping
default void aToBParent_after(ParentA a, @MappingTarget ParentB b) {
System.out.println("aToBParent_after");
}
@AfterMapping
default void bToAParent_after(ParentB b, @MappingTarget ParentA a) {
System.out.println("bToAParent_after");
}
}
//Before/After Mapping methods need to be on a class because the Impl instantiates the class/interface that contains them
public static class ParentMappingAfterConfig {
@AfterMapping
public void aToBParent_after(ParentA a, @MappingTarget ParentB b) {
System.out.println("aToBParent_after");
}
@AfterMapping
public void bToAParent_after(ParentB b, @MappingTarget ParentA a) {
System.out.println("bToAParent_after");
}
}
@MapperConfig(uses = ChildMappingAfterConfig.class)
public interface ChildMappingConfig extends ParentMappingConfig{
@InheritConfiguration(name="aToBParent")
@Mapping(target = "childB", source = "childA")
@Mapping(target = "childBIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore", "childAIgnore" }) // Have to repeat parentAIgnore because this replaces rather than extends the inherited ignoreUnmappedSourceProperties
ChildB aToBChild(ChildA a);
@InheritConfiguration(name="bToAParent")
@InheritInverseConfiguration(name="aToBChild")
@Mapping(target = "childAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore", "childBIgnore" }) // Have to repeat parentBIgnore because this replaces rather than extends the inherited ignoreUnmappedSourceProperties
ChildA bToAChild(ChildB b);
}
public static class ChildMappingAfterConfig extends ParentMappingAfterConfig{
@AfterMapping
public void aToBChild_after(ChildA a, @MappingTarget ChildB b) {
System.out.println("aToBChild_after");
}
@AfterMapping
public void bToAChild_after(ChildB b, @MappingTarget ChildA a) {
System.out.println("bToAChild_after");
}
}
@Mapper(config = ChildMappingConfig.class, unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.EXPLICIT)
public interface ChildMapping {
@InheritConfiguration(name="aToBChild"
void aToBChild(ChildA a, @MappingTarget ChildB b);
@InheritConfiguration(name="bToAChild")
void bToAChild(ChildB b, @MappingTarget ChildA a);
}
@MapperConfig(uses = GrandMappingAfterConfig.class)
public interface GrandMappingConfig extends ChildMappingConfig{
@InheritConfiguration(name="aToBChild")
@Mapping(target = "grandB", source = "grandA")
@Mapping(target = "grandBIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentAIgnore", "childAIgnore", "grandAIgnore"})
GrandB aToBGrand(GrandA a);
@InheritConfiguration(name="bToAChild")
@InheritInverseConfiguration(name="aToBGrand")
@Mapping(target = "grandAIgnore", ignore = true)
@BeanMapping(ignoreUnmappedSourceProperties = { "parentBIgnore", "childBIgnore", "grandBIgnore" })
GrandA bToAGrand(GrandB b);
}
public static class GrandMappingAfterConfig extends ChildMappingAfterConfig{
@AfterMapping
public void aToBGrand_after(GrandA a, @MappingTarget GrandB b) {
System.out.println("aToBGrand_after");
}
@AfterMapping
public void bToAGrand_after(GrandB b, @MappingTarget GrandA a) {
System.out.println("bToAGrand_after");
}
}
@Mapper(config = GrandMappingConfig.class, unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR, mappingInheritanceStrategy = MappingInheritanceStrategy.EXPLICIT)
public interface GrandMapping {
@InheritConfiguration(name="aToBGrand")
void aToBGrand(GrandA a, @MappingTarget GrandB b);
@InheritConfiguration(name="bToAGrand")
void bToAGrand(GrandB b, @MappingTarget GrandA a);
}
}Produce the following Impl: public class InheritanceTestWithIgnore$GrandMappingImpl implements InheritanceTestWithIgnore.GrandMapping {
private final InheritanceTestWithIgnore.GrandMappingAfterConfig grandMappingAfterConfig = new InheritanceTestWithIgnore.GrandMappingAfterConfig();
@Override
public void aToBGrand(InheritanceTestWithIgnore.GrandA a, InheritanceTestWithIgnore.GrandB b) {
if ( a == null ) {
return;
}
b.grandB = a.grandA;
b.childB = a.childA;
b.parentB = a.parentA;
grandMappingAfterConfig.aToBParent_after( a, b );
grandMappingAfterConfig.aToBChild_after( a, b );
grandMappingAfterConfig.aToBGrand_after( a, b );
}
@Override
public void bToAGrand(InheritanceTestWithIgnore.GrandB b, InheritanceTestWithIgnore.GrandA a) {
if ( b == null ) {
return;
}
a.grandA = b.grandB;
a.childA = b.childB;
a.parentA = b.parentB;
grandMappingAfterConfig.bToAParent_after( b, a );
grandMappingAfterConfig.bToAChild_after( b, a );
grandMappingAfterConfig.bToAGrand_after( b, a );
}
} |
Uh oh!
There was an error while loading. Please reload this page.
Because $InheritConfiguration "cannot refer to methods in a used mapper", we have two options:
Define the parent and child methods in the same class.
Define the methods in parent-child inheritance. Parent method in a parent class. Child method in a child class that inherits from the parent class.
For large projects, 2 is the only practical choice. Otherwise we'd have a single class with thousands of methods.
And this means that the child class will "see" both the parent method and the child method. Which means that mapstruct will create both methods in the subclass implementation, even though only one method will be used. (An unused method will also be created in the superclass implementation.)
This wastes a bit of storage, but that is a trivial issue. My concern is about performance: Does mapstruct do the analysis twice (once for the parent and once for the child) or does it do the analysis once then copy the method to both places?
(I am seeing performance issues during building and looking for alternatives. My current solution is to not use $InheritConfiguration and manually delegate to the parent mapping method. However, the price is that I lose the ability to see if I have missed mapping any properties. See comment #3213 (comment))
Thanks.
All reactions