如何自定义模型映射器

2022-09-01 13:31:28

我想使用模型映射器将实体转换为DTO并返回。大多数情况下它都可以工作,但是我如何自定义它。它有很多选择,很难弄清楚从哪里开始。什么是最佳实践?

我会在下面自己回答,但如果另一个答案更好,我会接受它。


答案 1

首先这里有一些链接

我对mm的印象是它设计得很好。代码很可靠,阅读起来很愉快。但是,文档非常简洁,示例很少。此外,API令人困惑,因为似乎有10种方法可以做任何事情,并且没有迹象表明为什么你会以这种或那种方式做到这一点。

有两种选择:推土机是最受欢迎的,Orika因其易用性而获得了好评。

假设你仍然想使用mm,以下是我所学到的。

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

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

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

与适当的 getter/setter。您可以执行以下操作:

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

“名称”将从 dd 复制到 di。

有很多方法可以自定义mm,但首先您需要了解它的工作原理。

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

每个 TypeMap 都包含一个属性映射,其中包含一个映射列表。因此,在示例中,mm 将自动创建一个 TypeMap<DogData, DogInfo>其中包含一个具有单个映射的 PropertyMap。

我们可以这样写

    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 是否存在,如果没有,请为<S、D>源/目标类型创建 TypeMap
  2. 调用 TypeMap 条件,如果它返回 FALSE,则不执行任何操作并停止
  3. 如有必要,调用 TypeMap 提供程序以构造新的目标对象
  4. 调用 TypeMap PreConverter(如果有)
  5. 执行下列操作之一:
    • 如果 TypeMap 具有自定义转换器,请将其称为
    • 或者,生成一个属性映射(基于配置标志以及添加的任何自定义映射),并使用它(注意:TypeMap还具有可选的自定义Pre/PostPropertyConverters,我认为这些转换器将在每次映射之前和之后运行。
  6. 调用 TypeMap PostConverter(如果有)

警告:这个流程图是有文档记录的,但我不得不猜测很多,所以它可能并不完全正确!

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

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

下面是自定义类型映射转换器的示例

    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代码。例如,setter的参数不相关,它只是一个占位符。你可以在这里做很多事情,比如

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

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

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

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

在此示例中,我必须编写一个转换器来将整数映射到布尔值。在大多数情况下,这不是必需的,因为mm会自动将整数转换为字符串等。

我被告知你也可以使用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 单例。


答案 2

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

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

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

和 Dto ->

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

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

private ModelMapper modelMapper;

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

就是这样。现在,您可以轻松使用ModelMapper.map(源,目标)。它将自动映射

modelMapper.map(student, studentDto);