jpa 使用@ColumnTransformer仅更新聚合中的相关实体

chhkpiq4  于 2023-06-23  发布在  其他
关注(0)|答案(1)|浏览(196)

在我们的springboot应用程序中,我试图保存一个聚合,它包含一个根实体(ParentEntity)和一组子实体(ChildEntity)。其意图是,所有操作都通过聚合完成。因此,不需要为ChildEntity创建存储库,因为ParentEntity应该管理所有的保存或更新操作。这就是实体的样子:

@Entity
@Table(name = "tab_parent", schema = "test")
public class ParentEntity implements Serializable {
    
    @Id
    @Column(name = "parent_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer parentId;
    
    @Column(name = "description")
    private String description;
    
    @Column(name = "created_datetime", updatable = false, nullable = false)
    @ColumnTransformer(write = "COALESCE(?,CURRENT_TIMESTAMP)")
    private OffsetDateTime created;
    
    @Column(name = "last_modified_datetime", nullable = false)
    @ColumnTransformer(write = "COALESCE(CURRENT_TIMESTAMP,?)")
    private OffsetDateTime modified;
    
    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "ParentEntity")
    private Set<ChildEntity> children;
    
    // constructor and other getters and setters
        
    public void setChildren(final Set<ChildEntity> children) {
        this.children = new HashSet<>(children.size());
        for (final ChildEntity child : children) {
            this.addChild(child);
        }
    }
    
    public ParentEntity addChild(final ChildEntity child) {
        this.children.add(child);
        child.setParent(this);
        return this;
    }
    
    public ParentEntity removeChild(final ChildEntity child) {
        this.children.add(child);
        child.setParent(null);
        return this;
    }
    
}

@Entity
@DynamicUpdate
@Table(name = "tab_child", schema = "test")
public class ChildEntity implements Serializable {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "child_id")
    private Integer childId;
    
    @Column(name = "language_id")
    private String languageId;
    
    @Column(name = "text")
    private String text;
    
    @Column(name = "created_datetime", updatable = false, nullable = false)
    @ColumnTransformer(write = "COALESCE(?,CURRENT_TIMESTAMP)")
    public OffsetDateTime created;
    
    @Column(name = "last_modified_datetime", nullable = false)
    @ColumnTransformer(write = "COALESCE(CURRENT_TIMESTAMP,?)")
    public OffsetDateTime modified;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id", updatable = false)
    private ParentEntity parent;
    
    // constructor and other getters and setters    
    
    public ParentEntity getParent() {
        return this.parent;
    }
    
    public void setParent(final ParentEntity parent) {
        this.parent = parent;
    }
    
}

这是保存或更新实体的store方法:

public Integer merge(final ParentDomainObject parentDomainObject) {
    final ParentEntity parentEntity =
            this.mapper.toParentEntity(parentDomainObject);
    final ParentEntity result = this.entityManager.merge(parentEntity);
    this.entityManager.flush();
    return result.getParentId();
}

这是通过id检索聚合的store方法:

public Optional<ParentDomainObject> findById(final Integer id) {
    return this.repo.findById(id).map(this.mapper::toParentDomainObject);
}

正如您所看到的,我们的架构严格地将存储层与服务层分开。因此,服务只知道域对象,根本不依赖Hibernate Entites。更新子级或父级时,首先加载父级。在服务层中,域对象被更新(设置字段,或者添加/删除子对象)。然后,使用更新后的域对象调用存储区的merge方法(参见代码片段)。
这是可行的,但并不完全像我们想要的那样。目前,每次更新都会导致父实体和每个chhild实体被保存,即使所有字段保持不变。我们添加了@DynamicUpdate注解。现在我们看到,“修改”字段是问题所在。我们使用@ColumnTransformer让数据库设置日期。现在,即使您调用服务更新方法而不做任何更改,Hibernate也会为EVERY对象生成一个更新查询,该查询只更新修改后的字段。最糟糕的是,当每个对象都被保存时,每个修改的日期也被更改为当前日期。但我们需要确切的信息,知道哪个物体真正改变了,什么时候改变了。
有没有什么方法可以告诉hibernate,在决定更新什么时,不应该考虑这个专栏。然而,当然,如果字段改变了,则更新操作应该确实更新修改的字段。

    • 更新:**

在@Christian Beikov提到@org.hibernate.annotations.Generated( GenerationTime.ALWAYS )之后,我的第二个方法如下:
我没有使用@Generated(它使用@ValueGenerationType( generatedBy = GeneratedValueGeneration.class )),而是创建了自己的注解,它使用自定义的AnnotationValueGeneration实现:

