Mapping Children with ModelMapper

Mapping Collections of Children with ModelMapper

In this post, I’m going to demonstrate how to use ModelMapper when you’re allowing entry of child records on the parent page.  In the absence of a flash of inspiration for creative examples, I’m using contact information as the parent that can have zero or many child phone number records.  If you want to follow along, use the Spring Initialzr and create a Gradle, Java Project with the Web, Thymeleaf, JPA and H2 dependencies.

Related Posts:

Prerequisites:

  • Java 8
  • Preferred IDE
  • Basic understanding of Spring Boot and Spring JPA

Gradle Dependency

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

I’m going to take us through the code to make the form below functional.

mm_parentchild_entry

Entities

The example uses two entity classes to model the information we’re storing.  The Contact class is the parent class and it has a collection of PhoneNumber objects.  I’ve omitted the getters and setters for the parent fields, but I want to point out that in the getter for the phoneNumbers list, I’m creating an empty list if the list is null.  There is one special method here, addPhoneNumber.  It takes a type and phone number and creates a new PhoneNumber using those values and adds it to the list.  Something to note is that it also sets the Contact field on the newly created PhoneNumber to “this“.  That’s important to get the relationship working properly.  (Note:  Cascade.ALL is used in the example, but in a real application consider your use case before you choose your cascade options.)

@Entity
public class Contact {
   @Id
   @GeneratedValue(strategy=GenerationType.AUTO)
   private Long id;
   private String firstName;
   private String lastName;
   private String title;
   private String company;
   private String notes;
   @OneToMany(mappedBy="contact", cascade=CascadeType.ALL, orphanRemoval=true)
   private List phoneNumbers;

   //Parent field getters and setters omitted for brevity

   /**
    * Gets phoneNumbers.
    * @return the phoneNumbers
    */
   public List getPhoneNumbers() {
      if (phoneNumbers == null) {
         phoneNumbers = new ArrayList();
      }
      return phoneNumbers;
   } 

   /**
    * Sets phoneNumbers.
    * @param phoneNumbers the phoneNumbers to set
    */
   public void setPhoneNumbers(List phoneNumbers) {
      this.phoneNumbers = phoneNumbers;
   }

   public void addPhoneNumber(String type, String phoneNumber) {
      PhoneNumber newNumber = new PhoneNumber();
      newNumber.setType(type);
      newNumber.setNumber(phoneNumber);
      newNumber.setContact(this);
      if (phoneNumbers == null) {
         phoneNumbers = new ArrayList();
      }
      phoneNumbers.add(newNumber);
   }

}

The class PhoneNumber represents a single phone number record.  It has a type (e.g. mobile, home etc) and a number field in addition to its id.  It also has its own reference back to its parent Contact.

@Entity
public class PhoneNumber {
   @Id
   @GeneratedValue(strategy=GenerationType.AUTO)
   private Long id;
   private String type;
   private String number;
   @ManyToOne
   @JoinColumn(name="contact_id")
   private Contact contact;

   //Getters and Setters omitted for brevity.

}

Data Transfer Objects

For the domain classes, there is a corresponding data transfer object (DTO) class.  The ContactDto class should look pretty familiar.  It’s worth pointing out again that the getPhoneNumbers() checks the phoneNumbers list for null and, if so, creates a new list before returning it.

public class ContactDto {
   private Long id;
   private String firstName;
   private String lastName;
   private String title;
   private String company;
   private String notes;
   private List phoneNumbers;

   //Getters/Setters omitted for brevity

   /**
    * Gets phoneNumbers.
    * @return the phoneNumbers
    */
   public List getPhoneNumbers() {
      if (phoneNumbers == null) {
         phoneNumbers = new ArrayList();
      }
      return phoneNumbers;
   }

   /**
    * Sets phoneNumbers.
    * @param phoneNumbers the phoneNumbers to set
    */
   public void setPhoneNumbers(List phoneNumbers) {
      this.phoneNumbers = phoneNumbers;
   }
}

