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

发布于 2024-07-17 13:35:56 字数 3914 浏览 2 评论 0原文

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

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

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

#1 的问题是,当您手动更改此列时,JPA 不知道如何刷新/重新附加此实体到持久性上下文。 它需要 ID 为 1234 的 TrialUser,而不是 ID 为 1234 的 PayingUser。它失败了。 在这里,我可能可以执行 EntityManager.clear() 并分离所有实体/清除 Per。 Context,但由于这是一个 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); 

注意手动 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 Set 导致了一些问题。 我对幕后发生的事情了解不够,无法破解这个难题。

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

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

In my specific case, I am making use of a discriminator column strategy. This means that my JPA implementation (Hibernate) creates a users table with a special DTYPE column. This column contains the class name of the entity. For example, my users table can have subclasses of TrialUser and PayingUser. These class names would be in the DTYPE column so that when the EntityManager loads the entity from the database, it knows which type of class to instantiate.

I've tried two ways of converting Entity types and both feel like dirty hacks:

  1. Use a native query to manually do an UPDATE on the column, changing its value. This works for entities whose property constraints are similar.
  2. Create a new entity of the target type, do a BeanUtils.copyProperties() call to move over the properties, save the new entity, then call a named query which manually replaces the new Id with the old Id so that all the foreign key constraints are maintained.

The problem with #1 is that when you manually change this column, JPA doesn't know how to refresh/reattach this Entity to the Persistance Context. It expects a TrialUser with Id 1234, not a PayingUser with Id 1234. It fails out. Here I could probably do an EntityManager.clear() and detach all Entities/clear the Per. Context, but since this is a Service bean, it would wipe pending changes for all users of the system.

The problem with #2 is that when you delete the TrialUser all of the properties you have set to Cascade=ALL will be deleted as well. This is bad because you're only trying to swap in a different User, not delete all the extended object graph.

Update 1: The problems of #2 have made it all but unusable for me, so I've given up on trying to get it to work. The more elegant of the hacks is definitely #1, and I have made some progress in this respect. The key is to first get a reference to the underlying Hibernate Session (if you're using Hibernate as your JPA implementation) and call the Session.evict(user) method to remove only that single object from your persistance context. Unfortunitely there is no pure JPA support for this. Here is some sample code:

  // 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); 

Notice the manual flush() which throws this exception:

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)

You can see that the Membership entity, of which User has a OneToMany Set, is causing some problems. I don't know enough about what's going on behind the scenes to crack this nut.

Update 2: The only thing that works so far is to change DTYPE as shown in the above code, then call entityManager.clear()

I don't completely understand the ramifications of clearing the entire persistence context, and I would have liked to get Session.evict() working on the particular Entity being updated instead.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

风渺 2024-07-24 13:35:56

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

放弃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、一个新的事务管理器,并手动将该事务管理器注入到存储库中以手动包装 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 中的缓存实体,除了它会抛出有关类型不同。

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

So I finally figured out a working solution:

Ditch the EntityManager for updating DTYPE. This is mainly because Query.executeUpdate() must run within a transaction. You can try running it within the existing transaction, but that is probably tied to the same persistence context of the Entity you're modifying. What this means is that after you update DTYPE you have to find a way to evict() the Entity. The easy way is to call entityManager.clear() but this results in all sorts of side effects (read about it in the JPA spec). The better solution is to get the underlying delegate (in my case, a Hibernate Session) and call Session.evict(user). This will probably work on simple domain graphs, but mine were very complex. I was never able to get @Cascade(CascadeType.EVICT) to work correctly with my existing JPA annotations, like @OneToOne(cascade = CascadeType.ALL). I also tried manually passing my domain graph a Session and having each parent Entity evict its children. This also didn't work for unknown reasons.

I was left in a situation where only entityManager.clear() would work, but I couldn't accept the side effects. I then tried creating a separate Persistence Unit specifically for Entity conversions. I figured I could localize the clear() operation to only that PC in charge of conversions. I set up a new PC, a new corresponding EntityManagerFactory, a new Transaction Manager for it, and manually injecting this transaction manager into the Repository for manual wrapping of the executeUpdate() in a transaction corresponding to the proper PC. Here I have to say that I don't know enough about Spring/JPA container managed transactions, because it ended up being a nightmare trying to get the local/manual transaction for executeUpdate() to play nicely with the container managed transaction getting pulled in from the Service layer.

