”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 使用 MapStruct 映射继承层次结构

使用 MapStruct 映射继承层次结构

发布于2024-11-08
浏览:406

Mapping inheritance hierarchies with MapStruct

Intro

MapStruct provides a rich set of features for mapping Java types. The technical documentation describes extensively the classes and annotations provided by MapStruct and how to use them. More complex use cases are described in several community written articles all over the web. To complement the pool of available articles, this article will focus on mapping inheritance hierarchies and provide a possible solution that offers simplicity and reusability. I assume that the reader has a basic knowledge of MapStruct. If you're interested in a running example, feel free to check out this repo and try things out.

Example

To demonstrate the capabilities of MapStruct in a simple way, we will use a very small and thus useless domain model for which the use of MapStruct seems overly complex, but which allows the code snippets to remain simple throughout the article. The real benefits of MapStruct become apparent especially with larger models.

// Source classes
public class SourceProject {
  private String name;
  private LocalDate dueDate;
  // getters   setters omitted throughout the code
}

// Target classes
public class TargetProject {
  private ProjectInformation projectInformation;
}

public class ProjectInformation {
  private String projectName;
  private LocalDate endDate;
}

As you can see, the source and target entities express the same information but are structured slightly differently. A mapper can be defined like this...

@Mapper
public interface ProjectMapper {
  @Mapping(target = "projectInformation.projectName", source = "name")
  @Mapping(target = "projectInformation.endDate", source = "dueDate")
  TargetProject mapProject(SourceProject source);
}

...and MapStruct will generate code that will look like this:

public class ProjectMapperImpl implements ProjectMapper {

    @Override
    public TargetProject mapProject(SourceProject source) {
        if ( source == null ) {
            return null;
        }

        TargetProject targetProject = new TargetProject();

        targetProject.setProjectInformation( sourceProjectToProjectInformation( source ) );

        return targetProject;
    }

    protected ProjectInformation sourceProjectToProjectInformation(SourceProject sourceProject) {
        if ( sourceProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceProject.getName() );
        projectInformation.setEndDate( sourceProject.getDueDate() );

        return projectInformation;
    }
}

Now let's introduce some new entities that use inheritance:

// Source classes
@Data
public class SourceScrumProject extends SourceProject {
  private Integer velocity;
}

// Target classes
@Data
public class TargetScrumProject extends TargetProject {
  private Velocity velocity;
}

@Data
public class Velocity {
  private Integer value;
}

If we want to use the parent mapper universally to map both the parent entity and child entities, we can use the @SubclassMapping annotation, which generates a dispatching via instanceof checks to the mapping of possible child classes.

@Mapper
public interface ProjectMapper {
  @Mapping(target = "projectInformation.projectName", source = "name")
  @Mapping(target = "projectInformation.endDate", source = "dueDate")
  @SubclassMapping(source = SourceScrumProject.class, target = TargetScrumProject.class)
  TargetProject mapProject(SourceProject source);

  @Mapping(target = "velocity.value", source = "velocity")
  @Mapping(target = "projectInformation.projectName", source = "name")
  @Mapping(target = "projectInformation.endDate", source = "dueDate")
  TargetScrumProject mapScrumProject(SourceScrumProject source);
}

This generates the following code.

public class ProjectMapperImpl implements ProjectMapper {

    @Override
    public TargetProject mapProject(SourceProject source) {
        if ( source == null ) {
            return null;
        }

        if (source instanceof SourceScrumProject) {
            return mapScrumProject( (SourceScrumProject) source );
        }
        else {
            TargetProject targetProject = new TargetProject();

            targetProject.setProjectInformation( sourceProjectToProjectInformation( source ) );

            return targetProject;
        }
    }

    @Override
    public TargetScrumProject mapScrumProject(SourceScrumProject source) {
        if ( source == null ) {
            return null;
        }

        TargetScrumProject targetScrumProject = new TargetScrumProject();

        targetScrumProject.setVelocity( sourceScrumProjectToVelocity( source ) );
        targetScrumProject.setProjectInformation( sourceScrumProjectToProjectInformation( source ) );

        return targetScrumProject;
    }

    protected ProjectInformation sourceProjectToProjectInformation(SourceProject sourceProject) {
        if ( sourceProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceProject.getName() );
        projectInformation.setEndDate( sourceProject.getDueDate() );

        return projectInformation;
    }

    protected Velocity sourceScrumProjectToVelocity(SourceScrumProject sourceScrumProject) {
        if ( sourceScrumProject == null ) {
            return null;
        }

        Velocity velocity = new Velocity();

        velocity.setValue( sourceScrumProject.getVelocity() );

        return velocity;
    }

