使用 MapStruct 簡化 Java 物件轉換

Why?

在應用程式的溝通中
往往牽涉到不同物件的轉換
例如 DTO 與 Entity 的互轉
傳統是用 getter、setter 處理

User user = new User();
user.setName(userDto.getName());
user.setEmail(userDto.getEmail());
//...

當然進階一點可以用 builder

User.builder()
    .name(userDto.getName())
    .email(userDto.getEmail())
    //...
    .build();

若是更複雜的邏輯可能會寫成 mapper 方法或物件
但是隨著專案規模漸長
這樣的手續會很繁瑣
MapStruct 就是為此而生的工具

How?

  1. Maven
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.5.Final</version> </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.5.5.Final</version> <scope>provided</scope>
    </dependency>
    
  2. 新增 interface
    public interface UserMapper {
    
    }
    
  3. 新增 @Mapper 註解
    import org.mapstruct.Mapper;
    
    @Mapper
    public interface UserMapper {
    
    }
    
  4. 宣告 Mapper 實例
    import org.mapstruct.Mapper;
    import org.mapstruct.factory.Mappers;
    
    @Mapper
    public interface UserMapper {
        UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    }
    
  5. 新增 method
    import org.mapstruct.Mapper;
    import org.mapstruct.factory.Mappers;
    
    import java.util.List;
    import java.util.Set;
    
    @Mapper
    public interface UserMapper {
        UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    
        UserDto toDto(User entity);
        User toEntity(UserDto dto);
    
        List<UserDto> toDto(List<User> entities);
        List<User> toEntity(List<UserDto> dtos);
    
        Set<UserDto> toDto(Set<User> entities);
        Set<User> toEntity(Set<UserDto> dtos);
    }
    
  6. MapStruct 於編譯時期自動生成實作
    @Generated(
        value = "org.mapstruct.ap.MappingProcessor",
        date = "2024-12-20T14:30:34+0800",
        comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21.0.5 (Eclipse Adoptium)"
    )
    public class UserMapperImpl implements UserMapper {
    
        @Override
        public UserDto toDto(User entity) {
            if ( entity == null ) {
                return null;
            }
    
            UserDto userDto = new UserDto();
    
            userDto.setName( entity.getName() );
            userDto.setPassword( entity.getPassword() );
            userDto.setEmail( entity.getEmail() );
    
            return userDto;
        }
        //...
    }
    

所產生的程式碼並無神奇之處
就是傳統的 getter、setter 或 builder
只是由框架代勞可以省去不少功夫

此外因為是純 Java 程式碼
不是使用反射技術,所以效能較佳

只要兩個物件 field name 一樣就能自動對應
不過實務上的映射當然不會那麼單純
不然何必轉換物件😂
因此還需要一些設定

設定以 annotation 為主,不會太難
否則就偏離想要省事的初衷了

Use Case

  1. field name 不同
    @Mapping(source = "name", target = "username")
    @Mapping(source = "type", target = "userType")
    UserDto toDto(User entity);
    
  2. 聚合物件
    @Mapping(source = "user.name", target = "username")
    @Mapping(source = "nation.name", target = "nation")
    UserDto toDto(User user, Nation nation);
    
  3. 型別不同
    // date to string
    @Mapping(source = "modifyDate", target = "modifyDate", dateFormat = "yyyy-MM-dd")
    UserDto toDto(User entity);
    
  4. 呼叫 expression:簡易邏輯
    @Mapping(target = "type", expression = "java(entity.getType() == null ? null : entity.getType().getChineseName())")
    UserDto toDto(User entity);
    
  5. 呼叫自定義方法:複雜邏輯
    @Mapping(source = "type", target = "type", qualifiedByName = "mapTypeToString")
    UserDto toDto(User entity);
    
    @Named("mapTypeToString")
    default String mapTypeToString(FinancialInstitutionType type) {
        if (type == null) {
            return null;
        }
        return type.getChineseName();
    }
    

JPA

一般做法都是在 service 層使用 MapStruct 轉換
如果在 repository 層使用 JPQL
就能在查詢的同時將結果直接轉為 DTO

public interface UserRepository extends JpaRepository<User, String> {
    @Query("SELECT new com.syun.dto.UserDto(user.id, user.username) FROM User user")
    List<UserDto> findAllUserDto();
}

當然這只能映射查詢結果中的欄位
無法進行複雜的轉換或關聯物件的映射

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *