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?
- 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>
- 新增 interface
public interface UserMapper { }
- 新增
@Mapper
註解import org.mapstruct.Mapper; @Mapper public interface UserMapper { }
- 宣告 Mapper 實例
import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; @Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); }
- 新增 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); }
- 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
- field name 不同
@Mapping(source = "name", target = "username") @Mapping(source = "type", target = "userType") UserDto toDto(User entity);
- 聚合物件
@Mapping(source = "user.name", target = "username") @Mapping(source = "nation.name", target = "nation") UserDto toDto(User user, Nation nation);
- 型別不同
// date to string @Mapping(source = "modifyDate", target = "modifyDate", dateFormat = "yyyy-MM-dd") UserDto toDto(User entity);
- 呼叫 expression:簡易邏輯
@Mapping(target = "type", expression = "java(entity.getType() == null ? null : entity.getType().getChineseName())") UserDto toDto(User entity);
- 呼叫自定義方法:複雜邏輯
@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();
}
當然這只能映射查詢結果中的欄位
無法進行複雜的轉換或關聯物件的映射