批量更新后的 JPA JQL 查询看到过时的值

Posted

技术标签:

【中文标题】批量更新后的 JPA JQL 查询看到过时的值【英文标题】:JPA JQL query after bulk UPDATE sees stale values 【发布时间】:2021-02-03 11:31:30 【问题描述】:

我有一个 EclipseLink JPA 演示应用程序,它在执行 JPQL UPDATE 语句后发出一个 JPQL SELECT 语句。 SELECT 语句“看到”过时的数据,如下所示:

如果未提供任何提示,则结果数据已过时 如果使用 .setHint("javax.persistence.cache.storeMode", "REFRESH"),则检索更新的数据 如果检索到 .setHint("javax.persistence.cache.retrieveMode", "BYPASS") 陈旧数据 如果我在查询之前执行 em.clear(),查询会检索更新的值(但这很明显,我想知道当持久性上下文未被清除时会发生什么)。然而,这指向 1 级缓存问题。

我无法理解 JPQL SELECT 从何处获取过时数据。它们显然不在共享缓存中(通过使用 BYPASS 提示和缓存接口的 contains() 方法确认)。

我做过的实验:

将 where 1=1 添加到过时检索查询以检查是否发生某些查询代码缓存(无更改) 从第二个 SELECT 查询中删除提示,以检查可能延迟的数据库提交是否存在时间问题(第二个查询也看到过时的数据)。

有什么想法吗?

    package examples.client;
    
    import examples.model.Employee;
    import java.util.List;
    
    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.Persistence;
    
    public class EmployeeUPDATEModification 
    
        public static void main(String[] args) 
            EntityManagerFactory emf = Persistence.createEntityManagerFactory("EmployeeService");
            EntityManager em = emf.createEntityManager();
    
            System.out.println("***INITIAL SALARY VALUES\n");
            List<Employee> initial = em.createQuery("SELECT e FROM Employee e", Employee.class).getResultList();
            for (Employee e : initial) 
                System.out.println(e.getSalary());
            
            System.out.println("***TESTING BULK UPDATE\n");
            em.getTransaction().begin();
            em.createQuery("UPDATE Employee e SET e.salary = e.salary*2").executeUpdate();
            em.getTransaction().commit();
    
            System.out.println("***SALARY VALUES AFTER BULK UPDATE FROM INITIAL LIST\n");
            for (Employee e : initial) 
                System.out.println(e.getSalary());
            
    
            System.out.println("\n***PRINTING THE SALARY FROM A QUERY WITHOUT HINT \n");
            List<Employee> result = em.createQuery("SELECT e FROM Employee e", Employee.class).getResultList();
            for (Employee e : result) 
                System.out.println(e.getSalary());
            
    
            System.out.println("\n***CHECKING THE SHARED CACHE\n");
            for (Employee e : result) 
                System.out.println(emf.getCache().contains(Employee.class, e.getId()) ? e + " is in shared chache"
                        : e + " is NOT in shared cache");
            
    
            System.out.println("\n***PRINTING THE SALARY FROM A QUERY WITH HINT \n");
            List<Employee> result3 = em.createQuery("SELECT e FROM Employee e", Employee.class)
                    .setHint("javax.persistence.cache.storeMode", "REFRESH").getResultList();
            for (Employee e : result3) 
                System.out.println(e.getSalary());
            
    
            // close the EM and EMF when done
            em.close();
            emf.close();
    
        
    

控制台

***INITIAL SALARY VALUES

[EL Fine]: sql: 2020-10-20 17:21:25.213--ServerSession(2092769598)--Connection(110651474)--Thread(Thread[main,5,main])--SELECT ID, NAME, SALARY FROM EMPLOYEE

100
200
300

***TESTING BULK UPDATE

[EL Fine]: sql: 2020-10-20 17:21:25.241--ClientSession(706665172)--Connection(110651474)--Thread(Thread[main,5,main])--UPDATE EMPLOYEE SET SALARY = (SALARY * ?)
    bind => [2]

***SALARY VALUES AFTER BULK UPDATE FROM INITIAL LIST

100
200
300

***PRINTING THE SALARY FROM A QUERY WITHOUT HINT 

[EL Fine]: sql: 2020-10-20 17:21:25.256--ServerSession(2092769598)--Connection(110651474)--Thread(Thread[main,5,main])--SELECT ID, NAME, SALARY FROM EMPLOYEE

100
200
300

***CHECKING THE SHARED CACHE

Employee id: 1 name: Piero salary: 100 is NOT in shared cache

Employee id: 2 name: Aldo salary: 200 is NOT in shared cache

Employee id: 3 name: Mario salary: 300 is NOT in shared cache

***PRINTING THE SALARY FROM A QUERY WITH HINT 

[EL Fine]: sql: 2020-10-20 17:21:25.271--ServerSession(2092769598)--Connection(110651474)--Thread(Thread[main,5,main])--SELECT ID, NAME, SALARY FROM EMPLOYEE

