Guide to Using ModelMapper – ModelMapper使用指南

最后修改: 2021年 10月 9日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In a previous tutorial, we’ve seen how to map lists with ModelMapper.

在之前的教程中,我们已经看到了如何用ModelMapper映射列表

In this tutorial, we’re going to show how to map our data between differently structured objects in ModelMapper.

在本教程中,我们将展示如何在ModelMapper中不同结构的对象之间映射我们的数据。

Although ModelMapper’s default conversion works pretty well in typical cases, we’ll primarily focus on how to match objects that aren’t similar enough to handle using the default configuration.

尽管 ModelMapper 的默认转换在典型情况下工作得很好,我们将主要关注如何匹配那些不够相似的对象,以使用默认配置来处理。

So, we’ll set our sights on property mappings and configuration changes this time.

因此,这次我们将把目光放在属性映射和配置变更上。

2. Maven Dependency

2.Maven的依赖性

To start using the ModelMapper library, we’ll add the dependency to our pom.xml:

为了开始使用 ModelMapper ,我们将在我们的pom.xml中添加该依赖关系。

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>2.4.4</version>
</dependency>

3. Default Configuration

3.默认配置

ModelMapper provides a drop-in solution when our source and destination objects are similar to each other.

当我们的源对象和目标对象彼此相似时,ModelMapper提供了一个直接的解决方案。

Let’s have a look at Game and GameDTO, our domain object and corresponding data transfer object, respectively:

让我们看看GameGameDTO,分别是我们的域对象和相应的数据传输对象。

public class Game {

    private Long id;
    private String name;
    private Long timestamp;

    private Player creator;
    private List<Player> players = new ArrayList<>();

    private GameSettings settings;

    // constructors, getters and setters
}

public class GameDTO {

    private Long id;
    private String name;

    // constructors, getters and setters
}

GameDTO contains only two fields, but the field types and names perfectly match the source.

GameDTO只包含两个字段,但字段类型和名称与来源完全一致。

In such a case, ModelMapper handles the conversion without additional configuration:

在这种情况下,ModelMapper处理转换,不需要额外配置。

@BeforeEach
public void setup() {
    this.mapper = new ModelMapper();
}

@Test
public void whenMapGameWithExactMatch_thenConvertsToDTO() {
    // when similar source object is provided
    Game game = new Game(1L, "Game 1");
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then it maps by default
    assertEquals(game.getId(), gameDTO.getId());
    assertEquals(game.getName(), gameDTO.getName());
}

4. What Is Property Mapping in ModelMapper?

4.什么是 ModelMapper 中的属性映射?

In our projects, most of the time, we need to customize our DTOs. Of course, this will result in different fields, hierarchies and their irregular mappings to each other. Sometimes, we also need more than one DTO for a single source and vice versa.

在我们的项目中,大多数时候,我们需要定制我们的DTOs。当然,这将导致不同的字段、层次结构和它们之间的不规则映射。有时,我们还需要为一个单一的来源提供一个以上的DTO,反之亦然。

Therefore, property mapping gives us a powerful way to extend our mapping logic.

因此,属性映射给了我们一个强大的方式来扩展我们的映射逻辑。

Let’s customize our GameDTO by adding a new field, creationTime:

让我们自定义我们的GameDTO,添加一个新字段,creationTime

public class GameDTO {

    private Long id;
    private String name;
    private Long creationTime;

    // constructors, getters and setters
}

And we’ll map Game‘s timestamp field into GameDTO‘s creationTime field. Notice that the source field name is different from the destination field name this time.

我们将把Gametimestamp字段映射到GameDTOcreationTime字段。请注意,这次的源字段名称与目标字段名称不同。

To define property mappings, we’ll use ModelMapper’s TypeMap.

为了定义属性映射,我们将使用 ModelMapper 的 TypeMap

So, let’s create a TypeMap object and add a property mapping via its addMapping method:

因此,让我们创建一个TypeMap对象,并通过其addMapping方法添加一个属性映射。

