当子对象被更改时,mapstruct和hibernate的父子关系问题

mspsb9vt  于 2023-08-06  发布在  其他
关注(0)|答案(1)|浏览(146)

在使用Jakarta JAX-RS的REST API项目中,我们使用mapstruct将DTOMap到Hibernate实体,并且我们遇到了父子关系的问题。
我们将两个类称为Parent和Child,第一个类与第二个类相关联。
当调用API方法保存Parent时,我们做两件事:

// find the entity in the database
Parent parent = parentRepo.findById(dto.id());
// update the entity with the dto's values
parentMapper.toDb(dto, parent);

字符串
除了DTO更改了引用的子对象(即父对象不像以前那样链接到同一个子对象,而是链接到另一个子对象)之外,一切正常。然后Hibernate将立即抛出一个Exception,只要Map器尝试调用setId()方法:

Caused by: org.hibernate.HibernateException: identifier of an instance of ChildEntity was altered from 123 to 456
at org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor.handleWrite(EnhancementAsProxyLazinessInterceptor.java:265)
at org.hibernate.bytecode.enhance.spi.interceptor.AbstractInterceptor.writeObject(AbstractInterceptor.java:157)
at com.acme.ChildEntity.$$_hibernate_write_id(ChildEntity.java)
at com.acme.BaseEntity.setId(BaseEntity.java:44)
at com.acme.ParentMapperImpl.resourceFileDtoToResourceFile(parentMapperImpl.java:272)
at com.acme.ParentMapperImpl.toDb(parentMapperImpl.java:210)


Hibernate的字节码不允许我们将id更新为新值,这是有意义的。
问题是mapstruct生成的Map器没有任何方法来区分这两种情况:

  • 子对象的属性被更新的情况(id保持不变,但其他属性被更新),在这种情况下,子对象应该保持相同的实体示例,并对其调用一些setProperty()
  • 在这种情况下,更新的是父子关系本身(id不同),在这种情况下,Child应该是数据库先前返回的示例之外的另一个示例

当前,以下Map器声明...

ParentEntity toDb(ParentDto dtoParent, @MappingTarget ParentEntity dbParent);


...生成以下Map器实现:

if ( dtoParent.child() != null ) {
    if ( dbParent.getChild() == null ) {
        dbParent.setChild( new ChildEntity() );
    }
    // this generated method calls every set() methods available
    childDtoToChildDb( dtoParent.child(), dbParent.getChild() );
}
else {
    dbParent.setChild( null );
}


它缺少类似于:

// check if the child is the same or another one
if (dbParent.getChild().getId().equals(dtoParent.child().id()) { ... }


一种可能性是告诉ParentMapper始终使用ChildMapper:

// declaration
@Mapper(... uses = {ChildMapper.class})
// implementation, replacing the whole previous if/else
dbParent.setChild( childMapper.toDb( dtoParent.child() ) );


在这种情况下,Map器总是用一个新的子实体替换子实体,但是它挑战我们在之后重新附加实体,并且它似乎倾向于其他警告(Map器总是创建一个新的子实体,而不考虑数据库中的内容)。
我想没有办法处理生成的Map器的所有内容,因为它应该能够在child替换的情况下调用database,但也许有一种方法可以自定义mapstruct或以更智能的方式调用其Map器。也许我们应该让mapstruct只将简单的DTOMap到简单的实体,并自己处理实体到实体/DTO到DTO的关系?

b4qexyjb

b4qexyjb1#

我不知道这是不是更好的方法,但我们找到了一个似乎很成功的方法,如果有人需要,我花几分钟把它写在这里。
基本思想是DTO应该总是更新一个真正的DB实体,只更新它知道的字段,只覆盖Hibernate实体中包含的部分DB状态。这样,我们可以确保实体只在DTO范围内被修改。
否则可能会导致问题,例如一个全新的对象只被部分填充,hVersion过期,问题中提到的id冲突。
要做到这一点,我们有三个部分:

#1Map器的ObjectFactory:

@ApplicationScoped
public class AppMapperDatabaseHandler {

    @PersistenceContext
    private EntityManager em;

    /**
     * Loads database entity from any type from the DTO id.
     * EntityDto is just a contract with id() method.
     */
    @ObjectFactory
    public <E extends BaseEntity> E load(@TargetType @NotNull Class<E> type, @NotNull EntityDto dto) {
        return em.find(type, dto.id());
    }

    /**
     * Same as before, but if the entity is not in the database, uses a Supplier to produces the entity (should come from the business layer).
     * @DatabaseEntityMayBeNew is a custom annotation made to guide mapstruct to know which methods to use.
     */
    @ObjectFactory
    @DatabaseEntityMayBeNew
    public <E extends BaseEntity> E loadOrSupply(@TargetType @NotNull Class<E> type, @NotNull EntityDto dto, @NotNull Supplier<E> supplier) {
        E entity = em.find(type, dto.id());
        return entity != null ? entity : supplier.get();
    }

    /**
     * Merge/persist at the end of the mapping if entity can be new, to ensure it's not transient.
     */
    @AfterMapping
    @DatabaseEntityMayBeNew
    public <E extends BaseEntity> E merge(EntityDto dto, Supplier<E> producer, @MappingTarget E entity) {
        return em.merge(entity);
    }
}

字符串

#2 MapperConfig中的原型定义:

/**
 * Prototype to all "toDb" method, applying the DatabaseEntityMayBeNew qualifier when there is a supplier. This method is never called nor implemented by itself, it's just a mapstruct contract.
 */
@BeanMapping(qualifiedBy = DatabaseEntityMayBeNew.class)
BaseEntity genericToDb(EntityDto dto, Supplier<? extends BaseEntity> producer);

#3每个DTO/DB对的几个Map器方法:

// case where the DTO can be mapped to a new entity
Parent toDb(ParentEditDto dto, Supplier<Parent> producer);

// case where the DTO is mapped to an already persisted entity
Parent toDb(ParentRightsDto dto);


以下是可以使用这些方法的两种情况:

// in a save REST method, where we use a business handler to create a new entity if needed
Parent parent = parentMapper.toDb(dto, () -> parentHandler.create());
// in another REST method, where the entity is already known
Parent parent = parentMapper.toDb(dto);


为了显示结果,下面是由mapstruct生成的方法:

@Override
public Parent toDb(ParentEditDto dto, Supplier<Parent> producer) {
    // ... check params...
    Parent parent = appMapperDatabaseHandler.loadOrSupply( Parent.class, dto, producer );
    // ... call all set()...
    // as you can see, the child entity also uses a toDb() method
    parent.setChild( childMapper.toDb( dto.child() ) );
    // this method contains the following, same as here:
    // Child child = appMapperDatabaseHandler.load( Child.class, dto );
    Parent target = appMapperDatabaseHandler.merge( dto, producer, parent );
    // ...
}

@Override
public Parent toDb(ParentRightsDto dto) {
    // ... check params...
    Parent parent = appMapperDatabaseHandler.load( Parent.class, dto );
    // ... call all set()...
    // ...
}

相关问题