    protected ProjectInformation sourceScrumProjectToProjectInformation(SourceScrumProject sourceScrumProject) {
        if ( sourceScrumProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceScrumProject.getName() );
        projectInformation.setEndDate( sourceScrumProject.getDueDate() );

        return projectInformation;
    }
}

We can already see some problems here:

  1. We are duplicating the @Mapping annotation from the parent mapping.
  2. Parts of the generated code are duplicated (sourceProjectToProjectInformation and sourceScrumProjectToProjectInformation).
  3. The interface becomes wider as it contains mapping methods for both the parent and child entities.

With only these two fields this doesn't seem terrible, but imagine what the generated code would look like if we had more child classes containing more fields. The effect would be much bigger.

Let's try to tackle problem #1. MapStruct offers the annotation @InheritConfiguration which allows us to reuse mapping configuration from either the same class or the mapping configuration class used:

@Mapper
public interface ProjectMapper {
  @Mapping(target = "projectInformation.projectName", source = "name")
  @Mapping(target = "projectInformation.endDate", source = "dueDate")
  @SubclassMapping(source = SourceScrumProject.class, target = TargetScrumProject.class)
  TargetProject mapProject(SourceProject source);

  @Mapping(target = "velocity.value", source = "velocity")
  @InheritConfiguration(name = "mapProject")
  TargetScrumProject mapScrumProject(SourceScrumProject source);
}

This at least saves us a lot of duplicate configuration. Spoiler: We will not want to use this anymore at a later stage. But let's first tackle problem #2 and #3.

Since we could have potentially wide interfaces with a lot of duplicated code, using, understanding and debugging of the generated code could become more difficult. It would be easier if we had a mapper for each subclass that is self-contained and only either dispatches to a child mapper or performs a mapping, but not both. So let's move the mapping of the Scrum projects to a separate interface.

@Mapper(uses = ScrumProjectMapper.class)
public interface ProjectMapper {
  @Mapping(target = "projectInformation.projectName", source = "name")
  @Mapping(target = "projectInformation.endDate", source = "dueDate")
  @SubclassMapping(source = SourceScrumProject.class, target = TargetScrumProject.class)
  TargetProject mapProject(SourceProject source);
}

@Mapper
public interface ScrumProjectMapper {
  @Mapping(target = "velocity.value", source = "velocity")
  @InheritConfiguration(name = "mapProject") // not working
  TargetScrumProject mapScrumProject(SourceScrumProject source);
}

We tell ProjectMapper to dispatch the mapping of ScrumProjects to ScrumProjectMapper via the uses-clause. The problem here is that the configuration from the mapProject-method is no longer visible to the ScrumProjectMapper. We could of course let it extend ProjectMapper, but then we have the problem of the wide interface and duplicated code again, as all methods are merged into ScrumProjectMapper. We could instead make ProjectMapper a config using the @MapperConfig annotation and reference it in ScrumProjectMapper, but since it also uses ScrumProjectMapper in the uses-clause to enable the dispatching, MapStruct would complain about the circular dependency. Furthermore, if we have an inheritance hierarchy with a height > 1, we quickly notice that MapStruct does not pass the config down the mapper hierarchy more than one level, making the config at level 0 unavailable on levels 2 and beyond.

Fortunately, there is a solution. The @Mapping annotation can be applied to other annotations. By declaring an annotation ProjectMappings, which basically wraps all the mapping information for Projects, we can reuse it anywhere we want. Let's see what this could look like.

@Mapper(uses = ScrumProjectMapper.class)
public interface ProjectMapper {
  @Mappings
  @SubclassMapping(source = SourceScrumProject.class, target = TargetScrumProject.class)
  TargetProject mapProject(SourceProject source);

  @Mapping(target = "projectInformation.projectName", source = "name")
  @Mapping(target = "projectInformation.endDate", source = "dueDate")
  @interface Mappings {
  }
}

@Mapper
public interface ScrumProjectMapper {
  @Mapping(target = "velocity.value", source = "velocity")
  @ProjectMapper.Mappings
  TargetScrumProject mapScrumProject(SourceScrumProject source);
}

Imagine that we have more child classes than just ScrumProject. By simply bundling the mapping information in the shared annotation we can centralize the information and avoid all the pitfalls that come with duplication. This also works for deeper inheritance hierarchies. I just need to annotate my mapping method with the @Mappings-annotation of the parent mapper that uses the annotation of its parent mapper, and so on.

We can see in the generated code now that the mappers either dispatch or do the mapping only for the classes they're built for:

public class ProjectMapperImpl implements ProjectMapper {

    private final ScrumProjectMapper scrumProjectMapper = Mappers.getMapper( ScrumProjectMapper.class );

