为什么一个JPA实体抱怨修改了标识符,而另一个却没有?

dbf7pr2w  于 8个月前  发布在  其他
关注(0)|答案(1)|浏览(79)

我正在使用Springboot开发授权微服务。我有权限、角色和用户的概念,每一个都表示为一个@Entity类,并有相应的JpaRepository
每个@Entity类都有一个@IdClass,它由一个字符串ID和一个时间戳组成(该字段称为createdAt)。其思想是,对其中一个实体对象的任何修改都将在数据库中创建一个新行。我无法修改此数据库架构。我遇到了在更新时修改ID(时间戳部分)抛出HibernateExeption的问题。我的解决方案是使用@PrePersist来修改时间戳。

@PrePersist
  private void updateTimestamp() {
    createdAt = Instant.now();
  }

这似乎对我的权限实体起作用。然而,当我测试更新我的角色实体时,我现在得到以下错误

org.hibernate.HibernateException: identifier of an instance of com.example.auth.model.Role was altered from RoleId(roleId=TEST_ROLE_R2, createdAt=2023-09-04T21:52:39.860899Z) to RoleId(roleId=TEST_ROLE_R2, createdAt=2023-08-31T23:12:25.927743Z)

用同样的@PrePersist方法
另一件奇怪的事情是,异常似乎是在访问权限存储库时抛出的。更新/创建角色时,将引用关联权限的信息。
在引发异常的

permissionRepository.findMostRecentPermissionById(p)

findMostRecentPermisisonById()

@Query(
        value = "SELECT * FROM auth.permissions WHERE permissionID = :permissionID ORDER BY createdat DESC LIMIT 1",
        nativeQuery = true
    )
    Permission findMostRecentPermissionById(String permissionID);

我根本不确定是什么导致了异常的抛出,我特别困惑的是为什么在这里抛出它。在代码的其他部分使用findMostRecentPermissionById方法时不会导致任何问题。任何见解或建议将不胜感激。

更新

我仍然不明白为什么异常会在原来的位置抛出。但是,使用entityManager.detach(entity)允许我手动设置时间戳字段并添加一个新实体。

kgqe7b3p

kgqe7b3p1#

我不确定您的模式设计的正确性-通常这种愿望(即,维护一组对象版本并主要查询最新版本)是通过更复杂的方法实现的,例如:

object_id, // unique identifier of version
chronicle_id, // identifier of root version
antecedent_id, // identifier of previous version
latest_flag // denotes the latest version

但是,Hibernate不支持更改实体标识符,这在documentation中有明确说明:
Hibernate和JPA都对相应的数据库列做了以下假设:

  • UNIQUE -值必须唯一标识每一行。
  • NOT NULL -值不能为空。对于复合ID,任何部分都不能为空。
  • IMMUTABLE -值一旦插入,就永远不能更改。这更像是一个一般性的指南,而不是一个严格的规则,因为意见各不相同。JPA定义了将identifier属性的值更改为undefined的行为;Hibernate不支持。如果您选择的PK值将被更新,Hibernate建议将可变值Map为自然id,并为PK使用代理id。参见第7章,自然身份。

从技术上讲,上述内容适用于数据库中的列,但同样适用于实体类。

回答最初的问题“为什么它有时有效,有时无效?“

实际行为取决于持久化上下文是否知道要修改的实体,如果持久化上下文没有关于要修改的实体的信息,它认为该实体是新的,一切都“按预期”工作,否则抛出异常,例如:

class EntityService {

  private EntityRepo entityRepo;

  // this throws exception
  // because spring maintains single session (persistence context)
  // per transaction, so both  entityRepo.findById and entityRepo.save
  // calls share the same instance of persistence context

  @Transactional
  void save1() {
     Entity entity = entityRepo.findById(...);
     entity.updateTimestamp();
     entityRepo.save(entity);
  }

  // this method does not throw exception
  // because entityRepo.findById and entityRepo.save calls
  // do not share the same instance of persistence context

  // @Transactional (propagation = Propagation.REQUIRES_NEW)
  void save2() {
     Entity entity = entityRepo.findById(...);
     entity.updateTimestamp();
     entityRepo.save(entity);
  }

}

相关问题