@Test
public void whenMapGameWithBasicPropertyMapping_thenConvertsToDTO() {
    // setup
    TypeMap<Game, GameDTO> propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
    propertyMapper.addMapping(Game::getTimestamp, GameDTO::setCreationTime);
    
    // when field names are different
    Game game = new Game(1L, "Game 1");
    game.setTimestamp(Instant.now().getEpochSecond());
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then it maps via property mapper
    assertEquals(game.getId(), gameDTO.getId());
    assertEquals(game.getName(), gameDTO.getName());
    assertEquals(game.getTimestamp(), gameDTO.getCreationTime());
}

4.1. Deep Mappings

4.1.深度映射

There are also different ways of mapping. For instance, ModelMapper can map hierarchies — fields at different levels can be mapped deeply.

也有不同的映射方式。例如,ModelMapper可以映射层次结构–不同层次的字段可以被深度映射。

Let’s define a String field named creator in GameDTO.

让我们在GameDTO中定义一个名为creator字符串域。

However, the source creator field on the Game domain isn’t a simple type but an object — Player:

然而,Game域上的源creator字段不是一个简单的类型,而是一个对象 – Player

public class Player {

    private Long id;
    private String name;
    
    // constructors, getters and setters
}

public class Game {
    // ...
    
    private Player creator;
    
    // ...
}

public class GameDTO {
    // ...
    
    private String creator;
    
    // ...
}

So, we won’t transfer the entire Player object’s data but only the name field, to GameDTO.

因此,我们不会将整个Player对象的数据,而只是将name字段,转移到GameDTO

In order to define the deep mapping, we use TypeMap‘s addMappings method and add an ExpressionMap:

为了定义深度映射,我们使用TypeMapaddMappings方法并添加一个ExpressionMap

@Test
public void whenMapGameWithDeepMapping_thenConvertsToDTO() {
    // setup
    TypeMap<Game, GameDTO> propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
    // add deep mapping to flatten source's Player object into a single field in destination
    propertyMapper.addMappings(
      mapper -> mapper.map(src -> src.getCreator().getName(), GameDTO::setCreator)
    );
    
    // when map between different hierarchies
    Game game = new Game(1L, "Game 1");
    game.setCreator(new Player(1L, "John"));
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then
    assertEquals(game.getCreator().getName(), gameDTO.getCreator());
}

4.2. Skipping Properties

4.2.跳过的属性

Sometimes, we don’t want to expose all the data in our DTOs. Whether to keep our DTOs lighter or conceal some sensible data, those reasons can cause us to exclude some fields when we’re transferring to DTOs.

有时,我们不想暴露我们的DTO中的所有数据。无论是为了保持我们的DTOs更轻便,还是为了隐藏一些合理的数据,这些原因都会导致我们在转移到DTOs时排除一些字段。

Luckily, ModelMapper supports property exclusion via skipping.

幸运的是,ModelMapper支持通过跳过进行属性排除。

Let’s exclude the id field from transferring with the help of the skip method:

让我们在skip方法的帮助下,排除id字段的转移。

@Test
public void whenMapGameWithSkipIdProperty_thenConvertsToDTO() {
    // setup
    TypeMap<Game, GameDTO> propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
    propertyMapper.addMappings(mapper -> mapper.skip(GameDTO::setId));
    
    // when id is skipped
    Game game = new Game(1L, "Game 1");
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then destination id is null
    assertNull(gameDTO.getId());
    assertEquals(game.getName(), gameDTO.getName());
}

Therefore, the id field of GameDTO is skipped and not set.

因此,GameDTOid字段被跳过,没有设置。

4.3. Converter

4.3.转换器

Another provision of ModelMapper is Converter. We can customize conversions for specific sources to destination mappings.

ModelMapper的另一个规定是Converter我们可以为特定的源到目标映射定制转换。

Suppose we have a collection of Players in the Game domain. Let’s transfer the count of Players to GameDTO.

假设我们在Game域中有一个Players的集合。让我们把Players的计数转移到GameDTO

As a first step, we define an integer field, totalPlayers, in GameDTO:

作为第一步,我们在GameDTO中定义一个整数字段,totalPlayers

