如何更改JPA中的实体类型? [英] How to change Entity type in JPA?

查看:103
本文介绍了如何更改JPA中的实体类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



我试过两种转换实体类型的方式,都觉得自己很脏:


  1. 使用本机查询手动在列上执行UPDATE,更改其值。这适用于属性约束类似的实体。

  2. 创建目标类型的新实体,执行 BeanUtils.copyProperties()调用以移动属性,保存新的实体,然后调用一个命名的查询,手动将新的Id替换为旧的Id,以便维护所有的外键约束。

#1的问题在于,当您手动更改此列时,JPA不知道如何刷新/重新将此实体重新附加到持久性上下文。它预计使用Id 1234的 TrialUser ,而不是使用Id 1234的 PayingUser 。它会失败。在这里,我可能会做一个EntityManager.clear()并分离所有实体/清除Per。上下文,但由于这是一个Service bean,它将清除系统中所有用户的挂起更改。



#2的问题在于,当您删除 TrialUser 您设置为Cascade = ALL的所有属性也将被删除。这是不好的,因为你只是想交换一个不同的用户,而不是删除所有的扩展对象图。

更新1 :#2的问题已经让我无法使用,所以我已经放弃了试图让它发挥作用。更优雅的黑客绝对是第一名,我在这方面取得了一些进展。关键是首先获得对底层Hibernate Session的引用(如果您使用Hibernate作为JPA实现)并调用Session.evict(user)方法从persistence上下文中仅删除单个对象。不幸的是,没有纯粹的JPA支持。以下是一些示例代码:

  //确保我们保存了任何待处理的更改
user = saveUser(user);

//将用户实例从持久化上下文中移除
final Session session =(Session)entityManager.getDelegate();
session.evict(user);

//更新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(); // ***问题在这里***

//使用新类型加载用户
return getUserById(userId);

注意引发此异常的手动 flush()

  org.hibernate.PersistentObjectException:传递给persist的分离实体:com.myapp.domain.Membership $ b $ 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)
在org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239)
在org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
在org.hibernate.engine .Cascade.cascadeCollectionElements(Cascade.java:319)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:265)
at org.hibernate.engine.Cascade.cascadeAssocia (Cascade.java:242)
在org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
在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)
在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集合,导致一些问题。我不知道幕后发生了什么事情来破解这个坚果。



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



t完全理解清除整个持久化上下文的后果,而且我希望 Session.evict()用于正在更新的特定实体。



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



我只能在 entityManager.clear()工作的情况下工作,但我无法接受副作用。然后我尝试创建一个专门用于实体转换的独立持久性单元。我想我可以将 clear()操作本地化为只负责转换的PC。我建立了一台新的PC,一个新的相应的 EntityManagerFactory ,一个新的事务管理器,并手工将这个事务管理器注入到Repository中,以手动包装 executeUpdate()在对应于正确PC的交易中。在这里,我不得不说我对Spring / JPA容器管理的事务不够了解,因为它最终成为一个噩梦,试图让 executeUpdate()的本地/手动事务与容器管理的事务从服务层拉入。



此时我抛出所有内容并创建了这个类:

  @Transactional(propagation = Propagation.NOT_SUPPORTED)
public class JdbcUserConversionRepository实现UserConversionRepository {

@Resource
private UserService userService;

private JdbcTemplate jdbcTemplate;

@Override
@SuppressWarnings(unchecked)
public User convertUserType(final User user,final Class targetClass){

//更新DTYPE
jdbcTemplate.update(update user set user.DTYPE =?where user.id =?,new Object [] {targetClass.getSimpleName(),user.getId()});

//在我们尝试将转换后的用户加载回Persistence
// Context中之前,我们需要将它们从PC中删除,以便EntityManager
//不会尝试在PC中加载缓存的。请记住,该用户的所有
//子实体将保留在PC中。这会导致
//通常在PC刷新时抛出一个问题,抛出一个分离的
//实体异常。在这个特定的情况下,我们返回一个新的User
//引用来替换旧的。这意味着如果我们只是驱逐
//用户,然后删除所有对它的引用,那么PC将无法
//深入到孩子中并尝试坚持它们。
userService.evictUser(user);

//将转换后的用户重新加载到持久性上下文
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(用户)驱逐当前存储在PC中的旧副本。此代码只是获取 Session 实例并调用 evict(用户)。有关更多详细信息,请参阅代码中的注释,但基本上,如果我们不这样做,任何对 getUser 的调用都会尝试返回仍在PC中的缓存实体,只不过它会抛出有关类型是不同的。

尽管我的初始测试进行得很顺利,但这个解决方案可能仍然存在一些问题。我将在发现它们时保持更新。


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.

解决方案

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.

这篇关于如何更改JPA中的实体类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