@ValueGenerationType(generatedBy = CreatedTimestampGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface InDbCreatedTimestamp {
}
public class CreatedTimestampGeneration
        implements AnnotationValueGeneration<InDbCreatedTimestamp> {
    
    @Override
    public void initialize(final InDbCreatedTimestamp annotation, final Class<?> propertyType) {
    }
    
    @Override
    public GenerationTiming getGenerationTiming() {
        return GenerationTiming.INSERT;
    }
    
    @Override
    public ValueGenerator<?> getValueGenerator() {
        return null;
    }
    
    @Override
    public boolean referenceColumnInSql() {
        return true;
    }
    
    @Override
    public String getDatabaseGeneratedReferencedColumnValue() {
        return "current_timestamp";
    }
}
@ValueGenerationType(generatedBy = ModifiedTimestampGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface InDbModifiedTimestamp {
}
public class ModifiedTimestampGeneration
        implements AnnotationValueGeneration<InDbModifiedTimestamp> {
    
    @Override
    public void initialize(final InDbModifiedTimestamp annotation, final Class<?> propertyType) {
    }
    
    @Override
    public GenerationTiming getGenerationTiming() {
        return GenerationTiming.ALWAYS;
    }
    
    @Override
    public ValueGenerator<?> getValueGenerator() {
        return null;
    }
    
    @Override
    public boolean referenceColumnInSql() {
        return true;
    }
    
    @Override
    public String getDatabaseGeneratedReferencedColumnValue() {
        return "current_timestamp";
    }
}

我现在在实体中使用这些注解而不是@ColumnTransformer注解。当我通过addChild()插入一个新的ChildEntity时,这可以完美地工作,因为现在不是所有聚合实体的所有时间戳都更新了。现在只设置新子项的时间戳。换句话说,InDbCreatedTimestamp按照它应该的方式工作。
遗憾的是,InDbModifiedTimestamp没有。由于GenerationTiming.ALWAYS,我希望每次发出INSERTOR UPDATE时,时间戳都会在数据库级别生成。如果我更改了ChildEntity的一个字段,然后保存了聚合,则如预期的那样,只为这一个数据库行生成一个update语句。然而,last_modified_datetime更新,这令人惊讶。不幸的是,这似乎仍然是一个开放的bug。这个问题准确地描述了我的问题:Link
有人能提供一个解决方案,如何让这个DB函数也在update上执行(不使用DB触发器)

llycmphe

llycmphe1#

您可以尝试在这些字段上使用@org.hibernate.annotations.Generated( GenerationTime.ALWAYS ),并使用数据库触发器或默认表达式来创建值。这样,Hibernate永远不会写入字段,而是在插入/更新后读取它。
总的来说,这有一些缺点(需要触发器,在插入/更新后需要选择),所以我认为这是Blaze-Persistence实体视图的完美用例。
我创建了这个库,以允许JPA模型和自定义接口或抽象类定义模型之间的轻松Map,就像类固醇上的Spring Data Projections一样。其思想是,您可以按照自己喜欢的方式定义目标结构(域模型),并通过JPQL表达式将属性(getter)Map到实体模型。
使用Blaze-Persistence Entity-Views,您的用例的DTO/域模型可能如下所示:

@EntityView(ParentEntity.class)
@UpdatableEntityView
public interface ParentDomainObject {
    @IdMapping
    Integer getParentId();
    OffsetDateTime getModified();
    void setModified(OffsetDateTime modified);
    String getDescription();
    void setDescription(String description);
    Set<ChildDomainObject> getChildren();

    @PreUpdate
    default preUpdate() {
        setModified(OffsetDateTime.now());
    }

    @EntityView(ChildEntity.class)
    @UpdatableEntityView
    interface ChildDomainObject {
        @IdMapping
        Integer getChildId();
        String getName();
    }
}

查询是将实体视图应用于查询的问题,最简单的就是按id查询。
ParentDomainObject a = entityViewManager.find(entityManager, ParentDomainObject.class, id);
Spring Data集成允许您几乎像Spring Data Projects一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<ParentDomainObject> findAll(Pageable pageable);

最好的部分是,它只会获取实际需要的状态!它还支持以高效的方式写/Map回持久性模型。由于它会为您执行脏跟踪,所以它只会在对象实际上脏时刷新更改。

public Integer merge(final ParentDomainObject parentDomainObject) {
    this.entityViewManager.save(this.entityManager, parentDomainObject);
    this.entityManager.flush();
    return parentDomainObject.getParentId();
}

相关问题