Deep Mapping with ModelMapper

Using ModelMapper for Deep Mapping in a Spring Boot Application

In previous posts, I’ve shown how to write custom converters and how to skip fields while mapping using ModelMapper.  For those who aren’t familiar with it, ModelMapper is a library for handling mapping between two similar classes.  A common use case is mapping between data transfer objects (dtos) and entity objects in a web application.  In this post, I’m going to demonstrate the deep mapping feature of ModelMapper in a Spring Boot application.  If you want to follow along with the example, use the Spring Initializr to create an empty application.  My example uses Gradle.  Choose Web, JPA, H2 and Thymeleaf dependencies.  Or you can just get the working code from github.

Prerequisites:

  • Java 8 or Greater
  • Favorite IDE
  • Basic understanding of Spring

Gradle Dependency:

implementation('com.github.jmnarloch:modelmapper-spring-boot-starter:1.1.0')

Project Setup

We’re going to be using two entity classes Item and Location.  The Location class represents a location in a warehouse.

@Entity
public class Location {
   @Id
   @GeneratedValue(strategy=GenerationType.IDENTITY)
   private Long id;

   private String warehouseName;
   private Integer rowNumber;
   private String binLabel;

   /*Getters and setters below*/
}

The Item class represents an imaginary inventory item (Disclaimer: this is not a super realistic example…) that has a Location.  Note:  For the sake of making the example work easily, the Location relationship is set to Cascade.ALL.  That’s not something I’d normally do lightly (if at all).

@Entity
public class Item {
   @Id
   @GeneratedValue(strategy=GenerationType.IDENTITY)
   private Long id;

   private String name;
   private String description;
   private Integer quantity;
   @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
   @JoinColumn(name="location_id", nullable=true)
   private Location location;

   /* Getters and setters below */
}

We also need our Repository interfaces.  For this example, they extend JpaRepository and have no custom methods.  Additionally, we need an ItemDto for transferring between our user input form to our entity.  For user input, we’ve flattened out the Location into the Item.

public class ItemDto {
   private Long id;
   private String name;
   private String description;
   private Integer quantity;
   private Long warehouseId;
   private String warehouseName;
   private Integer warehouseRowNumber;
   private String warehouseBinLabel;

   /* Getters and Setters below */
}

The code that’s important for this example is found in the ItemController class.

ItemController class

In the ItemController, we define two custom property maps that do the deep mapping.  We need one for each direction.  This mapping, sets the fields on the Location entity in the Item entity from the specified Warehouse fields on the entry form for the Item.  Using the map() method overrides the default mapping that ModelMapper tries to figure out automatically only for the fields specified.

PropertyMap<ItemDto, Item> warehouseMapping = new PropertyMap<ItemDto, Item>() {
   protected void configure() {
      map().getLocation().setId(source.getWarehouseId());
      map().getLocation().setWarehouseName(source.getWarehouseName());
      map().getLocation().setRowNumber(source.getWarehouseRowNumber());
      map().getLocation().setBinLabel(source.getWarehouseBinLabel());
   }
};

We do the same thing in the other direction to map the fields from the Location object in the Item entity into the flattened form fields, so we can see the warehouse fields in the list.

PropertyMap<Item, ItemDto> warehouseFieldMapping = new PropertyMap<Item, ItemDto>() {
   protected void configure() {
      map().setWarehouseId(source.getLocation().getId());
      map().setWarehouseName(source.getLocation().getWarehouseName());
      map().setWarehouseRowNumber(source.getLocation().getRowNumber());
      map().setWarehouseBinLabel(source.getLocation().getBinLabel());
   }
};

Each custom mapping can only be applied once, so we inject the ModelMapper instance into the constructor and add the mappings there.

@Autowired
public ItemController(ModelMapper modelMapper) {
   this.modelMapper = modelMapper;
   this.modelMapper.addMappings(warehouseFieldMapping);
   this.modelMapper.addMappings(warehouseMapping);
}

ModelMapper is used as it typically would in the actions on the controller.  The mappings are automatically applied based on the class types of the objects being mapped.

This is used for the list.  ModelMapper will automatically apply the warehouseFieldMapping property map.

List<ItemDto> itemDtos = items.stream().map(item -> modelMapper.map(item, ItemDto.class)).collect(Collectors.toList());

This is how it’s used when saving after adding or editing.  ModelMapper will automatically apply the warehouseMapping.

Item item = modelMapper.map(itemDto, Item.class);

Running the App

If we enter an Item like this, the warehouse fields will map to a Location object and get stored in the Location table.

Item_entry

The list of Items will reflect the location fields.

Item_list

When we list the locations, we see our Location has been saved into it, having been properly populated on the Item entity.

Location_list

This is a way of using Model Mapper to map from fields on a form into an object that belongs to another object.  Please feel free to comment with any questions or observations.

References:

 

Related Posts

 

Advertisement

5 thoughts on “Deep Mapping with ModelMapper

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s