在我的特定情况下,我正在使用区分列策略。这意味着我的JPA实现(hibernate)创建带有特殊 DTYPE 列的 users 表。此列包含实体的类名称。例如,我的 用户 表可以具有 TrialUser* 和 PayingUser的 子类。这些类名称将在 DTYPE 列中,以便EntityManager从数据库加载实体时,它知道要实例化的类的类型。 *** __
我尝试了两种转换实体类型的方法,但都觉得自己很脏:
#1的问题在于,当您手动更改此列时,JPA不知道如何刷新此实体或将其重新附加到持久性上下文。它需要一个 TrialUser ID为1234,而不是一个 PayingUser ID为1234它失败了。在这里,我可能会执行EntityManager.clear()并分离所有实体/清除Per。上下文,但是由于这是Service Bean,它将为系统的所有用户清除挂起的更改。
#2的问题是,当您删除 TrialUser 时,您设置为Cascade = ALL的所有属性也将被删除。这很糟糕,因为您仅尝试交换其他用户,而不删除所有扩展对象图。
更新1 :#2的问题对我来说几乎使一切都无法使用,因此我已经放弃尝试使其工作。最好的技巧无疑是第一,我在这方面取得了一些进步。关键是首先获取对基础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)
您可以看到 用户 具有一个OneToMany Set 的 Membership 实体引起了一些问题。我对幕后发生的事情了解得还不够多。
更新2 :到目前为止,唯一有效的方法是更改上述代码中所示的DTYPE,然后调用 entityManager.clear()
我不完全了解清除整个持久性上下文的后果,我希望让 Session.evict() 在要更新的特定实体上工作。
所以我终于找到了一个可行的解决方案:
放弃 EntityManager 以更新 DTYPE 。这主要是因为 Query.executeUpdate() 必须在事务中运行。您可以尝试在现有事务中运行它,但这可能与您正在修改的实体的持久性上下文有关。这意味着更新 DTYPE之后, 您必须找到一种方法 evict() 实体。最简单的方法是调用 entityManager.clear(), 但这会导致各种副作用(请参阅JPA规范中的相关内容)。更好的解决方案是获取基础委托(在我的情况下为Hibernate Session )并调用 Session.evict(user) 。这可能适用于简单的域图,但是我的却非常复杂。我一直无法使 @Cascade(CascadeType.EVICT) 与我现有的JPA批注(如 @OneToOne(cascade = CascadeType.ALL) ) 一起正常工作。我还尝试手动将域图传递给 会话, 并让每个父实体逐出其子代。由于未知原因,这也不起作用。
我被留在只有 entityManager.clear() 可以工作的情况下,但是我不能接受副作用。然后,我尝试创建专门用于实体转换的单独的持久性单元。我认为我可以将 clear() 操作本地化到仅负责转换的PC。我设置了一个新的PC,一个新的对应的 EntityManagerFactory ,一个新的Transaction Manager,然后将该事务管理器手动注入到存储库中,以便在与正确的PC对应的事务中手动包装 executeUpdate() 。在这里我不得不说,我对Spring / JPA容器管理的交易还不甚了解,因为它最终成为试图获取本地/手动交易的噩梦。 executeUpdate() 可以很好地处理从Service层拉入的容器管理的事务。
在这一点上,我抛出了所有内容并创建了这个类:
@Transactional(propagation = Propagation.NOT_SUPPORTED) public class JdbcUserConversionRepository implements UserConversionRepository { @Resource private UserService userService; private JdbcTemplate jdbcTemplate; @Override @SuppressWarnings("unchecked") public User convertUserType(final User user, final Class targetClass) { // Update the DTYPE jdbcTemplate.update("update user set user.DTYPE = ? where user.id = ?", new Object[] { targetClass.getSimpleName(), user.getId() }); // Before we try to load our converted User back into the Persistence // Context, we need to remove them from the PC so the EntityManager // doesn't try to load the cached one in the PC. Keep in mind that all // of the child Entities of this User will remain in the PC. This would // normally cause a problem when the PC is flushed, throwing a detached // entity exception. In this specific case, we return a new User // reference which replaces the old one. This means if we just evict the // User, then remove all references to it, the PC will not be able to // drill down into the children and try to persist them. userService.evictUser(user); // Reload the converted User into the Persistence Context return userService.getUserById(user.getId()); } public void setDataSource(final DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } }
我相信此方法可以使它起作用,它包含两个重要部分:
尽管我的初始测试进展顺利,但此解决方案可能仍然存在一些问题。由于它们被发现,我将保持更新。