ModelMapper:往返

图片

出于众所周知的原因,后端无法按原样从存储库返回数据。 最著名的是-基本依赖项不是以前端可以理解的形式从基础中获取的。 在这里,您可能会增加解析枚举的难度(如果枚举字段包含其他参数),以及因自动类型转换(或无法自动转换它们)而引起的许多其他困难。 这意味着需要使用数据传输对象-DTO,这对于背面和正面都是可以理解的。
将实体转换为DTO的方式很多。 您可以使用该库,您可以(如果项目很小的话)将以下内容放在一起:

@Component public class ItemMapperImpl implements ItemMapper { private final OrderRepository orderRepository; @Autowired public ItemMapperImpl(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public Item toEntity(ItemDto dto) { return new Item( dto.getId(), obtainOrder(dto.getOrderId()), dto.getArticle(), dto.getName(), dto.getDisplayName(), dto.getWeight(), dto.getCost(), dto.getEstimatedCost(), dto.getQuantity(), dto.getBarcode(), dto.getType() ); } @Override public ItemDto toDto(Item item) { return new ItemDto( item.getId(), obtainOrderId(item), item.getArticle(), item.getName(), item.getDisplayName(), item.getWeight(), item.getCost(), item.getEstimatedCost(), item.getQuantity(), item.getBarcode(), item.getType() ); } private Long obtainOrderId(Item item) { return Objects.nonNull(item.getOrder()) ? item.getOrder().getId() : null; } private Order obtainOrder(Long orderId) { return Objects.nonNull(orderId) ? orderRepository.findById(orderId).orElse(null) : null; } } 

这种自写的映射器具有明显的缺点:

  1. 不缩放。
  2. 当添加/删除最无关紧要的字段时,您将必须编辑映射器。

因此,正确的解决方案是使用映射器库。 我知道modelmapper和mapstruct。 自从我与modelmapper一起工作以来,我将对其进行讨论。但是,如果您,我的读者,对mapstruct有很好的了解,并且可以讲述其应用程序的所有细微差别,请写一篇有关它的文章,我将是第一个向我写它的人(这个库也非常有趣,但还没有时间输入)。

因此,模型映射器。

我想马上说,如果您不清楚什么,可以下载带有工作测试的完成项目,该链接位于文章结尾。

当然,第一步是添加依赖项。 我使用gradle,但是您很容易将依赖项添加到您的maven项目中。

 compile group: 'org.modelmapper', name: 'modelmapper', version: '2.3.2' 

这足以使映射器工作。 接下来,我们需要创建一个bin。

 @Bean public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); mapper.getConfiguration() .setMatchingStrategy(MatchingStrategies.STRICT) .setFieldMatchingEnabled(true) .setSkipNullEnabled(true) .setFieldAccessLevel(PRIVATE); return mapper; } 

通常,仅返回新的ModelMapper就足够了,但是根据我们的需要配置映射器并不是多余的。 我设置了严格的匹配策略,启用了字段映射,跳过了空字段,并设置了对字段的私有访问级别。

接下来,创建以下实体结构。 我们将拥有一个独角兽,该独角兽将具有一定数量的从属机器人,每个机器人将具有一定数量的纸杯蛋糕。

实体
抽象父:

 @MappedSuperclass @Setter @EqualsAndHashCode @NoArgsConstructor @AllArgsConstructor public abstract class AbstractEntity implements Serializable { Long id; LocalDateTime created; LocalDateTime updated; @Id @GeneratedValue public Long getId() { return id; } @Column(name = "created", updatable = false) public LocalDateTime getCreated() { return created; } @Column(name = "updated", insertable = false) public LocalDateTime getUpdated() { return updated; } @PrePersist public void toCreate() { setCreated(LocalDateTime.now()); } @PreUpdate public void toUpdate() { setUpdated(LocalDateTime.now()); } } 

独角兽

 @Entity @Table(name = "unicorns") @EqualsAndHashCode(callSuper = false) @Setter @AllArgsConstructor @NoArgsConstructor public class Unicorn extends AbstractEntity { private String name; private List<Droid> droids; private Color color; public Unicorn(String name, Color color) { this.name = name; this.color = color; } @Column(name = "name") public String getName() { return name; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "unicorn") public List<Droid> getDroids() { return droids; } @Column(name = "color") public Color getColor() { return color; } } 

