从数据传输对象(DTO)到具有双向一对多关联的Hibernate实体执行MapStruct映射的最佳方式是什么?

假设我们有一个BookDto,其中有多个ReviewDto类 comments :

public class BookDto {
  private List<ReviewDto> reviews;
  // getter and setters...
}

对应的Hibernate 实体BookReview有一对多的关联:

@Entity
public class Book {
  @OneToMany(mappedBy = "book", orphanRemoval = true, cascade = CascadeType.ALL)
  private List<Review> reviews = new ArrayList<>();
  
  public void addReview(Review review) {
    this.reviews.add(review);
    review.setBook(this);
  }
  //...
}
@Entity
public class Review {
  @ManyToOne(fetch = FetchType.LAZY)
  private Book book;

  public void setBook(Book book) {
    this.book = book;
  }
  //...
}

请注意,本书的addReview方法通过调用Hibernate专家推荐的review.setBook(this)(例如'Hibernate Tips: How to map a bi-directional many-to-one association' by Thorben Janssen'How to synchronize bidirectional entity associations with JPA and Hibernate' by Vlad Mihalcea)双向设置关联,以确保域模型关系的一致性.

现在,我们需要一个MapStruct映射器,它可以自动将 comments 链接回该书.

  1. 自定义映射方法:
@Mapper
public interface BookMapper {
  default Book mapBookDtoToBook(BookDto bookDto) {
    //...
    for (ReviewDto reviewDto : bookDto.getReviews()) {
      book.addReview(mapReviewDtoToReview(reviewDto));
    }
    //...
  }
  //...
}

如果这本书还有许多其他字段需要映射,那么这会变得很麻烦.[Update: This can be simplified as suggested by 100.]

  1. @AfterMapping的方法使关系具有双向性:
@Mapper
public interface BookMapper {
  Book mapBookDtoToBook(Book book); // Implementation generated by MapStruct

  @AfterMapping
  void linkReviewsToBook(@MappingTarget Book book) {
    for (Review review : book.getReviews()) {
      review.setBook(book);
    }
  }
  //...
}

这种方法允许MapStruct生成所有其他字段映射;但是,通过在after映射中将自动生成的setReviewssetBook操作分离,我们失go 了内聚性.

  1. Book中添加方法setBiDirectionalReviews,并指示MapStruct将其用作target:
@Entity
public class Book {
  //...
  public void setBiDirectionalReviews(List<Review> reviews) {
    this.reviews = reviews;
    for (Review review : this.reviews) {
      review.setBook(this);
    }
  }
}
@Mapper
public class BookMapper {
  @Mapping(source = "reviews", target = "biDirectionalReviews")
  Book mapBookDtoToBook(Book book);
}

Now we have re-established cohesion, but (1) we might still need the additional method addReview if we wanted to modify the existing reviews somewhere else, and (2) it feels somewhat hacky to abuse MapStruct's accessor naming strategy by pretending there were a field named "biDirectionalReviews".
Anyway, this is the best approach that I could find so far.

在MapStruct中映射双向关联有更好的解决方案吗?

推荐答案

Solution 1, 100

例子:

@Mapper
public interface JpaMapper {
    JpaMapper MAPPER = Mappers.getMapper( JpaMapper.class );

    Book toEntity(BookDTO s, @Context JpaContext ctx);

    @Mapping(target = "book", ignore = true)
    Review toEntity(ReviewDTO s, @Context JpaContext ctx);
}

public class JpaContext {
    private Book bookEntity;

    @BeforeMapping
    public void setEntity(@MappingTarget Book parentEntity) {
        this.bookEntity = parentEntity;
        // you could do stuff with the EntityManager here
    }

    @AfterMapping
    public void establishRelation(@MappingTarget Review childEntity) {
        childEntity.setBook(bookEntity);
        // you could do stuff with the EntityManager here
    }
}

用法:

Book book = JpaMapper.MAPPER.toEntity(bookDTO, new JpaContext());

生成的代码:

public class JpaMapperImpl implements JpaMapper {

    @Override
    public Book toEntity(BookDTO s, JpaContext ctx) {
        if ( s == null ) {
            return null;
        }

        Book book = new Book();

        ctx.setEntity( book );

        book.setId( s.getId() );
        book.setName( s.getName() );
        book.setReviews( reviewDTOListToReviewList( s.getReviews(), ctx ) );

        return book;
    }

