【发布时间】: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