机器人:

 @Setter @EqualsAndHashCode(callSuper = false) @Entity @Table(name = "droids") @AllArgsConstructor @NoArgsConstructor public class Droid extends AbstractEntity { private String name; private Unicorn unicorn; private List<Cupcake> cupcakes; private Boolean alive; public Droid(String name, Unicorn unicorn, Boolean alive) { this.name = name; this.unicorn = unicorn; this.alive = alive; } public Droid(String name, Boolean alive) { this.name = name; this.alive = alive; } @Column(name = "name") public String getName() { return name; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "unicorn_id") public Unicorn getUnicorn() { return unicorn; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "droid") public List<Cupcake> getCupcakes() { return cupcakes; } @Column(name = "alive") public Boolean getAlive() { return alive; } } 

蛋糕:

 @Entity @Table(name = "cupcakes") @Setter @EqualsAndHashCode(callSuper = false) @AllArgsConstructor @NoArgsConstructor public class Cupcake extends AbstractEntity { private Filling filling; private Droid droid; @Column(name = "filling") public Filling getFilling() { return filling; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "droid_id") public Droid getDroid() { return droid; } public Cupcake(Filling filling) { this.filling = filling; } } 


我们将这些实体转换为DTO。 至少有两种方法可以将依赖关系从实体转换为DTO。 一个意味着只保存ID而不是实体,但是从依赖关系中每个实体,如果有必要,我们将另外使用ID。 第二种方法涉及保持DTO依赖性。 因此,在第一种方法中,我们将List机器人转换为List机器人(我们仅将ID存储在新列表中),在第二种方法中,我们将保存在List机器人中。

DTO
抽象父:

 @Data public abstract class AbstractDto implements Serializable { private Long id; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") LocalDateTime created; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") LocalDateTime updated; } 

UnicornDto:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class UnicornDto extends AbstractDto { private String name; private List<DroidDto> droids; private String color; } 

DroidDto:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { private String name; private List<CupcakeDto> cupcakes; private UnicornDto unicorn; private Boolean alive; } 

纸杯蛋糕Dto:

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class CupcakeDto extends AbstractDto { private String filling; private DroidDto droid; } 


为了根据需要调整映射器,我们将需要创建自己的包装器类并重新定义映射集合的逻辑。 为此,我们创建一个组件类UnicornMapper,在那里自动映射我们的映射器并重新定义所需的方法。

包装器类的最简单版本如下所示:

 @Component public class UnicornMapper { @Autowired private ModelMapper mapper; @Override public Unicorn toEntity(UnicornDto dto) { return Objects.isNull(dto) ? null : mapper.map(dto, Unicorn.class); } @Override public UnicornDto toDto(Unicorn entity) { return Objects.isNull(entity) ? null : mapper.map(entity, UnicornDto.class); } } 

现在我们足以将映射器自动连接到某些服务,并使用toDto和toEntity方法进行提取。 映射器会将在对象中找到的实体转换为DTO,即DTO-转换为实体。

 @Service public class UnicornServiceImpl implements UnicornService { private final UnicornRepository repository; private final UnicornMapper mapper; @Autowired public UnicornServiceImpl(UnicornRepository repository, UnicornMapper mapper) { this.repository = repository; this.mapper = mapper; } @Override public UnicornDto save(UnicornDto dto) { return mapper.toDto(repository.save(mapper.toEntity(dto))); } @Override public UnicornDto get(Long id) { return mapper.toDto(repository.getOne(id)); } } 

但是,如果我们尝试以这种方式进行转换,然后调用例如toString,则将得到一个StackOverflowException,这就是为什么:UnicornDto包含DroidDto列表,该列表包含UnicornDto,其中包含DroidDto,依此类推,直到那一刻为止直到堆栈内存用完。 因此,对于逆依赖性,我通常不使用UnicornDto独角兽,而是使用Long unicornId。 通过这种方式,我们与Unicorn保持联系,但切断了循环依赖性。 让我们修复DTO,以便它们存储反向依赖的ID而不是反向DTO。

 @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor public class DroidDto extends AbstractDto { ... //private UnicornDto unicorn; private Long unicornId; ... } 

等等。

但是现在,如果我们调用DroidMapper,我们将获得unicornId == null。 这是因为ModelMapper无法确切确定Long是什么。 只是不打扰他。 而且,我们将必须调整必要的映射器,以教他们如何映射ID中的实体。

我们记得在初始化每个垃圾箱后,您可以手动进行工作。

  @PostConstruct public void setupMapper() { mapper.createTypeMap(Droid.class, DroidDto.class) .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter()); mapper.createTypeMap(DroidDto.class, Droid.class) .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter()); } 