    @Override
    public Review toEntity(ReviewDTO s, JpaContext ctx) {
        if ( s == null ) {
            return null;
        }

        Review review = new Review();

        review.setId( s.getId() );
        review.setName( s.getName() );

        ctx.establishRelation( review );

        return review;
    }

    protected List<Review> reviewDTOListToReviewList(List<ReviewDTO> list, JpaContext ctx) {
        if ( list == null ) {
            return null;
        }

        List<Review> list1 = new ArrayList<Review>( list.size() );
        for ( ReviewDTO reviewDTO : list ) {
            list1.add( toEntity( reviewDTO, ctx ) );
        }

        return list1;
    }
}


Solution 2, 100

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface JpaMapper {
    JpaMapper MAPPER = Mappers.getMapper( JpaMapper.class );

    Book toEntity(BookDTO s);

    @Mapping(target = "book", ignore = true)
    Review toEntity(ReviewDTO s);
}

生成的代码:

public class JpaMapperImpl implements JpaMapper {

    @Override
    public Book toEntity(BookDTO s) {
        if ( s == null ) {
            return null;
        }

        Book book = new Book();

        book.setId( s.getId() );
        book.setName( s.getName() );
        if ( s.getReviews() != null ) {
            for ( ReviewDTO review : s.getReviews() ) {
                book.addReview( toEntity( review ) );
            }
        }

        return book;
    }

    @Override
    public Review toEntity(ReviewDTO s) {
        if ( s == null ) {
            return null;
        }

        Review review = new Review();

        review.setId( s.getId() );
        review.setName( s.getName() );

        return review;
    }
}

单元测试:

    @Test
    public void test() {
        BookDTO bookDTO = new BookDTO();
        bookDTO.setId(1L);
        bookDTO.setName("Book 1");

        ReviewDTO reviewDTO1 = new ReviewDTO();
        reviewDTO1.setId(1L);
        reviewDTO1.setName("Review 1");

        ReviewDTO reviewDTO2 = new ReviewDTO();
        reviewDTO2.setId(2L);
        reviewDTO2.setName("Review 2");

        List<ReviewDTO> reviewDTOS = Arrays.asList(reviewDTO1, reviewDTO2);
        bookDTO.setReviews(reviewDTOS);

        Book book = JpaMapper.MAPPER.toEntity(bookDTO, new JpaContext());
        //Book book = JpaMapper.MAPPER.toEntity(bookDTO);

        Assert.assertNotNull(book);
        Assert.assertEquals(book.getId(), book.getId());
        Assert.assertEquals(book.getName(), bookDTO.getName());

        Assert.assertEquals(book.getReviews().size(), bookDTO.getReviews().size());
        Assert.assertEquals(book.getReviews().get(0).getId(), bookDTO.getReviews().get(0).getId());
        Assert.assertEquals(book.getReviews().get(1).getId(), bookDTO.getReviews().get(1).getId());

        book.getReviews().forEach(review -> Assert.assertEquals(review.getBook(), book));
    }

Java相关问答推荐

我应该避免在Android中创建类并在运行时编译它们吗?

空手道比赛条件

为什么我们仍然需要实现noArgsConstructor如果Java默认提供一个非参数化的构造函数?''

Java模式匹配记录

取消按钮,但没有任何操作方法引发和异常

在for—each循环中的AnimationTimer中的if语句'

如何打印本系列的第n项y=-(1)-(1+2)+(1+2+3)+(1+2+3+4)-(1+2+3+4+5)...Java中的(1+2+3+4...+n)

按属性值从流中筛选出重复项

在VS代码中,如何启用Java Main函数的&Q;Run|DEBUG&Q;代码?

有效的公式或值列表必须少于或等于255个字符

与IntArray相比,ArrayList<;Int>;对于大量元素的性能极差

从LineChart<;字符串、字符串和gt;中删除数据时出现特殊的ClassCastException;

JOLT根据值删除并保留其余的json键

有没有办法在o(log(N))中以系统的方式将数组中的小块元素复制和移动到新增长的数组中的左侧?

将java.util.Date(01.01.0001)转换为java.time.LocalDate将返回29.12.0000

协同 routine 似乎并不比JVM线程占用更少的资源

如果c不为null,Arrays.sort(T[]a,Comparator<;?super T>;c)是否会引发ClassCastException?

如何在单元测试中获得我的装饰Mapstruct映射器的实例?

ControlsFX RangeSlider在方向垂直时滞后

为什么child-pom会创建一个新版本