    @Override
    public TargetProject mapProject(SourceProject source) {
        if ( source == null ) {
            return null;
        }

        if (source instanceof SourceScrumProject) {
            return scrumProjectMapper.mapScrumProject( (SourceScrumProject) source );
        }
        else {
            TargetProject targetProject = new TargetProject();

            targetProject.setProjectInformation( sourceProjectToProjectInformation( source ) );

            return targetProject;
        }
    }

    protected ProjectInformation sourceProjectToProjectInformation(SourceProject sourceProject) {
        if ( sourceProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceProject.getName() );
        projectInformation.setEndDate( sourceProject.getDueDate() );

        return projectInformation;
    }
}

public class ScrumProjectMapperImpl implements ScrumProjectMapper {

    @Override
    public TargetScrumProject mapScrumProject(SourceScrumProject source) {
        if ( source == null ) {
            return null;
        }

        TargetScrumProject targetScrumProject = new TargetScrumProject();

        targetScrumProject.setVelocity( sourceScrumProjectToVelocity( source ) );
        targetScrumProject.setProjectInformation( sourceScrumProjectToProjectInformation( source ) );

        return targetScrumProject;
    }

    protected Velocity sourceScrumProjectToVelocity(SourceScrumProject sourceScrumProject) {
        if ( sourceScrumProject == null ) {
            return null;
        }

        Velocity velocity = new Velocity();

        velocity.setValue( sourceScrumProject.getVelocity() );

        return velocity;
    }

    protected ProjectInformation sourceScrumProjectToProjectInformation(SourceScrumProject sourceScrumProject) {
        if ( sourceScrumProject == null ) {
            return null;
        }

        ProjectInformation projectInformation = new ProjectInformation();

        projectInformation.setProjectName( sourceScrumProject.getName() );
        projectInformation.setEndDate( sourceScrumProject.getDueDate() );

        return projectInformation;
    }
}

I'd say this makes understanding and debugging a single mapper easier. As we've already covered a lot of ground, let's wrap things up at this point. There are still some edge cases that can lead to further problems, but these will be covered in the next part.

Wrap-up

Writing mappers with MapStruct for inheritance hierarchies should be a common task and easy to achieve, but you can quickly get stuck in some of MapStruct's quirks. Mapping the entire hierarchy in one class results in large classes implementing wide interfaces that are difficult to read and debug. When splitting the mappers into one per class, we want to reuse the mapping information from the parent mappers to avoid duplicating mapping information. Extending the parent mapper to make its mapping configuration visible for use in @InheritConfiguration is not desirable, as we will again have the problem of a wide interface with a lot of duplicated code. Using the parent mapper as a config is also not possible due to circular dependencies. We could see that the creating a custom annotation that bundles the mapping information for use in child mappers solves the problem. By additionally using SubclassMapping, the parent mapper provides the bundled information on how to map the entity it knows, contains only the mapping for exactly that class and dispatches the mapping of any other child entities down the hierarchy of mappers.

