ModelMapper:将属性从根传输到列表的每个元素

ModelMapper: transferring attribute from root to each elements of a list

型号

我们使用 org.modelmapper 进行 bean 映射。这是数据模型(为简洁起见,使用 public):

class Root {
  public String foo;
  public List<Element> elements;
}
class Element {
  public String foo; // <- this is the field that should be replicated from RootDTO
  public String bar;
}


class RootDTO {
  public String foo;
  public List<ElementDTO> elements;
}
class ElementDTO {
  public String bar; // notice how there is no `foo` attribute in the DTO
}

预期映射(示例)

输入:

RootDTO
|-- foo = "foo"
|-- elements
    |--ElementDTO(bar="bar")
    |--ElementDTO(bar="something")
    |--ElementDTO(bar="else")

输出:

Root
|-- foo = "foo"
|-- elements
    |--Element(foo="foo", bar="bar")
    |--Element(foo="foo", bar="something")
    |--Element(foo="foo", bar="else")

问题

如您所见,如果 Element.foo 字段不应该从 RootDTO.foo.[=32= 获取值,ModelMapper 会很容易地处理这种情况]

如何配置 ModelMapper 来实现我的目标?

我想要解决方案:

  1. 如果要在模型中引入新的匹配属性,则不需要开发人员更改映射代码。
  2. 通过对 ModelMapper 的单个方法调用来完成。

当前配置

myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings();

使用该配置时的结果

Root mappedRoot = myModelMapper.map(rootDto, Root.class);

assertEquals(rootDto.foo, mappedRoot.foo);
assertEquals(rootDto.elements.size(), mappedRoot.elements.size());
// let's assume we have 1 Element
assertEquals(rootDto.elements.get(0).bar, mappedRoot.elements.get(0).bar);

assertEquals(rootDto.foo, mappedRoot.elements.get(0).foo); // FAILS! But this is what I want
// indeed, the value of `mappedRoot.elements.get(0).foo` is `null` because it was not mapped

已探索的途径

想法 1

在我看来,如果我可以设置一个 order 我可以简单地这样配置它:

Converter<RootDTO, Root> replicateFooValue = new Converter<>() {
  @Override
  public Root convert(MappingContext<RootDTO, Root> context) {
    final String valueToReplicate = context.getSource().foo;
    for (Element elem : context.getDestination().elements) {
      elem.foo = valueToReplicate;
    }
    return context.getDestination();
  }
};

myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings()
    .thenUseConverter(replicateFooValue);

...但我认为这是不可能的。

想法 2

如果我可以在 Converter<> 本身中使用 ModelMapper 的映射功能,那么我可以在尝试简单地设置我想要的值之前使用 implicitMappings(),但是再一次:我不认为这是可能的。

想法 3

确保它有效的一种方法是:

Converter<RootDTO, Root> myCustomConverter = new Converter<>() {
  @Override
  public Root convert(MappingContext<RootDTO, Root> context) {
    final Root mappedRoot = new Root();
    // map each field individually, by hand
    return mappedRoot;
  }
};

myModelMapper.addConverter(myCustomConverter);

...但是如果我要在我的数据模型中添加新字段,它需要维护。

想法 4

添加一个 Converter<RootDTO, Element>,它只会填充 foo 值。这将导致以下用法:

Root mappedRoot = myModelMapper.map(rootDto, Root.class);
mappedRoot.elements.forEach(e -> myModelMapper.map(rootDto, e));

这并不理想,因为整个预期行为并没有封装在单个调用中:开发人员必须知道(并记住)进行第二次调用以实现所需的映射。

想法 5

创建一个包含 Idea 4 所示逻辑的实用程序 class。

这也是一个坏主意,因为它要求开发人员知道(并记住)为什么他们需要以这种方式进行特定映射,而不是使用 ModelMapper.

原来一种引入一些顺序的方法。这是解决方案:

Converter<RootDTO, Root> finishMapping = new Converter<>() {
  @Override
  public Root convert(MappingContext<RootDTO, Root> context) {
    final String srcFoo = context.getSource();
    var dest = context.getDestination();

    dest.elements.stream().forEach(e -> e.foo = srcFoo);
    return dest;
  }
};

myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings()
    .setPostConverter(finishMapping); // this is executed AFTER the implicit mapping