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

使用 MapStruct 映射继承层次结构

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

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]删除
最新教程 更多>
  • 如何在JavaScript对象中动态设置键?
    如何在JavaScript对象中动态设置键?
    在尝试为JavaScript对象创建动态键时,如何使用此Syntax jsObj['key' i] = 'example' 1;不工作。正确的方法采用方括号: jsobj ['key''i] ='example'1; 在JavaScript中,数组是一...
    编程 发布于2025-04-11
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-04-11
  • HTML格式标签
    HTML格式标签
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    编程 发布于2025-04-11
  • 如何使用PHP将斑点(图像)正确插入MySQL?
    如何使用PHP将斑点(图像)正确插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call ...
    编程 发布于2025-04-11
  • 如何使用“ JSON”软件包解析JSON阵列?
    如何使用“ JSON”软件包解析JSON阵列?
    parsing JSON与JSON软件包 QUALDALS:考虑以下go代码:字符串 } func main(){ datajson:=`[“ 1”,“ 2”,“ 3”]`` arr:= jsontype {} 摘要:= = json.unmarshal([] byte(...
    编程 发布于2025-04-11
  • 如何在无序集合中为元组实现通用哈希功能?
    如何在无序集合中为元组实现通用哈希功能?
    在未订购的集合中的元素要纠正此问题,一种方法是手动为特定元组类型定义哈希函数,例如: template template template 。 struct std :: hash { size_t operator()(std :: tuple const&tuple)const {...
    编程 发布于2025-04-11
  • 如何正确使用与PDO参数的查询一样?
    如何正确使用与PDO参数的查询一样?
    在pdo 中使用类似QUERIES在PDO中的Queries时,您可能会遇到类似疑问中描述的问题:此查询也可能不会返回结果,即使$ var1和$ var2包含有效的搜索词。错误在于不正确包含%符号。通过将变量包含在$ params数组中的%符号中,您确保将%字符正确替换到查询中。没有此修改,PDO...
    编程 发布于2025-04-11
  • 找到最大计数时,如何解决mySQL中的“组函数\”错误的“无效使用”?
    找到最大计数时,如何解决mySQL中的“组函数\”错误的“无效使用”?
    如何在mySQL中使用mySql 检索最大计数,您可能会遇到一个问题,您可能会在尝试使用以下命令:理解错误正确找到由名称列分组的值的最大计数,请使用以下修改后的查询: 计数(*)为c 来自EMP1 按名称组 c desc订购 限制1 查询说明 select语句提取名称列和每个名称...
    编程 发布于2025-04-11
  • 如何使用Python理解有效地创建字典?
    如何使用Python理解有效地创建字典?
    在python中,词典综合提供了一种生成新词典的简洁方法。尽管它们与列表综合相似,但存在一些显着差异。与问题所暗示的不同,您无法为钥匙创建字典理解。您必须明确指定键和值。 For example:d = {n: n**2 for n in range(5)}This creates a dicti...
    编程 发布于2025-04-11
  • 为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    为什么不````''{margin:0; }`始终删除CSS中的最高边距?
    在CSS 问题:不正确的代码: 全球范围将所有余量重置为零,如提供的代码所建议的,可能会导致意外的副作用。解决特定的保证金问题是更建议的。 例如,在提供的示例中,将以下代码添加到CSS中,将解决余量问题: body H1 { 保证金顶:-40px; } 此方法更精确,避免了由全局保证金重置引...
    编程 发布于2025-04-11
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-04-11
  • 如何使用PHP从XML文件中有效地检索属性值?
    如何使用PHP从XML文件中有效地检索属性值?
    从php $xml = simplexml_load_file($file); foreach ($xml->Var[0]->attributes() as $attributeName => $attributeValue) { echo $attributeName,...
    编程 发布于2025-04-11
  • 如何在Java中执行命令提示命令,包括目录更改,包括目录更改?
    如何在Java中执行命令提示命令,包括目录更改,包括目录更改?
    在java 通过Java通过Java运行命令命令可能很具有挑战性。尽管您可能会找到打开命令提示符的代码段,但他们通常缺乏更改目录并执行其他命令的能力。 solution:使用Java使用Java,使用processBuilder。这种方法允许您:启动一个过程,然后将其标准错误重定向到其标准输出。...
    编程 发布于2025-04-11
  • 如何为PostgreSQL中的每个唯一标识符有效地检索最后一行?
    如何为PostgreSQL中的每个唯一标识符有效地检索最后一行?
    postgresql:为每个唯一标识符提取最后一行,在Postgresql中,您可能需要遇到与在数据库中的每个不同标识相关的信息中提取信息的情况。考虑以下数据:[ 1 2014-02-01 kjkj 在数据集中的每个唯一ID中检索最后一行的信息,您可以在操作员上使用Postgres的有效效率: ...
    编程 发布于2025-04-11
  • 如何处理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-04-11

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

Copyright© 2022 湘ICP备2022001581号-3