在 MapStruct 中為目標屬性設定空值
1. 簡介
在本文中,我們將探討在使用 MapStruct 映射物件時將屬性一致設定為 null 的方法。
2.創建模型
為了說明映射,我們將建立兩個模型類別:一個代表持久實體文章,另一個代表文章更新物件。
首先,持久化Article類別:
public class Article {
private String id;
private String reviewedBy;
// no-arg and all-arg constructors
// getters and setters
}
以及用於改變實體的ArticleDTO類別:
public class ArticleDTO {
private String title;
// no-arg and all-arg constructors
// getters and setters
}
我們的想法是,每次我們使用ArticleDTO更新文章時,我們也會將Article's reviewedBy字段更新為null.
3. 將ArticleDTO映射到Article
我們將使用 MapStruct 將 DTO 對應到要儲存的新實體。第一步是建立具有@Mapper註解的ArticleMapper介面:
@Mapper
public interface ArticleMapper {
}
有了它,我們可以在其中添加抽象映射器方法,MapStruct 將自動為我們產生實作。
3.1. 使用expression屬性
MapStruct 允許我們在映射器中將 Java 表達式定義為字串,因此它會將相同的表達式複製到產生的實作程式碼中。因此,將reviewedBy設為 null 的一種方法是定義一個計算結果為 null 的表達式:
@Mapping(target = "title", source = "dto.title")
@Mapping(target = "id", source = "persisted.id")
@Mapping(target = "reviewedBy", expression = "java(null)")
Article toArticleUsingExpression(ArticleDTO dto, Article persisted);
我們也可以透過建立傳回 null 的自訂預設方法來做到這一點:
@Mapping(target = "title", source = "dto.title")
@Mapping(target = "id", source = "persisted.id")
@Mapping(target = "reviewedBy", expression = "java(getReviewedBy())")
Article toArticleUsingExpressionMethod(ArticleDTO dto, Article persisted);
default String getReviewedBy() {
return null;
}
兩者都實現了相同的功能,如測試所示:
@Test
void givenArticleDTO_whenToArticleUsingExpression_thenReturnsArticleWithNullStatus() {
Article oldArticle1 = new Article("Title 1", "John Doe");
Article oldArticle2 = new Article("Title 2", "John Doe");
Article result1 = articleMapper.toArticleUsingExpression(new ArticleDTO("Updated article title"), oldArticle1);
Article result2 = articleMapper.toArticleUsingExpressionMethod(new ArticleDTO("Updated article title"), oldArticle2);
assertThat(result1.getReviewedBy()).isNull();
assertThat(result2.getReviewedBy()).isNull();
assertThat(result1.getTitle()).isEqualTo("Updated article title");
assertThat(result2.getTitle()).isEqualTo("Updated article title");
}
如果我們需要在傳回空值之前應用任何附加邏輯,則使用getReviewedBy()方法的表達式可能更有優勢,有利於可重複使用性。
3.2. 使用qualifiedBy屬性
與expression類似,我們可以支援可重複使用性,並定義一個傳回 null 的預設命名方法。然後,我們可以在@Mapping註解的qualifiedBy屬性中使用它:
@Mapping(target = "title", source = "dto.title")
@Mapping(target = "id", source = "persisted.id")
@Mapping(target = "reviewedBy", qualifiedByName = "toNull")
Article toArticleUsingQualifiedBy(ArticleDTO dto, Article persisted);
@Named("toNull")
default String mapToNull(String property) {
return null;
}
使用qualifiedByName,我們可以使用@Named命名方法,並在帶有註解的映射器方法中重複使用它。值得注意的是,我們將方法命名為toNull並將其傳遞給@Mapping註解。這也能得到正確的結果:
@Test
void givenArticleDTO_whenToArticleUsingQualifiedBy_thenReturnsArticleWithNullStatus() {
Article oldArticle1 = new Article("Title 1", "John Doe");
Article result1 = articleMapper.toArticleUsingQualifiedBy(new ArticleDTO("Updated article 1 title"), oldArticle1);
assertThat(result1.getReviewedBy()).isNull();
assertThat(result1.getTitle()).isEqualTo("Updated article 1 title");
}
3.3. 使用ignore屬性
我們也可以使用Mapper註解的ignore屬性來忽略其序列化。因此,由於目標對像是由 MapStruct 建立的新對象,並且其所有欄位都初始化為 null,因此忽略某個屬性將使其欄位保留 null 值。
為了做到這一點,我們可以修改我們的映射器:
@Mapping(target = "title", source = "dto.title")
@Mapping(target = "id", source = "persisted.id")
@Mapping(target = "reviewedBy", ignore = true)
Article toArticleUsingIgnore(ArticleDTO dto, Article persisted);
這也給了我們正確的結果:
@Test
void givenArticleDTO_whenToArticleUsingIgnore_thenReturnsArticleWithNullReviewedBy() {
Article oldArticle1 = new Article("Title 1", "John Doe");
Article result1 = articleMapper.toArticleUsingIgnore(new ArticleDTO("Updated article 1 title"), oldArticle1);
assertThat(result1.getReviewedBy()).isNull();
assertThat(result1.getTitle()).isEqualTo("Updated article 1 title");
}
3.4. 使用@AfterMapping註解
MapStruct 允許我們使用@AfterMapping註解定義一個在映射完成後執行的預設方法。因此,我們可以定義一個帶有註解的方法,當映射目標為Article類型時,將reviewedBy字段設為 null:
@AfterMapping
default void setNullReviewedBy(@MappingTarget Article article) {
article.setReviewedBy(null);
}
@Mapping(target = "title", source = "dto.title")
@Mapping(target = "id", source = "persisted.id")
Article toArticleUsingAfterMapping(ArticleDTO dto, Article persisted);
這也給了我們預期的結果:
@Test
void givenArticleDTO_whenToArticleWithNullStatus_thenReturnsArticleWithNullStatus() {
Article oldArticle1 = new Article("Title 1", "John Doe");
Article result1 = articleMapper.toArticleUsingAfterMapping(new ArticleDTO("Updated article 1 title"), oldArticle1);
assertThat(result1.getReviewedBy()).isNull();
assertThat(result1.getTitle()).isEqualTo("Updated article 1 title");
}
透過這種方法,每當我們使用產生Article類型的映射器方法(來自定義AfterMapping的相同類別)時, setNullReviewedBy()方法就會運行並將reviewedBy設為 null。
因此,它的用法取決於我們處理的特定場景。如果我們必須始終產生reviewedBy為null的Articles ,那麼它就很合適。
否則,它可能會對給定映射器類別的其他方法產生副作用並產生不必要的結果。
4. 推廣到多態類型
在 MapStruct 1.5.0 之後,我們還可以為多態超類型定義映射器,從而允許映射行為自動傳遞給它們的子類型。
為了將其與將reviewedBy欄位設為null的行為結合,我們可以建立一個通用映射器,使用MapStruct子類型將reviewedBy設定為基礎實體的所有子類型。
4.1. 建立新的類別結構
為了說明泛型映射,我們使用一個包含兩種基底類型的新類別結構。一種是Reviewable類型:
public class Reviewable{
protected String title;
protected String reviewedBy;
// getters, setters, and constructors
}
另一個是ReviewableDTO:
public class ReviewableDTO {
private String title;
// getters, setters. and constructors
}
這樣,我們就可以建立Reviewable子類別:
public class Article extends Reviewable {
private String title;
// getter, setter, and constructors
}
public class WeeklyNews extends Reviewable {
//getters setters constructors
}
以及ReviewableDTO子類別:
public class ArticleDTO extends ReviewableDTO {
private String title;
// getters, setters, and constructors
}
public class WeeklyNewsDTO extends ReviewableDTO {
private String title;
// getters, setters, and constructors
}
4.2. 映射子類型
給定新的類別結構,我們可以定義ReviewableMapper:
@Mapper
public interface ReviewableMapper {
@SubclassMapping(source = ArticleDTO.class, target = Article.class)
@SubclassMapping(source = WeeklyNewsDTO.class, target = WeeklyNews.class)
@Mapping(target = "reviewedBy", expression = "java(null)")
Reviewable toReviewable(ReviewableDTO dto);
}
值得注意的是,新的映射器使用@SubclassMapping批註來接受ReviewableDTO的所有子類別並傳回Reviewable的所有子類別。此外,我們保留了expression=java(null)用於映射reviewedBy ,但我們可以選擇本文介紹的任何形式。
讓我們驗證所需的行為:
@Test
void givenArticleDTO_whenToReviewableUsingMapper_thenReturnsArticleWithNullStatus() {
Reviewable result1 = reviewableMapper.toReviewable(new ArticleDTO("Updated article 1 title"));
Reviewable result2 = reviewableMapper.toReviewable(new WeeklyNewsDTO());
assertThat(result1).isInstanceOf(Article.class);
assertThat(result2).isInstanceOf(WeeklyNews.class);
assertThat(result1.getReviewedBy()).isNull();
assertThat(result2.getReviewedBy()).isNull();
}
值得注意的是,映射器同時接受兩種Reviewable DTO子類型,並將它們對應到對應的Reviewable類型,這已透過AssertJ's isInstanceOf()方法驗證。此外,我們也實作了所需的行為:使用 ReviewableMapper 始終將reviewedBy設為 null ReviewableMapper.
5. 結論
在本文中,我們學習了使用 MapStruct 中的expression, qualifiedBy, @Mapper ignore屬性始終將特定物件欄位設為 null 的不同方法。此外,我們也探討如何使用@AfterMapping來實現此目的。
最後,我們學習如何使用 MapStruct 的@SubclassMapping來概括多型態類型的行為。
與往常一樣,原始碼可在 GitHub 上取得。