【问题标题】:How to change Entity type in JPA?如何更改 JPA 中的实体类型?
【发布时间】:2010-10-19 04:11:00
【问题描述】:

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

我尝试了两种转换 Entity 类型的方法,都感觉像肮脏的 hack:

  1. 使用本机查询手动对列执行更新,更改其值。这适用于属性约束相似的实体。
  2. 创建目标类型的新实体,执行 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)

您可以看到 Membership 实体(其中 User 具有 OneToMany 集)导致了一些问题。我对幕后发生的事情了解得不够多,无法破解这个坚果。

更新 2:目前唯一可行的方法是更改​​ DTYPE,如上述代码所示,然后调用 entityManager.clear()强>

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

【问题讨论】:

    标签: hibernate jpa transactions entitymanager


    【解决方案1】:

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

    放弃 EntityManager 以更新 DTYPE。这主要是因为 Query.executeUpdate() 必须在事务中运行。您可以尝试在现有事务中运行它,但这可能与您正在修改的实体的相同持久性上下文相关联。这意味着在您更新 DTYPE 之后,您必须找到一种方法来 evict() 实体。最简单的方法是调用 entityManager.clear() 但这会导致各种副作用(请参阅 JPA 规范)。更好的解决方案是获取底层委托(在我的例子中,一个 Hibernate Session)并调用 Session.evict(user)。这可能适用于简单的域图,但我的非常复杂。我一直无法让 @Cascade(CascadeType.EVICT) 与我现有的 JPA 注释一起正常工作,例如 @OneToOne(cascade = CascadeType.ALL)。我还尝试手动将我的域图传递给 Session 并让每个父实体驱逐其子实体。由于未知原因,这也不起作用。

    我处于只有 entityManager.clear() 可以工作的情况,但我无法接受副作用。然后我尝试为实体转换创建一个单独的持久性单元。我想我可以将 clear() 操作本地化到只负责转换的那台 PC。我设置了一台新PC,一个新的对应EntityManagerFactory,一个新的事务管理器,并手动将这个事务管理器注入到存储库中以手动包装executeUpdate()在对应于适当 PC 的事务中。在这里我不得不说我对 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);
        }
    }
    

    我相信这个方法有两个重要的部分可以让它发挥作用:

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

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

    【讨论】:

      【解决方案2】:

      您真的需要在 User 类型中表示订阅级别吗?为什么不在系统中添加订阅类型?

      然后,该关系将表示为:用户有一个订阅。如果您希望保留订阅历史记录,也可以将其建模为一对多关系。

      然后,当您想要更改用户的订阅级别时,您将创建一个新订阅并将其分配给用户,而不是实例化一个新用户。

      // When you user signs up
      User.setSubscription(new FreeSubscription());
      
      // When he upgrades to a paying account
      User.setSubscription(new PayingSubscription());
      

      试用用户与付费用户真的有那么大的不同吗?可能不是。聚合而不是继承会更好。

      订阅数据可以存储在单独的表中(映射为实体),也可以存储在用户表中(映射为组件)。

      【讨论】:

      • 我同意,但也许我选择了一个坏例子。我的现实生活情况更复杂:用户超类,具有 FamilyHead、FamilyMember、Individual、Pro 子类等的 Customer 子类以及具有 Staff、Admin 等子类的 Employee 子类。它们确实具有我不想提取的功能
      【解决方案3】:

      这个问题与 Java 的关系比其他任何事情都多。您不能更改实例的运行时类型(在运行时),因此 hibernate 不提供这种场景(例如不像 rails)。

      如果您想从会话中逐出用户,则必须逐出关联的实体。你有一些选择:

      • 使用 evict=cascade 映射实体(请参阅Session#evict javadoc)
      • 手动驱逐所有关联实体(这可能很麻烦)
      • 清除会话,从而驱逐所有实体(当然,您会丢失本地会话缓存)

      我在 grails 中遇到过类似的问题,我的解决方案类似于 grigory 的 解决方案。我复制所有实例字段,包括关联的实体,然后删除旧实体并写入新实体。如果您没有那么多关系,这很容易,但如果您的数据模型很复杂,则最好使用您描述的本机 sql 解决方案。

      这里是来源,如果你感兴趣的话:

      def convertToPacient = {
          withPerson(params.id) {person ->
            def pacient = new Pacient()
            pacient.properties = person.properties
            pacient.id = null
            pacient.processNumber = params.processNumber
            def ap = new Appointment(params)
            pacient.addToAppointments(ap);
      
            Person.withTransaction {tx ->
              if (pacient.validate() && !pacient.hasErrors()) {
      
                //to avoid the "Found two representations of same collection" error
                //pacient.attachments = new HashSet(person.attachments);
                //pacient.memberships = new HashSet(person.memberships);
                def groups = person?.memberships?.collect {m -> Group.get(m.group.id)}
                def attachs = []
                person.attachments.each {a ->
                  def att = new Attachment()
                  att.properties = a.properties
                  attachs << att
                }
      
                //need an in in order to add the person to a group
                person.delete(flush: true)
                pacient.save(flush: true)
                groups.each {g -> pacient.addToGroup(g)};
                attachs.each {a -> pacient.addToAttachments(a)}
                //pacient.attachments.each {att -> att.id = null; att.version = null; att.person = pacient};
      
                if (!pacient.save()) {
                  tx.setRollbackOnly()
                  return
                }
              }
            }
      

      【讨论】:

      • 我真的是这个答案。首先,我尝试了您的第三个选项:使用 EntityManager.clear() 清除会话。这行得通。问题是我会丢失持久性上下文中其他地方的任何未提交的更改。现在,我通过将 @Cascade(value = CascadeType.EVICT) 添加到导致问题的嵌套实体来解决第一个建议。但是,这不起作用,我仍然得到相同的错误(分离实体传递给持久化)。我将 Hibernate 标记放在 JPA 标记之上。这可能就是原因。
      • 我还尝试了第二个选项,方法是将 Session 传递给 User 对象,该对象将驱逐其子实体,将其传递给它们以便它们可以执行相同的操作,等等。没有任何效果。所有三种策略在同一个子实体上都以相同的错误结束:“分离的实体传递给持久化”
      【解决方案4】:

      这个问题更多地与Java有关 比什么都重要。你不能改变一个 实例的运行时类型(在 运行时),所以休眠不会 提供这种场景 (例如不像铁轨)。

      我遇到这个帖子是因为我(或认为我)遇到了类似的问题。我开始相信问题不在于技术中改变类型的机制,而是一般来说改变类型并不容易或直接。问题是类型通常具有不同的属性。除非您仅针对行为差异映射到不同类型,否则不清楚如何处理附加信息。因此,基于新对象的构造迁移类型是有意义的。

      问题与“Java 无关”。它通常与类型理论有关,更具体地说与对象关系映射有关。如果您的语言支持打字,我很想听听您如何在运行时自动更改对象的类型,并神奇地利用剩余的信息做一些智能的事情。只要我们在传播 FUD:当心你从谁那里得到建议:Rails 是一种框架,Ruby 是一种语言。

      【讨论】:

        【解决方案5】:

        当您说转换时,您可能歪曲了问题。在我看来,您真正想做的是基于另一个类的实例构造一个类的实例,例如:

        public PayingUser(TrialUser theUser) {
        ...
        }
        

        然后您可以删除旧的试用用户并保留新的付费用户。

        【讨论】:

        • 正确,但正如我在帖子中提到的那样,这会破坏所有外键约束。有很多表引用了 TrialUser 实体,包括交易记录、偏好、文件等。所有这些都需要转移到 PayingUser 实体,否则用户将失去一切
        • 对不起,我错过了外键。还是我回复后你加的?无论如何,您的复制构造函数也应该复制关系。这似乎很昂贵,所以不要计划在对象生命周期内发生超过一次或两次。
        猜你喜欢
        • 1970-01-01
        • 2013-12-04
        • 1970-01-01
        • 2023-02-24
        • 2023-04-04
        • 1970-01-01
        • 2020-05-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多