ModelMapper: Hin- und Rückfahrt

Bild

Aus bekannten Gründen kann das Backend keine Daten aus dem Repository zurückgeben. Die bekanntesten - die wesentlichen Abhängigkeiten werden nicht von der Basis in der Form genommen, in der die Front sie verstehen kann. Hier können Sie Schwierigkeiten beim Parsen von Enum hinzufügen (wenn die Enum-Felder zusätzliche Parameter enthalten) und viele andere Schwierigkeiten, die sich aus dem automatischen Typumwandeln ergeben (oder der Unfähigkeit, sie automatisch umzuwandeln). Dies impliziert die Notwendigkeit, ein Datenübertragungsobjekt (DTO) zu verwenden, das sowohl für die Rückseite als auch für die Vorderseite verständlich ist.
Das Konvertieren einer Entität in ein DTO kann auf viele verschiedene Arten erfolgen. Sie können die Bibliothek verwenden und (wenn das Projekt klein ist) Folgendes zusammenstellen:

@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; } } 

Solche selbstgeschriebenen Mapper haben offensichtliche Nachteile:

  1. Nicht skalieren.
  2. Wenn Sie selbst das unbedeutendste Feld hinzufügen / entfernen, müssen Sie den Mapper bearbeiten.

Daher ist die richtige Lösung die Verwendung einer Mapper-Bibliothek. Ich kenne Modelmapper und Mapstruct. Da ich mit modelmapper gearbeitet habe, werde ich darüber sprechen, aber wenn Sie, mein Leser, mit mapstruct gut vertraut sind und über alle Feinheiten seiner Anwendung Bescheid wissen, schreiben Sie bitte einen Artikel darüber, und ich werde der erste sein, der es mir schreibt (diese Bibliothek ist auch sehr interessant, aber es ist noch keine Zeit, es einzugeben).

Also Modelmapper.

Ich möchte sofort sagen, dass Sie, wenn Ihnen etwas nicht klar ist, das fertige Projekt mit einem Arbeitstest herunterladen können, einem Link am Ende des Artikels.

Der erste Schritt ist natürlich das Hinzufügen einer Abhängigkeit. Ich verwende gradle, aber es ist einfach für Sie, Ihrem Maven-Projekt eine Abhängigkeit hinzuzufügen.

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

Dies reicht aus, damit der Mapper funktioniert. Als nächstes müssen wir einen Bin erstellen.

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

Normalerweise reicht es aus, einfach einen neuen ModelMapper zurückzugeben, aber es ist nicht überflüssig, den Mapper für unsere Anforderungen zu konfigurieren. Ich habe eine strikte Übereinstimmungsstrategie festgelegt, die Feldzuordnung aktiviert, Nullfelder übersprungen und eine private Zugriffsebene für die Felder festgelegt.

Erstellen Sie als Nächstes die folgende Entitätsstruktur. Wir werden ein Einhorn haben, dem eine bestimmte Anzahl von Droiden untergeordnet ist, und jeder Droide wird eine bestimmte Anzahl von Cupcakes haben.

Entitäten
Abstraktes Elternteil:

 @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()); } } 

Einhorn:

 @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; } } 

Droide:

 @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; } } 

Cupcake:

 @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; } } 


Wir werden diese Entitäten in DTO konvertieren. Es gibt mindestens zwei Ansätze zum Konvertieren von Abhängigkeiten von einer Entität in ein DTO. Man impliziert, dass nur die ID anstelle der Entität gespeichert wird, aber dann wird jede Entität aus der Abhängigkeit, falls erforderlich, zusätzlich die ID abgerufen. Der zweite Ansatz besteht darin, das DTO abhängig zu halten. Im ersten Ansatz würden wir Listendroiden in Listendroiden konvertieren (wir speichern nur IDs in der neuen Liste), und im zweiten Ansatz werden wir in Listendroiden speichern.

DTO
Abstraktes Elternteil:

 @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; } 

CupcakeDto:

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


Um den Mapper an unsere Anforderungen anzupassen, müssen wir unsere eigene Wrapper-Klasse erstellen und die Logik für die Zuordnung von Sammlungen neu definieren. Dazu erstellen wir eine Komponentenklasse UnicornMapper, ordnen unseren Mapper dort automatisch zu und definieren die benötigten Methoden neu.

Die einfachste Version der Wrapper-Klasse sieht folgendermaßen aus:

 @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); } } 

