」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 使用 MapStruct 來映射繼承層次結構

使用 MapStruct 來映射繼承層次結構

發佈於2024-11-08
瀏覽:472

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]刪除
最新教學 更多>
  • 如何解決 MySQL 時區錯誤:Java 中的「伺服器時區值中歐時間」?
    如何解決 MySQL 時區錯誤:Java 中的「伺服器時區值中歐時間」?
    MySQL 連接器在Java 資料庫連線期間出現「伺服器時區值中歐時間」錯誤使用建立資料庫連線時會出現此問題Java 中的MySQL 連接器。此錯誤訊息表示提供的伺服器時區值「中歐時間」無法辨識或代表多個時區。若要解決此問題,必須使用 serverTimezone 設定屬性明確指定伺服器時區值。 常...
    程式設計 發佈於2024-11-08
  • 為什麼應該避免在 JSX Props 中使用箭頭函數或綁定?
    為什麼應該避免在 JSX Props 中使用箭頭函數或綁定?
    為什麼在JSX Props 中使用箭頭函數或Bind 是禁忌使用React 時,避免使用箭頭函數或Bind 非常重要在JSX屬性中綁定。這種做法可能會導致效能問題和不正確的行為。 效能問題在 JSX props 中使用箭頭函數或綁定會強制在每次渲染時重新建立這些函數。這意味著:舊函數被丟棄,觸發垃圾...
    程式設計 發佈於2024-11-08
  • 自動模式的 CSS 主題選擇器 [教學]
    自動模式的 CSS 主題選擇器 [教學]
    This tutorial shows you how to create a theme selector in Svelte, enabling multiple theme options for your website. It also includes an automatic them...
    程式設計 發佈於2024-11-08
  • 了解 Java 中的靜態實用方法
    了解 Java 中的靜態實用方法
    在现代软件开发中,非常重视干净、可重用和有效的编码。 Java 中的一项功能对实现这一目标大有帮助,称为静态实用方法。本文将探讨什么是静态实用方法、它们的好处、常见用例以及有效实现这些方法的最佳实践。 什么是静态实用方法? 静态实用方法是属于类的方法,而不是属于类的实例。这些方法是使...
    程式設計 發佈於2024-11-08
  • ## 如何在 JavaScript 中限制函數執行:自訂解決方案與函式庫解決方案
    ## 如何在 JavaScript 中限制函數執行:自訂解決方案與函式庫解決方案
    透過自訂實作實作 JavaScript 中的簡單節流使用 JavaScript 時,控制函數執行速率至關重要。節流函數限制函數呼叫的頻率,防止繁重的處理或重複的使用者操作。 在這篇文章中,我們提出了一個簡單的自訂節流函數來實現此目的,而不依賴 Lodash 或 Underscore 等外部函式庫。 ...
    程式設計 發佈於2024-11-08
  • 了解 WebSocket:React 開發人員綜合指南
    了解 WebSocket:React 開發人員綜合指南
    Understanding WebSockets: A Comprehensive Guide for React Developers In today’s world of modern web applications, real-time communication is ...
    程式設計 發佈於2024-11-08
  • 如何在 macOS 上安裝並啟用 Imagick for PHP
    如何在 macOS 上安裝並啟用 Imagick for PHP
    如果您在 macOS 上工作並且需要安裝 Imagick for PHP 8.3,則可能會遇到預設安裝較舊版本 PHP(例如 PHP 8.0)的問題。在這篇文章中,我將引導您完成確保 Imagick 已安裝並針對 PHP 8.3 正確配置的步驟。 步驟 1:透過 Homebrew ...
    程式設計 發佈於2024-11-08
  • 如何使用 JavaScript 為物件陣列新增附加屬性?
    如何使用 JavaScript 為物件陣列新增附加屬性?
    擴展具有附加屬性的物件陣列程式設計中普遍存在的任務涉及使用附加屬性來增強現有物件陣列。為了說明這個概念,請考慮包含兩個元素的物件陣列:Object {Results:Array[2]} Results:Array[2] [0-1] 0:Object id=1 name: &quo...
    程式設計 發佈於2024-11-08
  • 如何解決 CSS 中可變字體的文字筆畫問題?
    如何解決 CSS 中可變字體的文字筆畫問題?
    文本描邊難題:解決CSS 相容性問題使用-webkit-text-lines 創建引人注目的文本效果是網頁設計師的一項基本技術。但是,當將此屬性與可變字體一起使用時,可能會出現意外的筆劃行為。這種不一致不僅限於 Chrome,而是不同瀏覽器中更普遍的問題。 問題的癥結:可變字體和筆畫衝突可變字體具有...
    程式設計 發佈於2024-11-08
  • C++ 中的私有虛擬方法:平衡封裝與重寫
    C++ 中的私有虛擬方法:平衡封裝與重寫
    了解 C 中私有虛擬方法的好處 在物件導向程式設計中,私有方法封裝實作細節並限制其在一個班級。然而,在 C 中,虛函數提供後期綁定並允許物件的多態行為。透過結合這些概念,私有虛擬方法提供了獨特的優勢。 考慮以下用法,其中 HTMLDocument 繼承自多個基底類別:class HTMLDocume...
    程式設計 發佈於2024-11-08
  • 齋浦爾資料科學研究所:傳統與科技的邂逅
    齋浦爾資料科學研究所:傳統與科技的邂逅
    斋浦尔,粉红之城,长期以来一直是一座拥有丰富文化遗产、雄伟宫殿和充满活力的传统的城市,但这座城市的另一个特征是教育和技术进步。这是通过斋浦尔的几个数据科学研究所推出的,通过这些机构引导学生和专业人士进入快速变化的技术世界。 这些机构融合了传统与创新,在培养这座城市的未来科技人才方面发挥着重要作用。在...
    程式設計 發佈於2024-11-08
  • 如何根據多個條件過濾 JavaScript 物件數組?
    如何根據多個條件過濾 JavaScript 物件數組?
    基於多個條件過濾JavaScript中的數組問題陳述給定一個對象數組和一個過濾器對象,目標是過濾和根據篩選器中指定的多個條件簡化陣列。但是,當過濾器包含多個屬性時,會出現一個特定問題。 建議的解決方案考慮以下程式碼段:function filterUsers(users, filter) { v...
    程式設計 發佈於2024-11-08
  • 理解 Laravel 11 中 pluck() 和 select() 之間的差異
    理解 Laravel 11 中 pluck() 和 select() 之間的差異
    Laravel 是最受歡迎的 PHP 架構之一,提供了一系列強大的資料操作方法。其中,pluck() 和 select() 在處理集合時經常使用。儘管它們看起來相似,但它們的目的卻不同。在本文中,我們將探討這兩種方法之間的差異,解釋何時使用每種方法,並提供實際的編碼範例來示範它們在 Laravel ...
    程式設計 發佈於2024-11-08
  • 什麼是 Cloudflare? Web 效能與安全性公司概述
    什麼是 Cloudflare? Web 效能與安全性公司概述
    在快節奏的數位世界中,網站的速度、安全性和可靠性對於企業和使用者都至關重要。 Cloudflare 已成為確保網站平穩、安全和高效運作的基石。但 Cloudflare 到底是什麼?為什麼它成為網站所有者如此重要的工具?讓我們深入了解它的作用和產品。 Cloudflare 簡介 Cl...
    程式設計 發佈於2024-11-08
  • 如何最佳化 MySQL 索引效能以加快查詢速度?
    如何最佳化 MySQL 索引效能以加快查詢速度?
    優化MySQL索引效能要有效檢查MySQL索引的效能,可以使用以下查詢:EXPLAIN EXTENDED SELECT col1, col2, col3, COUNT(1) FROM table_name WHERE col1 = val GROUP BY col1 ORDER BY col...
    程式設計 發佈於2024-11-08

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3