There are a couple of things to discuss about the PhoneNumberDto class.  The first is that there’s a second Constructor that takes an id, type, and phoneNumber.  That’s there simply to make it more convenient when we’re creating PhoneNumberDtos and adding them to the list in ContactDto.  The other thing I want to mention is the deleted boolean.  Because this isn’t a Thymeleaf, Ajax, or JQuery (or something I haven’t thought of) tutorial, I implemented deletion with a simple delete checkbox.  The deleted field here corresponds with that checkbox.

public class PhoneNumberDto {
   private Long id;
   private String type;
   private String phoneNumber;
   private boolean deleted;


   /**
    * Creates an empty PhoneNumberDto object.
    */
   public PhoneNumberDto() {
   }

   /**
    * Creates a PhoneNumberDto with the values provided.
    * 
    * @param id
    * @param type
    * @param phoneNumber
    */
   public PhoneNumberDto(Long id, String type, String phoneNumber) {
      this.id = id;
      this.type = type;
      this.phoneNumber = phoneNumber;
   }

   //Getters/Setters omitted for brevity
}

ContactController

The ContactController class is where ModelMapper comes into things.  Because we’re going to be adding custom converters to our injected ModelMapper instance, we’re going to inject it into the constructor.  Once we assign the injected instance to our instance variable, we add two converters to it.  We’ll be discussing those next.

@Controller
public class ContactController {
   @Autowired
   private ContactRepository contactRepository;
   private ModelMapper modelMapper;