Jetzt reicht es aus, unseren Mapper automatisch in einen Dienst zu verdrahten und mit den Methoden toDto und toEntity zu ziehen. Der Mapper wandelt die im Objekt gefundenen Entitäten in DTO, DTO - in Entitäten um.

 @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)); } } 

Wenn wir jedoch versuchen, etwas auf diese Weise zu konvertieren und dann beispielsweise toString aufrufen, erhalten wir eine StackOverflowException. Hier ist der Grund: UnicornDto enthält die DroidDto-Liste, die UnicornDto enthält, die DroidDto enthält, und so weiter bis zu diesem Moment bis der Stapelspeicher aufgebraucht ist. Daher verwende ich für inverse Abhängigkeiten normalerweise nicht UnicornDto Unicorn, sondern Long UnicornId. Auf diese Weise bleiben wir mit Unicorn in Kontakt, schneiden aber die zyklische Abhängigkeit ab. Korrigieren wir unsere DTOs so, dass sie anstelle von umgekehrten DTOs die IDs ihrer Abhängigkeiten speichern.

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

usw.

Aber jetzt, wenn wir DroidMapper aufrufen, erhalten wir unicornId == null. Dies liegt daran, dass ModelMapper nicht genau bestimmen kann, was Long ist. Und stört ihn einfach nicht. Und wir müssen die erforderlichen Mapper optimieren, um ihnen das Zuordnen von Entitäten in IDs beizubringen.

Wir erinnern uns, dass Sie mit jedem Bin nach seiner Initialisierung manuell arbeiten können.

  @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()); } 

In @PostConstruct legen wir die Regeln fest, in denen wir angeben, welche Felder der Mapper nicht berühren soll, da wir für sie die Logik selbst bestimmen. In unserem Fall ist dies sowohl die Definition von unicornId im DTO als auch die Definition von Unicorn im Wesentlichen (da der Mapper auch nicht weiß, was er mit Long unicornId tun soll).

TypeMap - Dies ist die Regel, in der wir alle Nuancen der Zuordnung angeben und auch den Konverter festlegen. Wir haben darauf hingewiesen, dass wir zum Konvertieren von Droid nach DroidDto setUnicornId überspringen und bei der umgekehrten Konvertierung setUnicorn übergeben. Wir werden alle in den toDtoConverter () -Konverter für UnicornDto und in toEntityConverter () für Unicorn konvertieren. Wir müssen diese Konverter in unserer Komponente beschreiben.

Der einfachste Post-Converter sieht folgendermaßen aus:

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

Wir müssen seine Funktionalität erweitern:

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

Wir machen dasselbe mit dem inversen Konverter:

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

Tatsächlich fügen wir einfach eine zusätzliche Methode in jeden Nachkonverter ein, in der wir unsere eigene Logik für die fehlenden Felder schreiben.

  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)); } 

Bei der Zuordnung in DTO legen wir die Entitäts-ID fest. Bei der Zuordnung in DTO erhalten wir die Entität anhand der ID aus dem Repository.

Und alle.

Ich habe das notwendige Minimum gezeigt, um mit Modelmapper zu arbeiten, und den Code nicht besonders überarbeitet. Wenn Sie, Leser, etwas zu meinem Artikel hinzufügen möchten, würde ich mich über konstruktive Kritik freuen.

Das Projekt kann hier angesehen werden:
Projekt auf GitHub.

Fans von sauberem Code sahen wahrscheinlich bereits die Möglichkeit, viele der Codekomponenten in eine Abstraktion zu treiben. Wenn Sie einer von ihnen sind, schlage ich unter Katze vor.

Erhöhen Sie die Abstraktionsebene
Zunächst definieren wir eine Schnittstelle für die grundlegenden Methoden der Wrapper-Klasse.

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

Wir erben davon eine abstrakte Klasse.

 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) { } } 

Postkonverter und Methoden zum Ausfüllen bestimmter Felder können sicher dorthin gesendet werden. Erstellen Sie außerdem zwei Objekte vom Typ Class und einen Konstruktor, um sie zu initialisieren:

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

Jetzt wird die Codemenge in DroidMapper auf Folgendes reduziert:

 @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)); } } 

Ein Mapper ohne bestimmte Felder sieht im Allgemeinen einfach aus:

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

Source: https://habr.com/ru/post/de438808/


All Articles