版本声明 本文转载于:https://dev.to/jholsch/mapping-inheritance-hierarchies-with-mapstruct-7p0?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • Python中嵌套函数与闭包的区别是什么
    Python中嵌套函数与闭包的区别是什么
    嵌套函数与python 在python中的嵌套函数不被考虑闭合,因为它们不符合以下要求:不访问局部范围scliables to incling scliables在封装范围外执行范围的局部范围。 make_printer(msg): DEF打印机(): 打印(味精) ...
    编程 发布于2025-07-03
  • 我可以将加密从McRypt迁移到OpenSSL,并使用OpenSSL迁移MCRYPT加密数据?
    我可以将加密从McRypt迁移到OpenSSL,并使用OpenSSL迁移MCRYPT加密数据?
    将我的加密库从mcrypt升级到openssl 问题:是否可以将我的加密库从McRypt升级到OpenSSL?如果是这样,如何?答案:是的,可以将您的Encryption库从McRypt升级到OpenSSL。可以使用openssl。附加说明: [openssl_decrypt()函数要求iv参...
    编程 发布于2025-07-03
  • 如何限制动态大小的父元素中元素的滚动范围?
    如何限制动态大小的父元素中元素的滚动范围?
    在交互式接口中实现垂直滚动元素的CSS高度限制问题:考虑一个布局,其中我们具有与用户垂直滚动一起移动的可滚动地图div,同时与固定的固定sidebar保持一致。但是,地图的滚动无限期扩展,超过了视口的高度,阻止用户访问页面页脚。$("#map").css({ marginT...
    编程 发布于2025-07-03
  • 如何使用Python有效地以相反顺序读取大型文件?
    如何使用Python有效地以相反顺序读取大型文件?
    在python 中,如果您使用一个大文件,并且需要从最后一行读取其内容,则在第一行到第一行,Python的内置功能可能不合适。这是解决此任务的有效解决方案:反向行读取器生成器 == ord('\ n'): 缓冲区=缓冲区[:-1] ...
    编程 发布于2025-07-03
  • 如何使用替换指令在GO MOD中解析模块路径差异?
    如何使用替换指令在GO MOD中解析模块路径差异?
    在使用GO MOD时,在GO MOD 中克服模块路径差异时,可能会遇到冲突,其中3个Party Package将另一个PAXPANCE带有导入式套件之间的另一个软件包,并在导入式套件之间导入另一个软件包。如回声消息所证明的那样: go.etcd.io/bbolt [&&&&&&&&&&&&&&&&...
    编程 发布于2025-07-03
  • 如何使用Regex在PHP中有效地提取括号内的文本
    如何使用Regex在PHP中有效地提取括号内的文本
    php:在括号内提取文本在处理括号内的文本时,找到最有效的解决方案是必不可少的。一种方法是利用PHP的字符串操作函数,如下所示: 作为替代 $ text ='忽略除此之外的一切(text)'; preg_match('#((。 &&& [Regex使用模式来搜索特...
    编程 发布于2025-07-03
  • 如何高效地在一个事务中插入数据到多个MySQL表?
    如何高效地在一个事务中插入数据到多个MySQL表?
    mySQL插入到多个表中,该数据可能会产生意外的结果。虽然似乎有多个查询可以解决问题,但将从用户表的自动信息ID与配置文件表的手动用户ID相关联提出了挑战。使用Transactions和last_insert_id() 插入用户(用户名,密码)值('test','test...
    编程 发布于2025-07-03
  • 如何在Java的全屏独家模式下处理用户输入?
    如何在Java的全屏独家模式下处理用户输入?
    Handling User Input in Full Screen Exclusive Mode in JavaIntroductionWhen running a Java application in full screen exclusive mode, the usual event ha...
    编程 发布于2025-07-03
  • 如何克服PHP的功能重新定义限制?
    如何克服PHP的功能重新定义限制?
    克服PHP的函数重新定义限制在PHP中,多次定义一个相同名称的函数是一个no-no。尝试这样做,如提供的代码段所示,将导致可怕的“不能重新列出”错误。 但是,PHP工具腰带中有一个隐藏的宝石:runkit扩展。它使您能够灵活地重新定义函数。 runkit_function_renction_re...
    编程 发布于2025-07-03
  • 如何使用“ JSON”软件包解析JSON阵列?
    如何使用“ JSON”软件包解析JSON阵列?
    parsing JSON与JSON软件包 QUALDALS:考虑以下go代码:字符串 } func main(){ datajson:=`[“ 1”,“ 2”,“ 3”]`` arr:= jsontype {} 摘要:= = json.unmarshal([] byte(...
    编程 发布于2025-07-03
  • 如何处理PHP文件系统功能中的UTF-8文件名?
    如何处理PHP文件系统功能中的UTF-8文件名?
    在PHP的Filesystem functions中处理UTF-8 FileNames 在使用PHP的MKDIR函数中含有UTF-8字符的文件很多flusf-8字符时,您可能会在Windows Explorer中遇到comploreer grounder grounder grounder gro...
    编程 发布于2025-07-03
  • 如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?
    如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?
    将pandas dataframe列转换为dateTime格式示例:使用column(mycol)包含以下格式的以下dataframe,以自定义格式:})指定的格式参数匹配给定的字符串格式。转换后,MyCol列现在将包含DateTime对象。 date oped filtering > = p...
    编程 发布于2025-07-03
  • 如何修复\“常规错误:2006 MySQL Server在插入数据时已经消失\”?
    如何修复\“常规错误:2006 MySQL Server在插入数据时已经消失\”?
    How to Resolve "General error: 2006 MySQL server has gone away" While Inserting RecordsIntroduction:Inserting data into a MySQL database can...
    编程 发布于2025-07-03
  • 为什么PYTZ最初显示出意外的时区偏移?
    为什么PYTZ最初显示出意外的时区偏移?
    与pytz 最初从pytz获得特定的偏移。例如,亚洲/hong_kong最初显示一个七个小时37分钟的偏移: 差异源利用本地化将时区分配给日期,使用了适当的时区名称和偏移量。但是,直接使用DateTime构造器分配时区不允许进行正确的调整。 example pytz.timezone(...
    编程 发布于2025-07-03
  • 为什么不使用CSS`content'属性显示图像?
    为什么不使用CSS`content'属性显示图像?
    在Firefox extemers属性为某些图像很大,&& && && &&华倍华倍[华氏华倍华氏度]很少见,却是某些浏览属性很少,尤其是特定于Firefox的某些浏览器未能在使用内容属性引用时未能显示图像的情况。这可以在提供的CSS类中看到:。googlepic { 内容:url(&#...
    编程 发布于2025-07-03

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3