200
400
600

具有最佳日志记录级别的控制台

***PRINTING THE SALARY FROM A QUERY WITHOUT HINT 

[EL Finest]: query: 2020-10-20 18:14:08.097--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Execute query ReadAllQuery(referenceClass=Employee sql="SELECT ID, NAME, SALARY FROM EMPLOYEE")

[EL Finest]: connection: 2020-10-20 18:14:08.097--ServerSession(1006485584)--Connection(1482246673)--Thread(Thread[main,5,main])--Connection acquired from connection pool [default].

[EL Fine]: sql: 2020-10-20 18:14:08.097--ServerSession(1006485584)--Connection(1482246673)--Thread(Thread[main,5,main])--SELECT ID, NAME, SALARY FROM EMPLOYEE

[EL Finest]: connection: 2020-10-20 18:14:08.099--ServerSession(1006485584)--Connection(1482246673)--Thread(Thread[main,5,main])--Connection released to connection pool [default].

400
800
1200

PRINTING THE SALARY FROM A QUERY WITH HINT 

***

[EL Finest]: query: 2020-10-20 18:14:08.119--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Execute query ReadAllQuery(referenceClass=Employee sql="SELECT ID, NAME, SALARY FROM EMPLOYEE")

[EL Finest]: connection: 2020-10-20 18:14:08.119--ServerSession(1006485584)--Connection(1482246673)--Thread(Thread[main,5,main])--Connection acquired from connection pool [default].

[EL Fine]: sql: 2020-10-20 18:14:08.119--ServerSession(1006485584)--Connection(1482246673)--Thread(Thread[main,5,main])--SELECT ID, NAME, SALARY FROM EMPLOYEE

[EL Finest]: connection: 2020-10-20 18:14:08.121--ServerSession(1006485584)--Connection(1482246673)--Thread(Thread[main,5,main])--Connection released to connection pool [default].

[EL Finest]: transaction: 2020-10-20 18:14:08.122--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Merge clone Employee id: 1 name: Piero salary: 800 

[EL Finest]: transaction: 2020-10-20 18:14:08.122--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Merge clone Employee id: 2 name: Aldo salary: 1600 

[EL Finest]: transaction: 2020-10-20 18:14:08.123--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Merge clone Employee id: 3 name: Mario salary: 2400 

800
1600
2400

【问题讨论】:

启用 FINEST 日志记录级别揭示了带有和不带有提示的查询的差异。发生克隆+合并。 [EL Finest]:事务:2020-10-20 18:14:08.122--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Merge clone Employee id: 1 name: Piero 工资: 800 [EL Finest]:事务:2020-10-20 18:14:08.122--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Merge clone Employee id:2 姓名:Aldo 工资:1600 [EL Finest]:事务:2020-10-20 18:14:08.123--UnitOfWork(955611965)--Thread(Thread[main,5,main])--Merge clone Employee id:3 姓名:Mario 工资:2400 也许这就是答案。啊! ***.com/questions/15089177/… 请注意,生成的查询是 SELECT ID, NAME, SALARY FROM EMPLOYEE,因此显然新状态(工资值)是从数据库中检索但未使用,有利于缓存状态。这真是违反直觉。 【参考方案1】:

您发布的答案提到了您的问题 - 您从本地上下文中读取了一个实体,并使其因模型对象之外的更新而变得陈旧(您的批量更新查询)。每次从该 EntityManager 读取时,您都会返回相同的陈旧员工实例数据 - JPA 要求它维护对象身份,并且由于您可能在其中进行未提交的更改,因此不能将它们清除。所以它会执行一个完整的列表操作,但是当它看到一个已经缓存/管理的 Emp id 时,只返回那个实例。

JPA 规范指出批量更新和删除以在上下文中可能不可见的方式改变事物: 第 4.10 节: "持久化上下文与批量更新或删除的结果不同步。 执行批量更新或删除操作时应小心,因为它们可能导致数据库与活动持久性上下文中的实体之间的不一致。一般来说,批量更新和删除操作只能在新的持久性上下文中的事务内执行,或者在获取或访问状态可能受此类操作影响的实体之前执行。”

您可以通过强制刷新以后的查询、清除实体管理器或在提交事务后获取新的来解决此问题。然后所有读取都将使用数据库中的数据。

【讨论】:

以上是关于批量更新后的 JPA JQL 查询看到过时的值的主要内容,如果未能解决你的问题,请参考以下文章

JPA - 批量/批量更新 - 更好的方法是啥?

JPA 的批量更新触发 TransactionalEventListener?

JPA的批量更新会触发TransactionalEventListener吗?

Spring数据JPA存储库saveAll不生成批量插入查询

Spring Data JPA - 并发批量插入/更新

Spring Data JPA : 批量增删