10

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

我尝试了两种转换实体类型的方法,都感觉像肮脏的黑客:

  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)

您可以看到User具有 OneToMany 集的Membership实体导致了一些问题。我对幕后发生的事情知之甚少,无法破解这个坚果。

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

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

4

5 回答 5

3

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

放弃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)。有关更多详细信息,请参阅代码中的注释,但基本上,如果我们不这样做,任何对getUser的调用都会尝试返回仍在 PC 中的缓存实体,但它会抛出关于类型不同的错误。

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

于 2009-04-18T15:47:17.647 回答
2

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

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

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

// When you user signs up
User.setSubscription(new FreeSubscription());

// When he upgrades to a paying account
User.setSubscription(new PayingSubscription());

TrialUser 真的与 PayingUser 不同吗?可能不是。聚合而不是继承会更好地为您服务。

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

于 2009-04-14T19:18:46.380 回答
1

这个问题与 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
          }
        }
      }
于 2009-04-16T17:58:02.313 回答
0

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

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

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

于 2009-06-08T23:10:10.377 回答
-1

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

public PayingUser(TrialUser theUser) {
...
}

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

于 2009-04-13T17:24:27.740 回答