Hin und Her mit dem ModelMapper

„The function of a good software is to make the complex appear to be simple.“

Grady Booch

Häufig müssen Daten aus einer Darstellung in eine andere transformiert werden, weil z.B. eine Kopplung von Datenbank Entitäten an einen REST Controller unerwünscht ist. Im Beitrag Hin und Her mit MapStruct wurde der entsprechende Framework vorgestellt. Neben MapStruct und dem unschönen Selbermachen, gibt es aber noch weitere Alternativen.

Eine dieser Alternativen ist das ModelMapper Framework. Auch mit diesem Framework können Instanzen verschiedener Klassen aufeinander abgebildet werden und dabei in gewissen Rahmen manipuliert werden.

Um mit dem ModelMapper zu arbeiten, muss zuerst die entsprechende Dependency ins eigene Projekt aufgenommen werden.

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>2.3.0</version>
</dependency>

Danach kann mit dem ModelMapper auch schon direkt gearbeitet werden. Zuerst wird eine Instanz der Klasse ModelMapper erzeugt und dann ihre map Methode aufgerufen. Parameter der Methode sind hier die zu mappende Instanz und der Ergebnistyp.

ModelMapper modelMapper = new ModelMapper();
Einrichtung entity = repository.findById(1L);
EinrichtungDto dto = modelMapper.map(entity, EinrichtungDto.class);

Ähnlich wie MapStruct kann auch der ModelMapper beliebige Instanzen und ihre Attribute ohne zusätzliche Konfigurationen mappen. Erst wenn die eingebauten Strategien nicht mehr ausreichen, muss der ModelMapper konfiguriert werden.

Eine für alle Mapper typisches Feature ist das Ausblenden gewisser Attribute beim Mappen. So sollen beispielsweise die Zeitstempel der Änderungshistorie aus den Entities in die DTOs kopiert werden, aber der umgekehrte Weg soll verhindert werden.

TypeMap<Einrichtung, EinrichtungDto> typeMap = mapper.typeMap(Einrichtung.class, EinrichtungDto.class);
typeMap.addMappings(mapping -> mapping.skip(EinrichtungDto::setCreatedBy));
typeMap.addMappings(mapping -> mapping.skip(EinrichtungDto::setCreatedDate));
typeMap.addMappings(mapping -> mapping.skip(EinrichtungDto::setLastModifiedBy));
typeMap.addMappings(mapping -> mapping.skip(EinrichtungDto::setLastModifiedDate));

Der ModelMapper verwendet für diese Aufgabe eine TypeMap, in der alle Besonderheiten des Mappings eingetragen werden. In diesem Fall werden die vier Attribute createdby, createdDate, lastModifiedBy und lastModifiedDate ignoriert, weil ihre Setter übersprungen werden.

Im Unterschied zu MapStruct verwendet der ModelMapper keine Annotationen sondern Java Code. Das bedeutet mehr Schreibarbeit, aber auch mehr Sicherheit. MapStruct Warnhinweise über fehlende Mappings gehen manchmal unter und der generierte Source Code der MapStruct Mapper entspricht nicht immer dem, was der Entwickler sich vorgestellt hat.

Ein weiteres notwendiges Feature für Mapper ist das kopieren neuer Werte aus einem DTO in eine existierende Entity. Dafür sollen üblicherweise null Werte im DTO nicht dazu führen, dass bestehende Werte in der Entity überschrieben werden. Mit Hilfe der TypeMap und der Methode when können solche Bedingungen geprüft werden.

typeMap.addMappings(mapping -> mapping.when(Conditions.isNull()).skip(Einrichtung::set));

Danach kann eine Einrichtung Instanz mit dem Inhalt einer EinrichtungDto Instanz befüllt werden.

Einrichtung entity = ...
EinrichtungDto dto = ...
modelMapper.map(dto, entity);

Um die Bedingung nicht an jedem einzelnen Mapping zu überprüfen, kann dies zentral in der ModelMapper Instanz konfiguriert werden. Die folgende Zeile erlaubt ein Mapping nur auf Properties die nicht null sind.

mapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());

Der ModelMapper bietet noch eine Reihe weiterer interessanter Möglichkeiten um das Mappen zwischen verschiedenen Java Klassen zu ermöglichen und ist damit eine gute Alternative zu MapStruct.