我们正在开发 headless CMS,并且我们有一个庞大的系统,其中包含一些我们称为 EntityItems/ContentItems
的对象。这些基本上是数据库中可以由用户建模的实体。他们采用 JSON 风格的方法,其中这些模型可以包含数字、字符串等。这意味着这些项目是我们系统的核心,在大型项目中可以大量使用。
@Entity
@Getter
@Setter
@EqualsAndHashCode(callSuper = true, exclude = {"children", "parent"})
@NoArgsConstructor
@Table(indexes = {
@Index(name = "entityitem_objectState", columnList = "objectState"),
@Index(name = "entityitem_appscope", columnList = "appScope"),
@Index(name = "entityitem_type_id", columnList = "type_id")})
public class EntityItem extends AbstractContentItem {
private static final long serialVersionUID = 1L;
@ManyToOne(fetch = FetchType.EAGER)
private EntityItemType type;
....
@Convert(converter = ObjectMapToJsonConverter.class)
@Column(name = "itemProperties", columnDefinition = "jsonb")
private Map<String, String> itemPropertiesJson;
}
我们还有一个基于事件的系统,我们为用户提供对删除、创建和更新此类 EntityItems/ContentItems
的 react 。在其中一个事件中,我们有一个名为 changedProperties
的属性,用于记录此事件中发生的更改。
public class ContentItemChangedEvent {
private final long itemId;
private final List<String> changedProperties;
}
最后,我们有一个可与此类 EntityItems
配合使用的 EntityItemServiceImpl
。它有一个容器管理的实体管理器和一个 @Transactional
保存方法。
@Service
@Log4j
@Validated
public class EntityItemServiceImpl {
@Autowired
private EntityItemRepository entityItemRepository;
@Autowired
@Qualifier("defaultEntityManager")
private EntityManager entityManager;
//...
}
entityItemRepository
是一个基本的 Jpa/Crud 存储库。
public interface EntityItemRepository extends CrudRepository<EntityItem, Long>, JpaRepository<EntityItem, Long>, JpaSpecificationExecutor<EntityItem> {
....
}
当调用 save
方法时,我们希望接收该项目,获取其 JSON 属性并将它们与数据库中当前未提交的属性进行比较。
@Transactional
public synchronized EntityItem save(EntityItem item, ...) {
...
if (item.getId() != null) {
var oldItem = this.entityItemRepository.findById(item.getId()).orElse(null);
var changedProperties = this.getChangedProperties(item, oldItem);
}
...
this.entityItemRepository.save(item);
...
}
现在讨论这个问题:
有时保存项目时,由于它位于同一事务中,它已经由 EntityManager 管理,因此一级缓存启动并始终返回该项目的相同实例。简而言之,当调用 this.entityItemRepository.findById(item.getId()) 时,会给出与作为参数传递的实例相同的实例(由于一级缓存,如上所述)。
为了解决这个问题,我们尝试了以下方法,引入了另一种名为 fetchOldVersionOfItem(EntityItem item)
的方法,该方法现在如下所示:
@Transactional
public synchronized EntityItem save(EntityItem item, ...) {
...
if (item.getId() != null) {
var oldItem = this.entityItemRepository.fetchOldVersionOfItem(item).orElse(null);
var changedProperties = this.getChangedProperties(item, oldItem);
}
...
this.entityItemRepository.save(item);
...
}
我们尝试了以下实现,但由于某种原因都失败了: 1)
private Optional<EntityItem> fetchOldVersionOfItem(EntityItem item) {
this.entityManager.clear();
var oldItemOpt = this.findById(item.getId());
this.entityManager.clear();
return oldItemOpt;
}
此处,两次保存发生得非常快(因为这是 CMS 中的核心功能),并导致行已更新或删除
错误。用通常的 findById
替换此代码不再产生此错误。
2)
private Optional<EntityItem> fetchOldVersionOfItem(EntityItem item) {
if (this.entityManager.contains(item)) {
this.entityManager.detach(item);
var oldItemOpt = this.findById(item.getId());
oldItemOpt.ifPresent(oldItem -> this.entityManager.detach(oldItem));
this.entityManager.merge(item);
return oldItemOpt;
}
return findById(item.getId());
}
由于一些连续的保存,此代码产生乐观锁异常。删除它不再产生错误。
3)
private Optional<EntityItem> fetchOldVersionOfItem(EntityItem item) {
if (this.entityManager.contains(item)) {
this.entityManager.detach(item);
var oldItemOpt = this.findById(item.getId());
oldItemOpt.ifPresent(oldItem -> this.entityManager.detach(oldItem));
this.entityManager.merge(item);
return oldItemOpt;
}
var oldItemOpt = findById(item.getId());
oldItemOpt.ifPresent(oldItem -> this.entityManager.detach(oldItem));
return oldItemOpt;
}
这里有与上面相同的问题。
现在问问题: 这两种解决方案我们做错了什么?做这样的事情的最佳方法是什么?
请注意,我们测试了给定解决方案的不同变体(引入和删除 .contains
检查等),结果是相同的。此外,系统的此功能将承受很大的压力,因为基本上我们项目中发生的所有事情都与以某种方式保存项目有关。
感谢您的宝贵时间!
最佳答案
将旧状态保存在 transient 字段(不由 jpa 管理)中可能是实现此功能的好方法,因为它还避免了额外的数据库往返的需要。
你可以例如在“@PostLoad”回调中克隆原始 map 内容(不仅仅是引用),类似于此处对完整实体所做的操作: Getting object field previous value hibernate JPA
关于java - JPA 清除缓存以读取一项,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70386486/