如何编写没有联接的 JPA 2.1 更新条件查询?

Posted

技术标签:

【中文标题】如何编写没有联接的 JPA 2.1 更新条件查询?【英文标题】:How do I write a JPA 2.1 update criteria query without joins? 【发布时间】:2014-12-20 12:04:13 【问题描述】:

我正在使用 JPA 2.1、Hibernate 4.3.6.Final 和 mysql 5.5.37。我正在尝试使用 CriteriaBuilder API 编写将更新多行的更新查询。但是,当我尝试运行以下命令时,我收到“java.lang.IllegalArgumentException:UPDATE/DELETE 条件查询无法定义连接”……

final CriteriaBuilder qb = m_entityManager.getCriteriaBuilder();
    final CriteriaUpdate<MyClas-s-room> q = qb.createCriteriaUpdate(MyClas-s-room.class);
    final Root<MyClas-s-room> root = q.from(MyClas-s-room.class);
    final Join<MyClas-s-room, MyClas-s-roomUser> rosterJoin = root.join(MyClas-s-room_.roster);
    final Join<MyClas-s-roomUser, User> userJoin = rosterJoin.join(MyClas-s-roomUser_.user);

    final Calendar today = Calendar.getInstance();
    q.set(root.get(MyClas-s-room_.enabled), false)
     .where(qb.and(qb.equal(root.get(MyClas-s-room_.enabled),true),
                            qb.lessThanOrEqualTo(root.get(MyClas-s-room_.session).get(MyClas-s-roomSession_.schedule).<Calendar>get(MyClas-s-roomSchedule_.endDate), today)),
                            qb.equal(rosterJoin.get(MyClas-s-roomUser_.clas-s-roomRole).get(Clas-s-roomRole_.name), Clas-s-roomRoles.TEACHER),
                            qb.equal(userJoin.get(User_.organization).get(Organization_.importDataFromSis), false));

    return m_entityManager.createQuery(q).executeUpdate();

在不使用 JPQL 的情况下,是否有另一种方法可以编写查询,以便利用 JPA 2.1 更新多行?下面是异常的完整堆栈跟踪……

java.lang.IllegalArgumentException: UPDATE/DELETE criteria queries cannot define joins
    at org.hibernate.jpa.criteria.path.RootImpl.illegalJoin(RootImpl.java:81)
    at org.hibernate.jpa.criteria.path.AbstractFromImpl.join(AbstractFromImpl.java:330)
    at org.hibernate.jpa.criteria.path.AbstractFromImpl.join(AbstractFromImpl.java:324)
    at org.mainco.subco.clas-s-room.repo.MyClas-s-roomDaoImpl.disabledNonCleverExpiredClasses(MyClas-s-roomDaoImpl.java:495)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:319)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at com.sun.proxy.$Proxy66.disabledNonCleverExpiredClasses(Unknown Source)
    at com.follett.fdr.lycea.lms.clas-s-room.test.da.MyClas-s-roomDaoTest.testDisableNonCleverExpiredClass(MyClas-s-roomDaoTest.java:355)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

编辑:我用这段代码尝试了子查询的想法……

   final CriteriaBuilder qb = m_entityManager.getCriteriaBuilder();
    final CriteriaUpdate<Clas-s-room> q = qb.createCriteriaUpdate(Clas-s-room.class);
    final Root<Clas-s-room> mainRoot = q.from(Clas-s-room.class);

    // Subquery to select the classes we want to disable.
    final Subquery<Clas-s-room> subquery = q.subquery(Clas-s-room.class);
    final Root<Clas-s-room> root = subquery.from(Clas-s-room.class);
    final Join<Clas-s-room, Clas-s-roomUser> rosterJoin = root.join(Clas-s-room_.roster);
    final Join<Clas-s-roomUser, User> userJoin = rosterJoin.join(Clas-s-roomUser_.user);
    final SetJoin<User, Organization> orgJoin = userJoin.join(User_.organizations);
    final Calendar today = Calendar.getInstance();
    subquery.select(root)
            .where(qb.and(qb.equal(root.get(Clas-s-room_.enabled),true),
                          qb.lessThanOrEqualTo(root.get(Clas-s-room_.session).get(Clas-s-roomSession_.schedule).<Calendar>get(Clas-s-roomSchedule_.endDate), today)),
                          qb.equal(rosterJoin.get(Clas-s-roomUser_.clas-s-roomRole).get(Clas-s-roomRole_.name), Clas-s-roomRoles.TEACHER),
                          qb.equal(orgJoin.get(Organization_.importDataFromSis), false)
                          );

    // Build the update query
    q.set(mainRoot.get(Clas-s-room_.enabled), false)
     .where(mainRoot.in(subquery));

    // execute the update query
    return m_entityManager.createQuery(q).executeUpdate();

