Java

Tìm hiểu về ModelMapper trong Java

1. Tại sao cần phải Map?

Các object trong java có cấu trúc tương tự nhay nhưng khác object models, khi muốn set data của object này cho object kia với các field tương ứng, ta phải đi set từng field một rất mất công.

Ví dụ ta có 2 Ojbect như bên dưới:

public class CustomerDTO {

  private long customerId = 1;
   private String fullName = "Robert King";
   private String firstName = "Robert";
   private String lastName = "King";

}

public class AccountDTO {

   private long accountId;
    private String userName;
    private String password;
    private String fullName;
    private String firstName;
    private String lastName;
}

Giờ ta muốn set data của CustomerDTO vào AccountDTO để insert vào DB hay làm gì đó chẳng hạn, thì code tù nhất như sau:

  AccountDTO accountDTO = new AccountDTO();
   accountDTO.setFullName(customerDTO.getFullName());
   accountDTO.setFirstName(customerDTO.getFirstName());
   accountDTO.setLastName(customerDTO.getLastName());

Result:

   AccountDTO(accountId=0, userName=null, password=null, fullName=Robert King, firstName=Robert, lastName=King)

Như mình nói ở trên, nhìn code này hơi tù, thử nghĩ object của bạn có 100 field thì thế nào ?

2. Model Mapper là gì?

Giải quyết vấn đề trên thì mình có thể sử dụng ModelMapper. ModelMapper là 1 thư viện của java, được release dưới license của Apache (Apache License v2.0).

Để sử dụng ModelMapper thì phải import thư viện của ModelMapper vào, ở đây mình dùng maven:

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

Thử code lại đoạn code setting AccountDTO ở trên bằng thư viện của Model Mapper xem thế nào:

  ModelMapper modelMapper = new ModelMapper();
   AccountDTO accountDTO = new AccountDTO();
   CustomerDTO customerDTO = modelMapper.map(accountDTO, CustomerDTO.class);

Nhìn gọn hơn đúng không nào ? Nhưng mà khoan, nếu mapping như vậy thì trường CustomerDTO.customerId có được set cho trường nào không? và các trường AccountDTO.accountId, AccountDTO.userName, AccountDTO.password có giá trị thế nào? Xem kết quả nào Result:

AccountDTO(accountId=0, userName=null, password=null, fullName=Robert King, firstName=Robert, lastName=King)

May quá kết quả vẫn giống ở trên.

3. Cách hoạt động của Model Mapper

ModelMapper bao gồm 2 process: Matching process và Mapping process.

  • Matching process: xử lý này sẽ dựa vào config của ModelMapper để xác định các thuộc tính của source nguồn và source đích, xử lý matching sẽ dựa vào 5 "tính năng": Eligibility, Transformation, Tokenization, Matching , Handling Ambiguity

  • Mapping process: xứ lý mapping nguồn và đích theo độ ưu tiên dựa vào TypeMap và Converter:

    • If a TypeMap exists for the source and destination types, mapping will occur according to the Mappings defined in the TypeMap.
    • Else if a Converter exists that is capable of converting the source object to the destination type, mapping will occur using the Converter.
    • Else a new TypeMap is created for the source and destination types, and mapping will occur according to the implicit Mappings captured in the TypeMap.

4. Các bước của Matching process trong Model Mapper

Đọc đến đây bạn đã thấy chán chưa ? Nếu cảm thấy chán rồi thì voilà, mình không quan tâm đâu (hoho10).

Tại sao mình lại nói May quá kết quả vẫn giống ở trên.

Vì khi dùng Model Mapper hay bất cứ 1 thư viện nào đó, bạn phải control hết được các chức năng của nó. Thực tế mình đã dùng Model Mapper và gặp nhiều lỗi hớ hên do không nắm rõ bản chất của ModelMapper.

Vậy cùng đi vào chi tiết các "tính năng" của Model Mapper nào:

  1. Eligibility:

Eligibility dùng AccessLevel và NamingConventions để kiểm tra 1 field có thể matching:

  • AccessLevel:

    • AccessLevel.PUBLIC
    • AccessLevel.PROTECTED
    • AccessLevel.PACKAGE_PRIVATE
    • AccessLevel.PRIVATE
  • NamingConventions:

    • NamingConventions.NONE
    • NamingConventions.JAVABEANS_ACCESSOR
    • NamingConventions.JAVABEANS_MUTATOR

