如何在JPA中更改实体类型?

Rob*_*ell 10 hibernate jpa transactions entitymanager

在我的具体案例中,我正在使用一个鉴别器列策略.这意味着我的JPA实现(Hibernate)创建了一个带有特殊DTYPE列的用户表.此列包含实体的类名.例如,我的users表可以包含TrialUserPayingUser的子类.这些类名称将位于DTYPE列中,以便当EntityManager从数据库加载实体时,它知道要实例化的类的类型.

我尝试过两种转换实体类型的方法,两种方式都像脏黑客一样:

  1. 使用本机查询手动对列执行UPDATE,更改其值.这适用于属性约束相似的实体.
  2. 创建目标类型的新实体,执行BeanUtils.copyProperties()调用以移动属性,保存新实体,然后调用命名查询,该查询手动将旧Id替换为旧Id,以便所有外键约束维持.

#1的问题是当您手动更改此列时,JPA不知道如何刷新/重新附加此实体到持久性上下文.它需要一个TrialUser ID为1234,而不是一个PayingUser ID为1234它失败了.在这里,我可以做一个EntityManager.clear()并分离所有实体/清除Per.上下文,但由于这是一个Service bean,它将擦除系统所有用户的挂起更改.

#2的问题在于,当您删除TrialUser时,您设置为Cascade = ALL的所有属性也将被删除.这很糟糕,因为您只是尝试交换其他用户,而不是删除所有扩展对象图.

更新1:#2的问题使我几乎无法使用,所以我放弃了尝试让它工作.更优雅的黑客肯定是#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); 
Run Code Online (Sandbox Code Playgroud)

注意手动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)
Run Code Online (Sandbox Code Playgroud)

您可以看到,其中User具有OneToMany集的Membership实体正在导致一些问题.我不知道幕后发生了什么事情来破解这个坚果.

更新2:到目前为止唯一有效的方法是更改​​DTYPE,如上面的代码所示,然后调用 entityManager.clear()

我并不完全理解清除整个持久化上下文的后果,我本来希望让Session.evict()处理正在更新的特定实体.

Rob*_*ell 3

所以我终于找到了一个可行的解决方案:

放弃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和一个新的事务管理器,并手动将该事务管理器注入到存储库中,以便在与正确的 PC 对应的事务中手动包装executeUpdate() 。在这里,我不得不说,我对 Spring/JPA 容器管理事务了解不够,因为试图让executeUpdate()的本地/手动事务与容器管理事务很好地配合,这最终成为一场噩梦从服务层。

此时我扔掉了所有东西并创建了这个类:

@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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我认为该方法有两个重要部分使其发挥作用:

  1. 我已经用@Transactional(propagation = Propagation.NOT_SUPPORTED)对其进行了标记 ,它应该暂停来自服务层的容器管理事务,并允许在 PC 外部进行转换。
  2. 在尝试将转换后的实体重新加载回 PC 之前,我使用userService.evictUser(user);逐出当前存储在 PC 中的旧副本。。其代码只是获取Session实例并调用evict(user)。有关更多详细信息,请参阅代码中的注释,但基本上,如果我们不这样做,则任何对getUser的调用都会尝试返回仍在 PC 中的缓存实体,但它会抛出有关类型不同的错误。

虽然我最初的测试进展顺利,但这个解决方案可能仍然存在一些问题。当他们被发现时,我会及时更新。