public class GameDTO {
    // ...

    private int totalPlayers;
  
    // constructors, getters and setters
}

Respectively, we create the collectionToSize Converter:

我们分别创建了collectionToSize Converter

Converter<Collection, Integer> collectionToSize = c -> c.getSource().size();

Finally, we register our Converter via the using method while we’re adding our ExpressionMap:

最后,我们在添加ExpressionMap时,通过using方法注册我们的Converter

propertyMapper.addMappings(
  mapper -> mapper.using(collectionToSize).map(Game::getPlayers, GameDTO::setTotalPlayers)
);

As a result, we map Game‘s getPlayers().size() to GameDTO‘s totalPlayers field:

因此,我们将GamegetPlayers().size()映射到GameDTOtotalPlayers域。

@Test
public void whenMapGameWithCustomConverter_thenConvertsToDTO() {
    // setup
    TypeMap<Game, GameDTO> propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
    Converter<Collection, Integer> collectionToSize = c -> c.getSource().size();
    propertyMapper.addMappings(
      mapper -> mapper.using(collectionToSize).map(Game::getPlayers, GameDTO::setTotalPlayers)
    );
    
    // when collection to size converter is provided
    Game game = new Game();
    game.addPlayer(new Player(1L, "John"));
    game.addPlayer(new Player(2L, "Bob"));
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then it maps the size to a custom field
    assertEquals(2, gameDTO.getTotalPlayers());
}

4.4. Provider

4.4.供应商

In another use case, we sometimes need to provide an instance for the destination object instead of letting ModalMapper initialize it. This is where the Provider comes in handy.

在另一个用例中,我们有时需要为目标对象提供一个实例,而不是让ModalMapper初始化它。这就是Provider的用武之地。

Accordingly, ModelMapper’s Provider is the built-in way to customize the instantiation of destination objects.

因此,ModelMapper的Provider是定制目标对象实例化的内置方式。

Let’s make a conversion, not Game to DTO, but Game to Game this time.

让我们进行一次转换,这次不是Game到DTO,而是GameGame

So, in principle, we have a persisted Game domain, and we fetch it from its repository.

所以,原则上,我们有一个持久化的Game域,我们从其存储库中获取它。

After that, we update the Game instance by merging another Game object into it:

之后,我们通过将另一个Game对象合并到其中来更新Game实例。

@Test
public void whenUsingProvider_thenMergesGameInstances() {
    // setup
    TypeMap<Game, Game> propertyMapper = this.mapper.createTypeMap(Game.class, Game.class);
    // a provider to fetch a Game instance from a repository
    Provider<Game> gameProvider = p -> this.gameRepository.findById(1L);
    propertyMapper.setProvider(gameProvider);
    
    // when a state for update is given
    Game update = new Game(1L, "Game Updated!");
    update.setCreator(new Player(1L, "John"));
    Game updatedGame = this.mapper.map(update, Game.class);
    
    // then it merges the updates over on the provided instance
    assertEquals(1L, updatedGame.getId().longValue());
    assertEquals("Game Updated!", updatedGame.getName());
    assertEquals("John", updatedGame.getCreator().getName());
}

4.5. Conditional Mapping

4.5.条件性映射

ModelMapper also supports conditional mapping. One of its built-in conditional methods we can use is Conditions.isNull().

ModelMapper也支持条件映射。其内置的条件方法之一,我们可以使用Conditions.isNull()

Let’s skip the id field in case it’s null in our source Game object:

让我们跳过id字段,以防它在我们的源Game对象中是null

@Test
public void whenUsingConditionalIsNull_thenMergesGameInstancesWithoutOverridingId() {
    // setup
    TypeMap<Game, Game> propertyMapper = this.mapper.createTypeMap(Game.class, Game.class);
    propertyMapper.setProvider(p -> this.gameRepository.findById(2L));
    propertyMapper.addMappings(mapper -> mapper.when(Conditions.isNull()).skip(Game::getId, Game::setId));
    
    // when game has no id
    Game update = new Game(null, "Not Persisted Game!");
    Game updatedGame = this.mapper.map(update, Game.class);
    
    // then destination game id is not overwritten
    assertEquals(2L, updatedGame.getId().longValue());
    assertEquals("Not Persisted Game!", updatedGame.getName());
}