Ví dụ:

CustomerDTO {
     private long customerId = 1;
     private String fullName = "Robert King";
     private String firstName = "Robert";
     private String lastName = "King";
     protected String name = "protected Name";
 }

Source code mapping:

ModelMapper modelMapper = new ModelMapper();
 modelMapper.getConfiguration().setFieldMatchingEnabled(true);
 modelMapper.getConfiguration().setFieldAccessLevel(Configuration.AccessLevel.PROTECTED);
 CustomerDTO sourceDto = new CustomerDTO();
 AccountDTO destinationDto = modelMapper.map(sourceDto, AccountDTO.class);

Result:

AccountDTO(accountId=0, name=protected Name, userName=null, password=null, fullName=null, firstName=null, lastName=null)
  1. Transformation:

Trước khi matching, NameTransformer thực hiện chuyển đổi name property thành simple name property. Ví dụ: object nguồn có method getName, object đích có phương thức setName thì NameTransformer sẽ chuyển tên phương thức thành name

  1. Tokenization: Tokenization có 1 pre-define condition để matching: NameTokenizers
  • NameTokenizers.CAMEL_CASE : Default
  • NameTokenizers.UNDERSCORE

Giả sử giờ thêm 1 trường name nào đó vào AccountDTO thử xem thế nào:

public class AccountDTO {

   private long accountId;
    private String name;
    private String userName;
    private String password;
    private String fullName;
    private String firstName;
    private String lastName;

}

Code mapping:

  ModelMapper modelMapper = new ModelMapper();
   AccountDTO accountDTO = new AccountDTO();
   CustomerDTO customerDTO = modelMapper.map(accountDTO, CustomerDTO.class);

Result:

  Exception:
   Exception in thread "main" org.modelmapper.ConfigurationException: ModelMapper configuration errors:
   1) The destination property com.first.application.TestSample$AccountDTO.setName() matches multiple source property hierarchies:

Tại sao lại xảy ra lỗi này ? Như đã nói ở trên: Trước khi thực hiện mapping thì ModelMapper sẽ thực hiện xử lý Matching, cụ thể trong trường hợp này là Tokenization

Vì default của NameTokenizers là CAMEL_CASE nên:

  • CustomerDTO.fullName
  • CustomerDTO.firstName
  • CustomerDTO.lastName

sau khi được Tokenization đều có thể set value của 3 trường của CustomerDTO vào AccountDTO.name, do đó xảy ra exception vì không biết phải set trường nào của CustomerDTO cho AccountDTO.name.

Để fix lỗi này thì có thể set NameTokenizers về UNDERSCORE

ModelMapper modelMapper = new ModelMapper();
 modelMapper.getConfiguration().setSourceNameTokenizer(NameTokenizers.UNDERSCORE	);  
 CustomerDTO customerDTO = new CustomerDTO(); 
 AccountDTO accountDTO = modelMapper.map(customerDTO, AccountDTO.class);

Result:

AccountDTO(accountId=0, name=null, userName=null, password=null, fullName=Robert King, firstName=Robert, lastName=King)

Ở đây xảy ra exception nên mình có thể biết để fix, nhưng nhiều trường hợp tự động mapping sẽ dẫn đến sai dữ liệu.

  1. Matching:

MatchingStrategies được dùng để xác định khi properties của object nguồn và đích match với nhau dựa vào name và class name tokens.

  1. Handling Ambiguity:

Khi matching strategies không chính xác, có thể nhiều property của object nguồn có thể khớp với property của object đích. Khi điều này xảy ra thì matching engine sẽ chọn kết quả map gần nhất để thực hiện mapping.

5. Tổng kết

Trên đây chỉ là tổng quan, giới thiệu khái quát về ModelMapper, vẫn còn rất nhiều tính năng của ModelMapper mà có dịp mình sẽ giới thiệu thêm, bạn có thể tham khảo tại đây

Nguồn tham khảo:

Registration Login
Sign in with social account
or
Lost your Password?
Registration Login
Sign in with social account
or
A password will be send on your post
Registration Login
Registration