iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0

在Java的歷史中,我們為了更責任分明的處理資料,誕生出了許多物件,如對應資料庫的Persistent Object(PO)、用來傳輸溝通使用的Data Transfer Object(DTO)、以及用來顯示的Value Object(VO),等等…

這樣針對同一個(廣義來說)物件,根據不同使用情境進行封裝,很好展現出Java的特色(隱藏不必要的細節),但缺點是管理這些物件變得比較麻煩,我們會經常將一個物件轉換為另外一個物件,比方說我們把DTO轉為PO並存入資料庫中。那有沒有一個方法可以幫助我們處理物件轉換。

有,就今天要介紹的Mapstruct。MapStruct比起傳統常看到的ObjectMapper,多了更多的功能可以使用,以符合現在更複雜的場景。

要使用Mapstruct,你首先要導入依賴。

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.0.Beta1</version>
</dependency>

假設我們現在有一隻註冊API,他負責將傳來的會員資料,判斷是否可以被註冊,並將資料存入資料庫中,我們主要要將Member轉為MemberPO,規格如下。

public class Member {

    private String name;
    @Schema(description = "Unique identifier of the User", example = "Clark")
    private String account;
    private String password;
    private String email;
    private String phone;
    private String address;
    private LocalDate birthday;
}

public class MemberPO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String name;
    @Column(nullable = false, unique = true)
    private String account;
    @Column(nullable = false)
    private String password;
    @Column(columnDefinition = "INET")
    private InetAddress ip;
    @Column(name = "wallet_id")
    private Long walletId;
    @Column(nullable = false, unique = true)
    private String email;
    @Column(name = "phone_number")
    private String phoneNumber;
    @Column(columnDefinition = "TEXT")
    private String address;
    // Update by SQL Trigger
    @Column(name = "created_at", nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;
    // Update by SQL Trigger
    @Column(name = "updated_at", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime updatedAt;
    @Column(nullable = false)
    private String status = MemberStatus.ACTIVE.name();
    @Column(nullable = false)
    private String role = MemberRole.USER.name();
    @Column
    private LocalDate birthdate;
    @Column(name = "profile_picture_url")
    private String profilePictureUrl;
    @Column(name = "last_login")
    private LocalDateTime lastLogin;
    @Column(name = "login_attempts", nullable = false)
    private int loginAttempts = 0;
    @Column(name = "security_question")
    private String securityQuestion;
    @Column(name = "security_answer")
    private String securityAnswer;
    @Column(name = "two_factor_enabled", nullable = false)
    private boolean twoFactorEnabled = false;
}

這兩個物件差異很大,欄位名稱,資料數量等等都不同,要怎麼做呢?

首先,我們要先定義出一個MapStruct來處理

@Mapper
public interface MemberMapper {
		// 實例化Mapstruct
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
}

接著我們可以加入我們的轉換方法

@Mapper
public interface MemberMapper {
		// 實例化Mapstruct
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);
    
    MemberPO memberToMemberPo(Member member);
    Member memberPOToMember(MemberPO memberPO);
}
// TEST
MemberPO memberPO = MemberMapper.INSTANCE.memberToMemberPo(member);
Member member = MemberMapper.INSTANCE.memberPOToMember(memberPO);

我建立了兩個方法,以應對Member轉MemberPO與MemberPO轉回Member物件的情境。

基本上只要這樣兩個物件就可以進行互相轉換了。但我們剛剛有提到,兩者物件的欄位名稱並不相同,針對這部分,可以加上@Mapping進行Mapping,如果邏輯比較特殊,也可以自己掛一個轉換器,我們在加上這部分吧。

@Mapping(source = "phone",target = "phoneNumber")
@Mapping(source = "birthday",target = "birthdate")
@Mapping(source = "ip", target = "ip", qualifiedByName = "stringToInetAddress")
MemberPO memberToMemberPo(Member member);

@Named("stringToInetAddress")
    default InetAddress stringToInetAddress(String ip) {
        try {
            return InetAddress.getByName(ip);
        } catch (UnknownHostException e) {
            throw new RuntimeException("Invalid IP address: " + ip, e);
        }
    }

source 代表來源,也就是傳入的物件屬性,target則代表回傳的物件屬性,透過這樣的方式去映射兩個物件,qualifiedByName則可以掛一個轉換器,以下把String的 IP轉換為 InetAddress物件。

如果你想要針對一些欄位做出預設值?Mapstruct也支援這個功能,比方說在Member轉換為MemberPO後自動配置角色權限為一般使用者(USER)、並預設帳號狀態為啟用狀態。

/**
 * 轉型後設定預設值
 * @param memberPO 轉換對象
 * @param member 轉換來源
 */
@AfterMapping
default void setDefaultValues(@MappingTarget MemberPO memberPO, Member member) {
  if (memberPO.getRole() == null) {
      memberPO.setRole(MemberRole.USER.name());
  }
  if (memberPO.getStatus() == null) {
      memberPO.setStatus(MemberStatus.ACTIVE.name());
  }
}

基本上,我常用的主要操作就是以上這些了。Mapstruct非常靈活,提供了額外寫一個方法進行手動轉換以外的方法,有興趣的朋友可以試試看這個工具,那今天的部分就到這裡了,就明天見吧。


上一篇
[DAY 19] Spring Reflection
下一篇
[DAY 21] Spring Test
系列文
週日時在做什麼?有沒有空?可以來寫SpringBoot嗎?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言