Notice that by using the isNull conditional combined with the skip method, we guarded our destination id against overwriting with a null value.

注意,通过使用isNull条件和skip方法,我们保护了我们的目标id不被null值覆盖。

Moreover, we can also define custom Conditions.

此外,我们还可以定义自定义Conditions.

Let’s define a condition to check if the Game‘s timestamp field has a value:

让我们定义一个条件来检查Gametimestamp字段是否有一个值。

Condition<Long, Long> hasTimestamp = ctx -> ctx.getSource() != null && ctx.getSource() > 0;

Next, we use it in our property mapper with when method:

接下来,我们用when方法在我们的属性映射器中使用它。

TypeMap<Game, GameDTO> propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
Condition<Long, Long> hasTimestamp = ctx -> ctx.getSource() != null && ctx.getSource() > 0;
propertyMapper.addMappings(
  mapper -> mapper.when(hasTimestamp).map(Game::getTimestamp, GameDTO::setCreationTime)
);

Finally, ModelMapper only updates the GameDTO‘s creationTime field if the timestamp has a value greater than zero:

最后,ModelMapper只更新GameDTOcreationTime字段,如果timestamp的值大于零。

@Test
public void whenUsingCustomConditional_thenConvertsDTOSkipsZeroTimestamp() {
    // setup
    TypeMap<Game, GameDTO> propertyMapper = this.mapper.createTypeMap(Game.class, GameDTO.class);
    Condition<Long, Long> hasTimestamp = ctx -> ctx.getSource() != null && ctx.getSource() > 0;
    propertyMapper.addMappings(
      mapper -> mapper.when(hasTimestamp).map(Game::getTimestamp, GameDTO::setCreationTime)
    );
    
    // when game has zero timestamp
    Game game = new Game(1L, "Game 1");
    game.setTimestamp(0L);
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then timestamp field is not mapped
    assertEquals(game.getId(), gameDTO.getId());
    assertEquals(game.getName(), gameDTO.getName());
    assertNotEquals(0L ,gameDTO.getCreationTime());
    
    // when game has timestamp greater than zero
    game.setTimestamp(Instant.now().getEpochSecond());
    gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then timestamp field is mapped
    assertEquals(game.getId(), gameDTO.getId());
    assertEquals(game.getName(), gameDTO.getName());
    assertEquals(game.getTimestamp() ,gameDTO.getCreationTime());
}

5. Alternative Ways of Mapping

5.绘图的其他方法

Property mapping is a good approach in most cases because it allows us to make explicit definitions and clearly see how the mapping flows.

在大多数情况下,属性映射是一个很好的方法,因为它允许我们做出明确的定义,并清楚地看到映射的流程。

However, for some objects, especially when they have different property hierarchies, we can use the LOOSE matching strategy instead of TypeMap.

然而,对于某些对象,特别是当它们有不同的属性层次时,我们可以使用LOOSE匹配策略而不是TypeMap

5.1. Matching Strategy LOOSE

5.1.匹配策略LOOSE

To demonstrate the benefits of loose matching, let’s add two more properties into GameDTO:

为了证明松散匹配的好处,让我们在GameDTO中再添加两个属性。

public class GameDTO {
    //...
    
    private GameMode mode;
    private int maxPlayers;
    
    // constructors, getters and setters
}

Notice that mode and maxPlayers correspond to the properties of GameSettings, which is an inner object in our Game source class:

注意,modemaxPlayers对应于GameSettings的属性,后者是我们Game源类中的一个内部对象。

public class GameSettings {

    private GameMode mode;
    private int maxPlayers;

    // constructors, getters and setters
}

This way, we can perform a two-way mapping, both from Game to GameDTO and the other way around without defining any TypeMap:

这样,我们可以进行双向映射,既可以从GameGameDTO,也可以反过来,而无需定义任何TypeMap

