一尘不染

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

hibernate

在我的特定情况下,我正在使用区分列策略。这意味着我的JPA实现(hibernate)创建带有特殊 DTYPE 列的 users
表。此列包含实体的类名称。例如,我的 用户 表可以具有 TrialUser*PayingUser的 子类。这些类名称将在
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的问题对我来说几乎使一切都无法使用,因此我已经放弃尝试使其工作。最好的技巧无疑是第一,我在这方面取得了一些进步。关键是首先获取对基础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() 在要更新的特定实体上工作。


阅读 436

收藏
2020-06-20

共1个答案

一尘不染

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

放弃 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);
    }
}

我相信此方法可以使它起作用,它包含两个重要部分:

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

尽管我的初始测试进展顺利,但此解决方案可能仍然存在一些问题。由于它们被发现,我将保持更新。

2020-06-20