在我的具体情况下,我正在使用鉴别器列策略。这意味着我的 JPA 实现(Hibernate)创建了一个带有特殊DTYPE列的用户表。此列包含实体的类名。例如,我的users表可以有TrialUser和PayingUser的子类。这些类名将在DTYPE列中,以便 EntityManager 从数据库加载实体时,它知道要实例化哪种类型的类。
我尝试了两种转换实体类型的方法,都感觉像肮脏的黑客:
- 使用本机查询手动对列执行更新,更改其值。这适用于属性约束相似的实体。
- 创建目标类型的新实体,执行BeanUtils.copyProperties()调用以移动属性,保存新实体,然后调用命名查询,该查询手动将新 Id 替换为旧 Id,以便所有外键约束得到维护。
#1 的问题是,当您手动更改此列时,JPA 不知道如何将此实体刷新/重新附加到持久性上下文。它需要一个ID 为 1234 的TrialUser,而不是一个 ID 为 1234 的PayingUser。它失败了。在这里,我可能会做一个 EntityManager.clear() 并分离所有实体/清除 Per。上下文,但由于这是一个服务 bean,它会擦除系统所有用户的未决更改。
#2 的问题是,当您删除TrialUser时,您设置为 Cascade=ALL 的所有属性也将被删除。这很糟糕,因为您只是试图交换不同的用户,而不是删除所有扩展对象图。
更新 1:#2 的问题使我几乎无法使用它,所以我放弃了尝试让它工作。更优雅的 hack 绝对是 #1,我在这方面取得了一些进展。关键是首先获取对底层 Hibernate Session 的引用(如果您使用 Hibernate 作为 JPA 实现)并调用 Session.evict(user) 方法从持久性上下文中仅删除该单个对象。不幸的是,没有纯粹的 JPA 支持。这是一些示例代码:
// Make sure we save any pending changes
user = saveUser(user);
// Remove the User instance from the persistence context
final Session session = (Session) entityManager.getDelegate();
session.evict(user);
// Update the DTYPE
final String sqlString = "update user set user.DTYPE = '" + targetClass.getSimpleName() + "' where user.id = :id";
final Query query = entityManager.createNativeQuery(sqlString);
query.setParameter("id", user.getId());
query.executeUpdate();
entityManager.flush(); // *** PROBLEM HERE ***
// Load the User with its new type
return getUserById(userId);
请注意引发此异常的手动flush() :
org.hibernate.PersistentObjectException: detached entity passed to persist: com.myapp.domain.Membership
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
at org.hibernate.impl.SessionImpl.firePersistOnFlush(SessionImpl.java:671)
at org.hibernate.impl.SessionImpl.persistOnFlush(SessionImpl.java:663)
at org.hibernate.engine.CascadingAction$9.cascade(CascadingAction.java:346)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:291)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:319)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:265)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:242)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
at org.hibernate.engine.Cascade.cascade(Cascade.java:153)
at org.hibernate.event.def.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:154)
at org.hibernate.event.def.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:145)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:88)
at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58)
at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:996)
at org.hibernate.impl.SessionImpl.executeNativeUpdate(SessionImpl.java:1185)
at org.hibernate.impl.SQLQueryImpl.executeUpdate(SQLQueryImpl.java:357)
at org.hibernate.ejb.QueryImpl.executeUpdate(QueryImpl.java:51)
at com.myapp.repository.user.JpaUserRepository.convertUserType(JpaUserRepository.java:107)
您可以看到User具有 OneToMany 集的Membership实体导致了一些问题。我对幕后发生的事情知之甚少,无法破解这个坚果。
更新 2:到目前为止唯一有效的方法是更改 DTYPE,如上面的代码所示,然后调用entityManager.clear()
我不完全理解清除整个持久性上下文的后果,我希望让Session.evict()处理正在更新的特定实体。