@Test
public void whenUsingLooseMappingStrategy_thenConvertsToDomainAndDTO() {
    // setup
    this.mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE);
    
    // when dto has flat fields for GameSetting
    GameDTO gameDTO = new GameDTO();
    gameDTO.setMode(GameMode.TURBO);
    gameDTO.setMaxPlayers(8);
    Game game = this.mapper.map(gameDTO, Game.class);
    
    // then it converts to inner objects without property mapper
    assertEquals(gameDTO.getMode(), game.getSettings().getMode());
    assertEquals(gameDTO.getMaxPlayers(), game.getSettings().getMaxPlayers());
    
    // when the GameSetting's field names match
    game = new Game();
    game.setSettings(new GameSettings(GameMode.NORMAL, 6));
    gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then it flattens the fields on dto
    assertEquals(game.getSettings().getMode(), gameDTO.getMode());
    assertEquals(game.getSettings().getMaxPlayers(), gameDTO.getMaxPlayers());
}

5.2. Auto-Skip Null Properties

5.2.自动跳转的空属性

Additionally, ModelMapper has some global configurations that can be helpful. One of them is the setSkipNullEnabled setting.

此外,ModelMapper 有一些全局配置,可能会有帮助。其中之一是setSkipNullEnabled设置。

So, we can automatically skip the source properties if they’re null without writing any conditional mapping:

所以,如果源属性是null,我们可以自动跳过它们,而不需要写任何条件映射

@Test
public void whenConfigurationSkipNullEnabled_thenConvertsToDTO() {
    // setup
    this.mapper.getConfiguration().setSkipNullEnabled(true);
    TypeMap<Game, Game> propertyMap = this.mapper.createTypeMap(Game.class, Game.class);
    propertyMap.setProvider(p -> this.gameRepository.findById(2L));
    
    // when game has no id
    Game update = new Game(null, "Not Persisted Game!");
    Game updatedGame = this.mapper.map(update, Game.class);
    
    // then destination game id is not overwritten
    assertEquals(2L, updatedGame.getId().longValue());
    assertEquals("Not Persisted Game!", updatedGame.getName());
}

5.3. Circular Referenced Objects

5.3.循环引用的对象

Sometimes, we need to deal with objects that have references to themselves.

有时,我们需要处理那些对自己有引用的对象。

Generally, this results in a circular dependency and causes the famous StackOverflowError:

一般来说,这会导致循环依赖,并引起著名的StackOverflowError

org.modelmapper.MappingException: ModelMapper mapping errors:

1) Error mapping com.bealdung.domain.Game to com.bealdung.dto.GameDTO

1 error
	...
Caused by: java.lang.StackOverflowError
	...

So, another configuration, setPreferNestedProperties, will help us in this case:

因此,另一个配置,setPreferNestedProperties,将在这种情况下帮助我们。

@Test
public void whenConfigurationPreferNestedPropertiesDisabled_thenConvertsCircularReferencedToDTO() {
    // setup
    this.mapper.getConfiguration().setPreferNestedProperties(false);
    
    // when game has circular reference: Game -> Player -> Game
    Game game = new Game(1L, "Game 1");
    Player player = new Player(1L, "John");
    player.setCurrentGame(game);
    game.setCreator(player);
    GameDTO gameDTO = this.mapper.map(game, GameDTO.class);
    
    // then it resolves without any exception
    assertEquals(game.getId(), gameDTO.getId());
    assertEquals(game.getName(), gameDTO.getName());
}

Therefore, when we pass false into setPreferNestedProperties, the mapping works without any exception.

因此,当我们在false中传递setPreferNestedProperties时,映射的工作没有任何例外。

6. Conclusion

6.结语

In this article, we explained how to customize class-to-class mappings with property mappers in ModelMapper.

在这篇文章中,我们解释了如何在ModelMapper中用属性映射器定制类与类之间的映射。

We also saw some detailed examples of alternative configurations.

我们还看到了一些替代配置的详细例子。

As always, all the source code for the examples is available over on GitHub.

像往常一样,这些例子的所有源代码都可以在GitHub上找到