当前位置: 首页 > 面试题库 >

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

王英彦
2023-03-14
问题内容

在我的特定情况下,我正在使用区分列策略。这意味着我的JPA实现(hibernate)创建带有特殊 DTYPE 列的 users
表。此列包含实体的类名称。例如,我的 用户 表可以具有 TrialUser*PayingUser的 子类。这些类名称将在
DTYPE 列中,以便EntityManager从数据库加载实体时,它知道要实例化的类的类型。
*** __

我尝试了两种转换实体类型的方法,但都觉得自己很脏:

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

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

#2的问题是,当您删除 TrialUser 时,您设置为Cascade =
ALL的所有属性也将被删除。这很糟糕,因为您仅尝试交换其他用户,而不删除所有扩展对象图。

更新1
:#2的问题对我来说几乎使一切都无法使用,因此我已经放弃尝试使其工作。最好的技巧无疑是第一,我在这方面取得了一些进步。关键是首先获取对基础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)

您可以看到 用户 具有一个OneToMany Set 的 Membership 实体引起了一些问题。我对幕后发生的事情了解得还不够多。

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

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


问题答案:

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

放弃 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 ,一个新的Transaction
Manager,然后将该事务管理器手动注入到存储库中,以便在与正确的PC对应的事务中手动包装 executeUpdate()
。在这里我不得不说,我对Spring / JPA容器管理的交易还不甚了解,因为它最终成为试图获取本地/手动交易的噩梦。
executeUpdate() 可以很好地处理从Service层拉入的容器管理的事务。

在这一点上,我抛出了所有内容并创建了这个类:

@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) 对其进行了标记, 这应该挂起来自Service层的容器管理的事务,并允许在PC外部进行转换。
  2. 在尝试将转换后的实体重新加载到PC之前,我使用 userService.evictUser(user) 将当前存储在PC中的旧副本逐出 。为此的代码只是获取一个 Session 实例并调用 evict(user) 。有关更多详细信息,请参见代码中的注释,但是基本上,如果我们不这样做,则对 getUser的 任何调用都将尝试返回仍在PC中的缓存的Entity,除了会引发关于类型不同的错误。

尽管我的初始测试进展顺利,但此解决方案可能仍然存在一些问题。由于它们被发现,我将保持更新。



 类似资料:
  • 我知道类似的问题,但它们是在Hibernate的范围内被问到和回答的。我也有同样的问题,但我正在使用Eclipselink,想知道对于Hibernate,是否有更好的或替代的解决方案。 简单地回顾一下这个问题: 我有两个相互继承的类: 现在我想把一个孩子变成父母,或者反过来。 我希望现在p的数据库行是。 另一种方法是这样的: 在这种情况下,我希望与一起保存,并丢失的值。 然而,上面所描述的并不奏效

  • 问题内容: 我有Spring MVC + JPA应用程序。 我的应用程序中有几个实体在不断变化。我希望能够审核此更改。我发现有一个注释可以跟踪对某些字段或整个实体的更改。我想知道是否有任何方法可以配置此跟踪选项- 我希望能够跟踪更改的内容以及更改的人。还可以对SQL 1个表中的多个实体进行更改吗?还可以跟踪- 实体字段的变化吗? 谢谢 问题答案: 是的,您可以跟踪所做的更改,更新的用户和时间戳。

  • 问题内容: 因此,我查看了有关使用Spring Data的JPA的各种教程,并且在许多场合都做了不同的事情,而且我不确定什么是正确的方法。 假设存在以下实体: 我们还有一个DTO,该DTO在服务层中检索,然后移交给控制器/客户端。 因此,现在假设客户想要在Webui中更改其名称-然后将执行一些控制器操作,在该操作中将有具有旧ID和新名称的更新的DTO。 现在,我必须将此更新的DTO保存到数据库中。

  • 在我的项目中,我将数据存储到本地数据库中,为此我使用了Room Library。根据android文档,我已经创建了一个实体类、一个抽象数据库类和一个Dao接口,我已经成功地存储和检索了数据。 有人能帮我解决这个问题吗?提前道谢。

  • 我正在尝试使用Querydsl(4.1.4)查询JPA,如本文http://www.Querydsl.com/static/Querydsl/latest/reference/html/ch02.html#jpa_integration所述。我使用Hibernate(5.2.12.final)作为JPA后端。我使用和处理器从JPA注释类生成Querydsl查询类型。 这将打印: 原始JpaUpda

  • 主要内容:JPA实体更新示例JPA允许我们通过更新实体来更改数据库中的记录。 JPA实体更新示例 在这里,我们将演示如何根据主键更新学生的年龄。 完整的项目代码如下所示 - 这个例子包含以下步骤 - 第1步: 在包下创建一个名为的实体类,它包含属性:,和。 文件:StudentEntity.java 的代码如下 - 第2步: 将实体类和其他数据库配置映射到文件中。 文件:persistence.xml 的代码如下 - 在包下