在@PostConstruct中,我们将设置规则以指示映射器不应接触的字段,因为对于它们,我们将自行确定逻辑。 在我们的示例中,这既是DTO中的unicornId的定义,又是本质上的Unicorn的定义(因为映射器也不知道该如何处理Long unicornId)。

TypeMap-这是我们指定映射的所有细微差别并设置转换器的规则。 我们指出,要从Droid转换为DroidDto,我们跳过setUnicornId,而在反向转换中,我们传递setUnicorn。 我们将在UnicornDto的toDtoConverter()转换器和Unicorn的toEntityConverter()中全部进行转换。 我们必须在组件中描述这些转换器。

最简单的后转换器看起来像这样:

  Converter<UnicornDto, Unicorn> toEntityConverter() { return MappingContext::getDestination; } 

我们需要扩展其功能:

  public Converter<UnicornDto, Unicorn> toEntityConverter() { return context -> { UnicornDto source = context.getSource(); Unicorn destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } 

我们对逆转换器执行相同的操作:

  public Converter<Unicorn, UnicornDto> toDtoConverter() { return context -> { Unicorn source = context.getSource(); UnicornDto destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } 

实际上,我们只需在每个后转换器中插入一个附加方法,就可以为缺少的字段编写自己的逻辑。

  public void mapSpecificFields(Droid source, DroidDto destination) { destination.setUnicornId(Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId()); } void mapSpecificFields(DroidDto source, Droid destination) { destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null)); } 

在DTO中进行映射时,我们设置实体ID。 在DTO中进行映射时,我们通过ID从存储库中获取实体。

就是这样。

我展示了开始使用modelmapper所需的最低要求,并且没有特别重构代码。 如果您(读者)对我的文章有所添加,我将很高兴听到建设性的批评。

该项目可以在这里查看:
在GitHub上进行项目。

干净代码的爱好者可能已经看到了将许多代码组件驱动为抽象的机会。 如果您是其中之一,我建议您选择猫咪。

提高抽象水平
首先,我们为包装器类的基本方法定义一个接口。

 public interface Mapper<E extends AbstractEntity, D extends AbstractDto> { E toEntity(D dto); D toDto(E entity); } 

我们从中继承一个抽象类。

 public abstract class AbstractMapper<E extends AbstractEntity, D extends AbstractDto> implements Mapper<E, D> { @Autowired ModelMapper mapper; private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; } @Override public E toEntity(D dto) { return Objects.isNull(dto) ? null : mapper.map(dto, entityClass); } @Override public D toDto(E entity) { return Objects.isNull(entity) ? null : mapper.map(entity, dtoClass); } Converter<E, D> toDtoConverter() { return context -> { E source = context.getSource(); D destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } Converter<D, E> toEntityConverter() { return context -> { D source = context.getSource(); E destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } void mapSpecificFields(E source, D destination) { } void mapSpecificFields(D source, E destination) { } } 

转换器后和用于填充特定字段的方法可以安全地发送到那里。 另外,创建两个类型为Class的对象和一个构造函数以对其进行初始化:

  private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; } 

现在,DroidMapper中的代码量减少为以下内容:

 @Component public class DroidMapper extends AbstractMapper<Droid, DroidDto> { private final ModelMapper mapper; private final UnicornRepository unicornRepository; @Autowired public DroidMapper(ModelMapper mapper, UnicornRepository unicornRepository) { super(Droid.class, DroidDto.class); this.mapper = mapper; this.unicornRepository = unicornRepository; } @PostConstruct public void setupMapper() { mapper.createTypeMap(Droid.class, DroidDto.class) .addMappings(m -> m.skip(DroidDto::setUnicornId)).setPostConverter(toDtoConverter()); mapper.createTypeMap(DroidDto.class, Droid.class) .addMappings(m -> m.skip(Droid::setUnicorn)).setPostConverter(toEntityConverter()); } @Override public void mapSpecificFields(Droid source, DroidDto destination) { destination.setUnicornId(getId(source)); } private Long getId(Droid source) { return Objects.isNull(source) || Objects.isNull(source.getId()) ? null : source.getUnicorn().getId(); } @Override void mapSpecificFields(DroidDto source, Droid destination) { destination.setUnicorn(unicornRepository.findById(source.getUnicornId()).orElse(null)); } } 

没有特定字段的映射器通常看起来很简单:

 @Component public class UnicornMapper extends AbstractMapper<Unicorn, UnicornDto> { @Autowired public UnicornMapper() { super(Unicorn.class, UnicornDto.class); } } 

Source: https://habr.com/ru/post/zh-CN438808/


All Articles