   /**
    * Constructs a new ContactController with the autowired ModelMapper provided.
    * 
    * Adds custom converters to the injected Model Mapper instance.
    * 
    * @param modelMapper
    */
   @Autowired
   public ContactController(ModelMapper modelMapper) {
      this.modelMapper = modelMapper;
      this.modelMapper.addConverter(populateExistingNumbers);
      this.modelMapper.addConverter(handlePhoneNumbersEntered);
   }

We need one converter for each “direction” of mapping.  So our first converter is going to cover mapping from the DTO to the entity object.  We create a new Converter and specify the source type first and the destination type second.  Then we have to implement the convert method.  Because this converter is going to be used in place of the one Model Mapper would create on its own, we have to map all of the Contact level fields ourselves even though it’s very straightforward.  That’s what the first six lines of code are all about.  Next, we handle the case where we have a new Contact.  It’s the easiest case, because if the parent Contact is new, then all the child PhoneNumber records are also going to be new.  All we have to do is loop through the list of PhoneNumberDto and add them to the List of PhoneNumber on our destination contact.  We use the addPhoneNumber method I mentioned in my discussion of the Contact entity above.  This is very important because if the parent Contact value is not added to the PhoneNumber children, they will not save to the database correctly.  ModelMapper actually almost handles the mapping of the list of PhoneNumberDto to list of PhoneNumber without help except for that one very important detail.

Handling the case where we’re editing an existing contact is more complicated.  We have to actually load the existing Contact from the database so we can compare it’s list of PhoneNumber to the incoming phone numbers.  For each PhoneNumberDto coming from the user input, we have to loop through the existing PhoneNumber list.  If we find a match, we check the delete flag.  If it’s deleted we don’t have to do anything to it; it will be deleted from the database when it doesn’t appear in the collection on the updated parent Contact.  If it’s not deleted, we update the PhoneNumber object with the Type and PhoneNumber from the form and add it to the destination Contact’s list.  We set a found flag and break out of the innermost loop.  If there’s no match, it’s new and we add it the same way we did for a new Contact.

Converter<ContactDto, Contact> handlePhoneNumbersEntered = new Converter<ContactDto, Contact>() {

   @Override
   public Contact convert(MappingContext<ContactDto, Contact> context) {
      //This custom converter replaces the one automatically created by ModelMapper,
      //So we have to map each of the contact fields as well.
      context.getDestination().setId(context.getSource().getId());
      context.getDestination().setFirstName(context.getSource().getFirstName());
      context.getDestination().setLastName(context.getSource().getLastName());
      context.getDestination().setTitle(context.getSource().getTitle());
      context.getDestination().setCompany(context.getSource().getCompany());
      context.getDestination().setNotes(context.getSource().getNotes());

      //if new, we just have to create phone numbers for each number, so deal with that first
      if (context.getSource().getId() == null) {
         for (PhoneNumberDto numberDto : context.getSource().getPhoneNumbers()) {
            context.getDestination().addPhoneNumber(numberDto.getType(), numberDto.getPhoneNumber());
         }
      } else {
         Contact existing = contactRepository.getOne(context.getSource().getId());

         for (PhoneNumberDto phoneNumDto : context.getSource().getPhoneNumbers()) {
            boolean found = false;
            //For each phone number coming in from the form, check the existing phone numbers
            //from the database. If there's a match, update the phone number object and add it to the destination 
            //phone numbers collection, unless it's deleted and then we leave it out of the collection.
            for (PhoneNumber phoneNumber : existing.getPhoneNumbers()) {
               if (phoneNumDto.getId() != null && phoneNumDto.getId().longValue() == phoneNumber.getId().longValue()) {
                  found = true;
                  if (!phoneNumDto.isDeleted()) {
                     phoneNumber.setType(phoneNumDto.getType());
                     phoneNumber.setNumber(phoneNumDto.getPhoneNumber());
                     context.getDestination().getPhoneNumbers().add(phoneNumber);
                  }
                  break;
               }
            }
            //For input phone numbers that aren't in the database, add it to the destination.
            if (!found && !phoneNumDto.getPhoneNumber().isEmpty()) {
               context.getDestination().addPhoneNumber(phoneNumDto.getType(), phoneNumDto.getPhoneNumber());
            }
         }
      }

      return context.getDestination();
   }

};

The second converter handles the mapping from a Contact entity to a ContactDto for displaying to the user.  As mentioned above, we have to first map all the parent Contact fields.  Once we’ve done that, we add a new empty PhoneNumberDto to the beginning of the list so the user has somewhere to enter a new phone number.  Then we just loop through the List of PhoneNumber on the source Contact and create PhoneNumberDto objects for them and add them to the list on the destination ContactDto.  Note that we’re using the special constructor I added in PhoneNumberDto as a convenience.

Converter<Contact, ContactDto> populateExistingNumbers = new Converter<Contact, ContactDto>() {

   @Override
   public ContactDto convert(MappingContext<Contact, ContactDto> context) {
      //This custom converter replaces the one automatically created by ModelMapper,
      //So we have to map each of the contact fields as well.
      context.getDestination().setId(context.getSource().getId());
      context.getDestination().setFirstName(context.getSource().getFirstName());
      context.getDestination().setLastName(context.getSource().getLastName());
      context.getDestination().setTitle(context.getSource().getTitle());
      context.getDestination().setCompany(context.getSource().getCompany());
      context.getDestination().setNotes(context.getSource().getNotes());

      //Add an empty phone number for adding a new number
      context.getDestination().getPhoneNumbers().add(new PhoneNumberDto());

      for (PhoneNumber number : context.getSource().getPhoneNumbers()) {
         context.getDestination().getPhoneNumbers().add(new PhoneNumberDto(number.getId(), number.getType(), number.getNumber()));
      }
      return context.getDestination();
   }
};

That’s all it takes to handle the mapping collections of children between an entity and a data transfer object using ModelMapper.  To enhance usability, it would be nice to be able to add on demand a new row for phone numbers and delete could certainly be enhanced.  And let’s not forget validation… I’ll provide a few links in the References and Further Reading for anyone interested in pursuing that.  All example code is available on GitHub.

References and Further Reading

 

Advertisements

5 thoughts on “Mapping Children 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 )

Google photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s