如何自定义ModelMapper

How to customize ModelMapper

我想使用 ModelMapper 将实体转换为 DTO 并返回。大多数情况下它有效,但我如何自定义它。它有太多的选择,以至于很难弄清楚从哪里开始。什么是最佳实践?

下面我会自己回答,如果有更好的回答我会采纳

首先是一些链接

我对mm的印象是做工非常好。代码很扎实,读起来很愉快。但是,文档非常简洁,示例很少。 api 也令人困惑,因为似乎有 10 种方法可以做任何事情,但没有说明为什么你会这样做。

有两种选择:Dozer is the most popular, and Orika 易用性得到好评。

假设你仍然想使用 mm,这是我了解到的。

主要 class、ModelMapper 应该是您应用中的单例。对我来说,这意味着 @Bean 使用 Spring。对于简单的情况,它开箱即用。例如,假设您有两个 classes:

class DogData
{
    private String name;
    private int mass;
}

class DogInfo
{
    private String name;
    private boolean large;
}

与适当的getters/setters。你可以这样做:

    ModelMapper mm = new ModelMapper();
    DogData dd = new DogData();
    dd.setName("fido");
    dd.setMass(70);
    DogInfo di = mm.map(dd, DogInfo.class);

并且 "name" 将从 dd 复制到 di。

自定义mm的方法有很多种,但首先你需要了解它是如何工作的。

mm 对象包含每个有序类型对的 TypeMap,例如 将是两个 TypeMap。

每个 TypeMap 包含一个带有映射列表的 属性Map。因此,在示例中,mm 将自动创建一个 TypeMap,其中包含具有单个映射的 属性Map。

我们可以这样写

    TypeMap<DogData, DogInfo> tm = mm.getTypeMap(DogData.class, DogInfo.class);
    List<Mapping> list = tm.getMappings();
    for (Mapping m : list)
    {
        System.out.println(m);
    }

它会输出

PropertyMapping[DogData.name -> DogInfo.name]

当您调用 mm.map() 时,它就是这样做的,

  1. 查看TypeMap是否存在,如果不存在则为 source/destination 类型
  2. 调用 TypeMap Condition,如果它 returns FALSE,什么都不做并停止
  3. 必要时调用 TypeMap Provider 构造新的目标对象
  4. 调用 TypeMap PreConverter 如果它有一个
  5. 执行以下操作之一:
    • 如果 TypeMap 有一个自定义转换器,调用它
    • 或者,生成一个属性映射(基于配置标志加上任何自定义映射 添加),并使用它 (注意:TypeMap 也有可选的自定义 Pre/PostPropertyConverters 我认为 会 运行 在 之前和之后每个映射 .)
  6. 调用 TypeMap PostConverter 如果它有一个

注意:这个流程图是 sort of documented 但我不得不猜测很多,所以它可能不完全正确!

您可以自定义此过程的每个步骤。但最常见的两个是

  • 步骤 5a。 – 编写自定义 TypeMap 转换器,或
  • 步骤 5b。 – 编写自定义 属性 映射。

这是一个 自定义 TypeMap 转换器的示例:

    Converter<DogData, DogInfo> myConverter = new Converter<DogData, DogInfo>()
    {
        public DogInfo convert(MappingContext<DogData, DogInfo> context)
        {
            DogData s = context.getSource();
            DogInfo d = context.getDestination();
            d.setName(s.getName());
            d.setLarge(s.getMass() > 25);
            return d;
        }
    };

    mm.addConverter(myConverter);

注意 转换器是单向。如果要将 DogInfo 自定义为 DogData,则必须另外编写一个。

这是一个自定义属性地图的示例:

    Converter<Integer, Boolean> convertMassToLarge = new Converter<Integer, Boolean>()
    {
        public Boolean convert(MappingContext<Integer, Boolean> context)
        {
            // If the dog weighs more than 25, then it must be large
            return context.getSource() > 25;
        }
    };

    PropertyMap<DogData, DogInfo> mymap = new PropertyMap<DogData, DogInfo>()
    {
        protected void configure()
        {
            // Note: this is not normal code. It is "EDSL" so don't get confused
            map(source.getName()).setName(null);
            using(convertMassToLarge).map(source.getMass()).setLarge(false);
        }
    };

    mm.addMappings(mymap);