但有例外……

javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: could not execute statement
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1763)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:1771)
    at org.hibernate.jpa.spi.AbstractQueryImpl.executeUpdate(AbstractQueryImpl.java:87)
    at …
Caused by: java.sql.SQLException: You can't specify target table ‘my_clas-s-room' for update in FROM clause
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1074)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4096)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4028)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2490)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2651)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2734)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2155)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2458)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2375)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2359)
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:208)
    ... 49 more

【问题讨论】:

我知道您对更新的 JPQL 查询不感兴趣,但是这样可以吗? 好的,如果有一种 JPQL 方法可以在一个语句中更新所有内容,那么我会接受。我们一直在代码库中到处使用 CriteriaBuilder,但我读到 JPQL 可以做与 CriteriaBuilder 相同的事情。 我认为这取决于 RDBMS 所期望的 SQL(因为 JPQL 提供与 Criteria 相同的内容)。也许 RDBMS 根本不允许在 UPDATE/DELETE 语句中加入? (有些没有) 【参考方案1】:

连接只能在标准 SQL 中的 SELECT 中进行。它在 JPQL / Criteria 中不起作用也就不足为奇了。

您应该尝试使用Subquery 进行连接并在where 子句中指定in 表达式。

要创建子查询,它与标准 CriteriaQuery 相同,因为在 CommonAbstractCriteria 中定义了 subquery 方法是一个通用分类器。

this exchange 中有一个子查询示例。

【讨论】:

您的子查询解决方案几乎是正确的,您不应将整个实体指定为谓词,而应仅指定 Id 属性。它的意思不是“mainRoot.in(子查询)”,而是“mainIdAttrPredicate.in(子查询)”。另一点是关于仅选择子查询中的 Id。它的意思不是“.select(root)”而是“.select(idAttrPredicate)”。 如果我尝试只选择 sbuquery --subquery.select(root.get(Clas-s-room_.id)) 中的 id - 代码甚至无法编译。编译错误是“方法选择(表达式)不适用于参数(路径)”。 Path 专门用于表达式,即关于子查询的泛型类型>。您不应选择实体类,而应选择 Id 类型。在我给你作为链接的 *** 交换中,他们使用 Integer ("Subquery sq = c.subquery(Integer.class);")。 得到了和之前一样的异常,其根源是“Caused by: java.sql.SQLException: You can't specify target table 'my_clas-s-room' for update in FROM clause” 我记得那个问题,它是 MySQL 特有的。详情见that exchange。似乎指定的解决方法对您没有帮助,忘记子查询。内部连接点可以帮助您解决问题。我确信所有其他 RDBMS 都不允许在更新中加入(也许 SQL Server 中有一个技巧)。【参考方案2】:

在处理您的问题时,我找到了一些解决方案。

您可以使用HibernateCriteria接口来创建条件而不是使用JPQL条件,并根据您的实际条件添加几个Restriction,如下所示。

Session session = (Session) entityManager.getDelegate();

Criteria criteria = session.createCriteria(MyClas-s-room.class,"myclas-s-room");
criteria.createCriteria("myclas-s-room.roster","rosterJoin");
criteria.createCriteria("rosterJoin.user","userJoin");
criteria.createCriteria("myclas-s-room.session","session");
criteria.createCriteria("session.schedule","schedule");
criteria.createCriteria("rosterJoin.clas-s-roomRole","clas-s-roomRole");
criteria.createCriteria("userJoin.organization","organization");


