如何更改JPA中的实体类型?
在我的具体案例中,我正在使用鉴别器列策略。 这意味着我的 JPA 实现 (Hibernate) 创建了一个带有特殊 DTYPE 列的 users 表。 此列包含实体的类名称。 例如,我的users 表可以包含TrialUser 和PayingUser 的子类。 这些类名将位于 DTYPE 列中,以便当 EntityManager 从数据库加载实体时,它知道要实例化哪种类型的类。
我尝试了两种转换实体类型的方法,但都感觉像是肮脏的黑客:
- 使用本机查询手动对列进行更新,更改其值。 这适用于属性约束相似的实体。
- 创建目标类型的新实体,执行 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:
- Use a native query to manually do an UPDATE on the column, changing its value. This works for entities whose property constraints are similar.
- 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
所以我终于找到了一个可行的解决方案:
放弃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() 的本地/手动事务能够很好地发挥作用最终成为一场噩梦从服务层拉入容器管理的事务。
此时我扔掉了所有内容并创建了这个类:
我相信这个方法有两个重要部分使它起作用:
这应该暂停来自服务层的容器管理事务,并允许在 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:
There are two important parts of this method which I believe make it work:
which should suspend the container managed transaction coming in from the Service layer and allow the conversion to take place external of the PC.
Though my initial tests have gone well, this solution may still have some problems. I will keep this updated as they are uncovered.
您真的需要在用户类型中表示订阅级别吗? 为什么不在系统中添加订阅类型?
该关系将表达为:一个用户有一个订阅。 如果您希望保留订阅历史记录,也可以将其建模为一对多关系。
然后,当您想要更改用户的订阅级别时,您可以创建一个新的订阅并将其分配给用户,而不是实例化新用户。
试用用户与付费用户真的有那么不同吗? 可能不会。 使用聚合而不是继承会更好。
订阅数据可以存储在单独的表中(映射为实体),也可以存储在用户表内(映射为组件)。
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.
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).
这个问题与 Java 的关系比其他任何事情都更重要。 您无法更改实例的运行时类型(在运行时),因此 hibernate 不提供此类场景(例如与 Rails 不同)。
如果您想从会话中驱逐用户,则必须驱逐关联的实体。 您有一些选择:
我在 grails 中也遇到过类似的问题,我的解决方案类似于 grigory's 解决方案。 我复制所有实例字段,包括关联的实体,然后删除旧实体并写入新实体。 如果您没有那么多关系,这可能很容易,但如果您的数据模型很复杂,您最好使用您描述的本机 sql 解决方案。
如果您有兴趣,这是来源:
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:
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:
我遇到这篇文章是因为我(或认为我)遇到了类似的问题。 我开始相信问题不在于技术中改变类型的机制,但一般来说,发展类型并不容易或直接。 问题在于类型通常具有不同的属性。 除非您只是为了行为差异而映射到不同类型,否则不清楚如何处理附加信息。 因此,基于新对象的构造来迁移类型是有意义的。
该问题与“Java 无关”。 它与一般的类型论有关,更具体地说与这里的对象关系映射有关。 如果您的语言支持输入,我很想听听您如何在运行时自动更改对象的类型,并神奇地利用剩余信息执行一些智能操作。 只要我们传播 FUD:请注意您从谁那里接受建议:Rails 是一个框架,Ruby 是一种语言。
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.
当你说转换时,你可能歪曲了问题。 在我看来,您真正想做的是基于另一个类的实例构造一个类的实例,例如:
然后您可以删除旧的试用用户并保留新的付费用户。
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:
Then you can delete old trial user and persist new paying user.