pm.configure 函数真的很时髦。这不是实际代码。它是虚拟 EDSL code 以某种方式被解释的。例如 setter 的参数是不相关的,它只是一个占位符。你可以在这里做很多事情,比如

  • when(条件).map(getter).setter
  • when(condition).skip().setter – 安全地忽略字段。
  • using(converter).map(getter).setter – 自定义字段转换器
  • with(provider).map(getter).setter – 自定义字段构造函数

注意自定义映射已添加到默认映射,因此您不是例如,需要指定

            map(source.getName()).setName(null);

在您的自定义 PropertyMap.configure() 中。

在这个例子中,我必须写一个 Converter 来将 Integer 映射到 Boolean。在大多数情况下,这不是必需的,因为 mm 会自动将 Integer 转换为 String 等

我听说您还可以创建映射 using Java 8 lambda 表达式。我试过了,但我想不通。

最终建议和最佳实践

默认情况下 mm 使用 MatchingStrategies.STANDARD 这是危险的。它很容易选择错误的映射并导致奇怪的、难以发现的错误。如果明年其他人向数据库添加一个新列怎么办?所以不要这样做。确保使用严格模式:

    mm.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

始终编写单元测试并确保所有映射都经过验证。

    DogInfo di = mm.map(dd, DogInfo.class);
    mm.validate();   // make sure nothing in the destination is accidentally skipped

使用 mm.addMappings() 修复所有验证失败,如上所示。

将所有映射放在一个中心位置,在该位置创建 mm 单例。

过去 6 个月以来我一直在使用它,我将解释我的一些想法:

首先推荐作为唯一实例使用(单例,springbean,...),说明书上有说明,我想大家都同意。

ModelMapper 是一个很棒的映射库并且非常灵活。由于它的灵活性,有许多方法可以获得相同的结果,这就是为什么它应该在最佳实践手册中说明何时使用一种或其他方法来做同样的事情。

ModelMapper 开始有点困难,它的学习曲线非常紧凑,有时不容易理解做某事的最佳方法或如何做其他事情。因此,首先需要准确阅读和理解手册。

您可以使用以下设置根据需要配置映射:

Access level
Field matching
Naming convention
Name transformer
Name tokenizer 
Matching strategy

默认配置是最好的 (http://modelmapper.org/user-manual/configuration/),但如果您想对其进行自定义,也可以做到。

只有一件事与匹配策略配置有关,我认为这是最重要的配置,需要小心。我会使用 StrictStandard 但绝不会使用 Loose,为什么?

  • 由于 Loose 是最灵活、最智能的映射器,它可以映射一些您意想不到的属性。所以,明确地说,要小心它。我认为最好创建自己的 PropertyMap 并在需要时使用 Converters 而不是将其配置为 Loose。

否则,重要的是 validate 所有 属性 匹配,你验证它是否有效,并且使用 ModelMapper 更需要它,因为智能映射是通过反射完成的,所以你不会有编译器帮助,它将继续编译,但映射将在没有意识到的情况下失败。这是我最不喜欢的事情之一,但它需要避免样板和手动映射。

最后,如果你确定在你的项目中使用ModelMapper,你应该按照它建议的方式使用它,不要将它与手动映射混合使用(例如),如果你不知道就使用ModelMapper如何做某事是可能的(调查,...)。有时很难用模型映射器(我也不喜欢它)像手工那样做,但这是你应该付出的代价,以避免在其他 POJO 中进行样板映射。

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class EntityDtoConversionUtil {

    @Autowired
    private ModelMapper modelMapper;

    public Object convert(Object object,Class<?> type) {

        Object MapperObject=modelMapper.map(object, type);

        return MapperObject;

    }


}

我在使用 ModelMapper 进行映射时遇到问题。不仅属性而且我的源和目标类型都不同。我这样做解决了这个问题 ->

如果源和目标类型不同。例如,

@Entity
class Student {
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "laptop_id")
    private Laptop laptop;
}

和 Dto ->

class StudentDto {
    private Long id;
    private LaptopDto laptopDto;
}

此处,源和目标类型不同。因此,如果您的 MatchingStrategies 是严格的,您将无法在这两种不同类型之间进行映射。 现在要解决这个问题,只需将下面的代码放在控制器的构造函数中 class 或任何要使用 ModelMapper->

的 class
private ModelMapper modelMapper;

public StudentController(ModelMapper modelMapper) {
    this.modelMapper = modelMapper;
    this.modelMapper.typeMap(Student.class, StudentDto.class).addMapping(Student::getLaptop, StudentDto::setLaptopDto);
}
        

就是这样。现在您可以轻松使用 ModelMapper.map(source, destination) 了。它会自动映射

modelMapper.map(student, studentDto);