final Calendar today = Calendar.getInstance();

criteria.add(Restrictions.eq("myclas-s-room.enabled",true));
criteria.add(Restrictions.le("schedule.endDate",today));
criteria.add(Restrictions.eq("clas-s-roomRole.name",Clas-s-roomRoles.TEACHER));
criteria.add(Restrictions.eq("organization.importDataFromSis",false));

List<MyClas-s-room> myClassList = (List<MyClas-s-room>)criteria.list();

for(MyClas-s-room room : myClassList) 
    room.setEnabled(false);
    session.saveOrUpdate(room);

Hibernate 中创建条件实际上会在两个表之间创建INNER JOIN

myClassList 将包含您想要更新的所有对象,现在对其进行迭代并更新您想要的值。希望这能解决您的问题。

注意:这只是解决您问题的一种方法。遇到异常请自行调试。

【讨论】:

您好,您的解决方案选择了数据,但没有更新任何内容。我想做的是编写一个 CriteriaBuilder 查询,根据特定条件更新多行。【参考方案3】:

如果您只是寻找解决方案,我建议:

    选择您需要的数据(使用 CriteriaQuery,因为您需要它们)。 浏览获取的数据列表并更新事务中的所有内容。

该解决方案的代码有点复杂,但更具可读性,尤其是在您有复杂的 JOIN 时。

对上述解决方案的一个小改进是在条件构建器的 WHERE 部分中使用子查询。这应该可行,尽管我从未尝试过(我喜欢总是获取被更改的数据,最终我会想要记录一些关于它的内容,比如历史记录)。 JPQL 示例:

    Query updateQuery = em.createQuery("UPDATE MyClasroom SET enabled=false WHERE enabled=true AND id IN (SELECT id FROM MyClasroom WHERE ...)");
    updateQuery.setParameter("phoneNo", "phoneNo");
    ArrayList<Long> paramList = new ArrayList<Long>();
    paramList.add(123L);
    updateQuery.setParameter("ids", paramList);
    int nrUpdated = updateQuery.executeUpdate();

这是符合 JPA 的(作为其关联的 Criteria 查询),但某些数据库可能不支持它(例如 MySql 不允许更新 the same table as the one specified in the subquery)。如果这是您的情况,您唯一的选择就是按照我的第一个建议进行操作。

【讨论】:

谢谢,但我对 JPA 2.1 的印象是,我可以编写一个 UPDATE CriteriaBuilder 语句,允许我一次更新多行。如果地球上绝对没有办法做到这一点,那么我会回来接受你的回答。 您当然可以使用标准更新同一查询中的更多行,但可能不使用 JOIN,这似乎是您的问题。 我对一次性更新多行且不使用 JOIN 的解决方案持开放态度。 因为您在更新时使用了不同的表,所以您需要来自它们的数据才能做出决策。所以一种解决方案是在这些条件下获取所有MyClas-s-rooms 的ID(使用任何你想要的:CriteriaBuilderJPQL),然后使用JPQS(或CriteriaBuilder)和UPDATE MyClas-s-room SET enabled=false WHERE id IN :myIdList 之类的东西. 由于我想在一个语句中更新所有内容,是否可以用实际查询替换 :myIdList 并且我可以只执行一个语句?【参考方案4】:
    JPA 中的更新不允许加入。

    即使使用像

    这样的子查询

    更新 MyClas-s-room set enabled=false where id in (select c.id from MyClas-s-room c join c.roster r ... where ...)

将失败,因为 MySQL 不允许这样做 - 更新并从同一个表中选择 (doc)。

    在 JPQL 中也不可能(因为第 2 点)。

【讨论】:

以上是关于如何编写没有联接的 JPA 2.1 更新条件查询?的主要内容,如果未能解决你的问题,请参考以下文章

如何编写包含联接、更新和排序的查询?

如何使用本机查询在 JPA 中执行嵌套联接

如何使用布尔条件编写 JPA 查询

如何编写与集合完全匹配的 JPA 条件查询?

sqlserver 联接查询的一些注意点

Java JPA - 内部联接查询不起作用