At this point I threw out everything and created this class:

@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);
    }
}

There are two important parts of this method which I believe make it work:

  1. I've marked it with @Transactional(propagation = Propagation.NOT_SUPPORTED)
    which should suspend the container managed transaction coming in from the Service layer and allow the conversion to take place external of the PC.
  2. Before trying to reload the converted Entity back into the PC, I evict the old copy currently stored in the PC with userService.evictUser(user);. The code for this is simply getting a Session instance and calling evict(user). See the comments in code for more details, but basically if we don't do this any calls to getUser will try to return the cached Entity still in the PC, except that it will throw an error about the type being different.

Though my initial tests have gone well, this solution may still have some problems. I will keep this updated as they are uncovered.

半仙 2024-07-24 13:35:56

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

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

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

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

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

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

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

Do you really need to represent the subscription level in the User type? Why don't you add a Subscription type in your system?

The relation would then be expressed as: a User has one Subscription. It could also be modeled as a one-to-many relationship if you wish to keep an history of subscriptions.

Then when you want to change the Subscription level of a User, instead of instantiating a new User you would create a new Subscription and assign it to a user.

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

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

Is a TrialUser really that different from a PayingUser? Probably not. You would be better served with aggregation instead of inheritance.

The Subscription data can be stored in a separate table (mapped as an Entity) or it can be stored inside the Users table (mapped as a Component).

御守 2024-07-24 13:35:56

这个问题与 Java 的关系比其他任何事情都更重要。 您无法更改实例的运行时类型(在运行时),因此 hibernate 不提供此类场景(例如与 Rails 不同)。

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

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

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

The problem is more related to Java than anything else. You can't change a runtime type of an instance (at runtime), so hibernate does not provide for this kind of scenarios (unlike rails for example).

If you want to evict the user from the session, you'll have to evict associated entities. You have some choices:

  • Map the entity with evict=cascade (see the Session#evict javadoc)
  • Manually evict all associated entites (this can be cumbersome)
  • Clear the session, thus evicting all entities (of course, you'll lose the local session cache)

I've had a similar problem in grails, and my solution is similar to grigory's solution. I copy all instance fields, including the associated entities, then delete the old entity and write the new one. If you don't have that many relations this can be easy, but if your data model is complex you'll be better off using the native sql solution you described.

Here is the source, in case you're interested:

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
          }
        }
      }
一场信仰旅途 2024-07-24 13:35:56

该问题与Java更相关
比其他任何事情都重要。 你不能改变一个
实例的运行时类型(位于
运行时),所以休眠不
为此类场景提供
(例如与 Rails 不同)。

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

该问题与“Java 无关”。 它与一般的类型论有关,更具体地说与这里的对象关系映射有关。 如果您的语言支持输入,我很想听听您如何在运行时自动更改对象的类型,并神奇地利用剩余信息执行一些智能操作。 只要我们传播 FUD:请注意您从谁那里接受建议:Rails 是一个框架,Ruby 是一种语言。

The problem is more related to Java
than anything else. You can't change a
runtime type of an instance (at
runtime), so hibernate does not
provide for this kind of scenarios
(unlike rails for example).

I encountered this post because I am (or thought I was) having a similar problem. I've come to believe the problem is not with mechanics of changing types in the technology, but that evolving the types in general is not at all easy or straightforward. The issue is that types quite often have varying attributes. Unless you are mapping to different types only for behavioral differences, it isn't clear what to do with the additional information. For this reason, it makes sense to migrate types based on construction of new objects.

The problem is not 'related to Java'. it is related to type theory in general and more specifically object-relational mapping here. If your language supports typing, I'd love to hear how you can automatically change the type of an object at runtime and magically do something intelligent with the left over information. As long as we are spreading FUD: beware who you take advice from: Rails is a framework, Ruby is a language.

晨光如昨 2024-07-24 13:35:56

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

public PayingUser(TrialUser theUser) {
...
}

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

When you say conversion you probably misrepresent the problem. What you really is trying to do in my opinion is to construct instance of one class based on an instance of the other class, for example:

public PayingUser(TrialUser theUser) {
...
}

Then you can delete old trial user and persist new paying user.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文