在我们的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触发器)
1条答案
按热度按时间llycmphe1#
您可以尝试在这些字段上使用
@org.hibernate.annotations.Generated( GenerationTime.ALWAYS )
,并使用数据库触发器或默认表达式来创建值。这样,Hibernate永远不会写入字段,而是在插入/更新后读取它。总的来说,这有一些缺点(需要触发器,在插入/更新后需要选择),所以我认为这是Blaze-Persistence实体视图的完美用例。
我创建了这个库,以允许JPA模型和自定义接口或抽象类定义模型之间的轻松Map,就像类固醇上的Spring Data Projections一样。其思想是,您可以按照自己喜欢的方式定义目标结构(域模型),并通过JPQL表达式将属性(getter)Map到实体模型。
使用Blaze-Persistence Entity-Views,您的用例的DTO/域模型可能如下所示:
查询是将实体视图应用于查询的问题,最简单的就是按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
最好的部分是,它只会获取实际需要的状态!它还支持以高效的方式写/Map回持久性模型。由于它会为您执行脏跟踪,所以它只会在对象实际上脏时刷新更改。