【问题标题】:Updating only relevant entities in aggregates with @ColumnTransformer使用 @ColumnTransformer 仅更新聚合中的相关实体
【发布时间】:2021-03-11 11:17:23
【问题描述】:

在我们的 Spring Boot 应用程序中,我试图保存一个聚合,它由一个根实体 (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。 当更新子或父时,首先加载父。在服务层中,更新域对象(设置字段,或添加/删除子对象)。 然后用更新后的域对象调用store的merge方法(见代码sn-p)。

这可行,但并不完全如我们所愿。目前,每次更新都会导致父实体和每个子实体被保存,即使所有字段都保持不变。我们添加了@DynamicUpdate 注释。现在我们看到,“修改”字段是问题所在。 我们使用@ColumnTransformer 让数据库设置日期。现在,即使您在不更改任何内容的情况下调用服务更新方法,Hibernate 也会为每个对象生成一个更新查询,它只更新修改后的字段。 最糟糕的是,当每个对象都被保存时,每个修改日期也会更改为当前日期。但我们需要确切了解哪个对象真正发生了变化以及何时发生变化。

有没有办法告诉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,我希望每次发出 INSERT OR UPDATE 时都会在数据库级别生成时间戳。如果我更改 ChildEntity 的字段,然后保存聚合,则会按预期仅为这一数据库行生成更新语句。但是,last_modified_datetime没有更新,这令人惊讶。 不幸的是,这似乎仍然是一个开放的错误。这个问题准确地描述了我的问题:Link

有人可以提供一个解决方案,如何在更新时也执行这个 db 函数(不使用 db 触发器)

【问题讨论】:

    标签: java spring-boot hibernate jpa domain-driven-design


    【解决方案1】:

    您可以尝试在这些字段上使用@org.hibernate.annotations.Generated( GenerationTime.ALWAYS ),并使用数据库触发器或默认表达式来创建值。这样,Hibernate 将永远不会写入该字段,而是在插入/更新后读取它。

    总的来说,这有一些缺点(需要触发器,需要在插入/更新后进行选择),所以我认为这是 Blaze-Persistence Entity Views 的完美用例。

    我创建了该库以允许在 JPA 模型和自定义接口或抽象类定义模型之间轻松映射,例如 Spring Data Projections on steroids。这个想法是您按照自己喜欢的方式定义目标结构(域模型),并通过 JPQL 表达式将属性(getter)映射到实体模型。

    使用 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 Projections 一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

    Page<ParentDomainObject> findAll(Pageable pageable);
    

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

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

    【讨论】:

    • 感谢您的回复,并为迟到的答案感到抱歉:) 我不确定您的示例将如何设置,例如。 last_modified_datetime 列,因为它在界面中丢失。在我看来,我仍然需要一个数据库触发器,对吧?如果不。 Blaze Persistence 可能很适合我们,我们甚至可能在未来使用它。目前我们一定会休眠,我接近解决方案(请参阅我更新的问题)。看来这个解决方案只是因为休眠中的一个开放错误而不起作用
    • 您可以继续使用触发器或使用实体视图更新前生命周期侦听器,该侦听器仅在存在脏属性时调用。我更新了答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-12
    • 1970-01-01
    相关资源
    最近更新 更多