后端数据库异步更改时如何刷新JPA实体?

Posted

技术标签:

【中文标题】后端数据库异步更改时如何刷新JPA实体?【英文标题】:How to refresh JPA entities when backend database changes asynchronously? 【发布时间】:2012-10-26 20:59:45 【问题描述】:

我有一个 PostgreSQL 8.4 数据库,其中包含一些表和视图,这些表和视图本质上是一些表的连接。我使用 NetBeans 7.2(如 here 所述)创建从这些视图和表派生的基于 REST 的服务,并将它们部署到 Glassfish 3.1.2.2 服务器。

还有另一个进程异步更新某些用于构建视图的表中的内容。我可以直接查询视图和表并查看这些更改是否正确发生。但是,当从基于 REST 的服务中提取时,这些值与数据库中的值不同。我假设这是因为 JPA 已经在 Glassfish 服务器上缓存了数据库内容的本地副本,并且 JPA 需要刷新关联的实体。

我尝试向 NetBeans 生成的 AbstractFacade 类添加几个方法:

public abstract class AbstractFacade<T> 
    private Class<T> entityClass;
    private String entityName;
    private static boolean _refresh = true;

    public static void refresh()  _refresh = true; 

    public AbstractFacade(Class<T> entityClass) 
        this.entityClass = entityClass;
        this.entityName = entityClass.getSimpleName();
    

    private void doRefresh() 
        if (_refresh) 
            EntityManager em = getEntityManager();
            em.flush();

            for (EntityType<?> entity : em.getMetamodel().getEntities()) 
                if (entity.getName().contains(entityName)) 
                    try 
                        em.refresh(entity);
                        // log success
                    
                    catch (IllegalArgumentException e) 
                        // log failure ... typically complains entity is not managed
                    
                
            

            _refresh = false;
        
    

...

然后,我从 NetBeans 生成的每个 find 方法调用 doRefresh()。通常发生的情况是 IllegalArgumentsException 被抛出,声明类似于 Can not refresh not managed object: EntityTypeImpl@28524907:MyView [ javaType: class org.my.rest.MyView descriptor: RelationalDescriptor(org.my.rest.MyView --&gt; [DatabaseTable(my_view)]), mappings: 12].

所以我正在寻找一些关于如何正确刷新与视图关联的实体以使其保持最新的建议。

更新: 原来我对潜在问题的理解是不正确的。它与another question I posted earlier 有点相关,即视图没有可用作唯一标识符的单个字段。 NetBeans 要求我选择一个 ID 字段,所以我只选择了应该是多部分键的一部分。这表现出具有特定 ID 字段的所有记录都相同的行为,即使数据库具有具有相同 ID 字段但其余部分不同的记录。 JPA 只是查看了我告诉它的唯一标识符,并简单地提取了它找到的第一条记录。

我通过添加一个唯一标识符字段解决了这个问题(一直无法让多部分密钥正常工作)。

【问题讨论】:

请发布您的 persistence.xml 减去任何 &lt;class/&gt; 条目。特别是二级缓存控件。 另外,这些也不是全部例外。会有一个原因链导致 EclipseLink 抛出 real 异常。请显示 EclipseLink 异常和(如果抛出)任何底层 PgJDBC 异常。如果您的应用程序没有输出它们,请查看 Glassfish 日志。 接近于***.com/q/2400430/398670 【参考方案1】:

您可以完全禁用缓存(请参阅:http://wiki.eclipse.org/EclipseLink/FAQ/How_to_disable_the_shared_cache%3F),但要准备好承受相当大的性能损失。

否则,您可以使用

以编程方式执行清除缓存
em.getEntityManagerFactory().getCache().evictAll();

您可以将它映射到一个 servlet,这样您就可以在外部调用它 - 如果您的数据库很少在外部修改并且您只是想确保 JPS 会选择新版本,这会更好

【讨论】:

【参考方案2】:

JPA 默认不做任何缓存。您必须明确配置它。我相信这是您选择的架构风格的副作用:REST。我认为缓存发生在 Web 服务器、代理服务器等。我建议您阅读 this 并进行更多调试。

【讨论】:

JPA 没有指定任何关于缓存的内容;由 JPA 实现来指定缓存。 EclipseLink 例如确实默认有缓存。【参考方案3】:

只是一个想法,但是您如何收到您的 EntityManager/Session/whatever?

如果您在一个会话中查询实体,它将在下一个会话中分离,您必须将其合并回持久性上下文中才能再次对其进行管理。

尝试使用分离的实体可能会导致那些非托管异常,您应该重新查询实体,或者您可以尝试使用合并(或类似方法)。

【讨论】:

我尝试了您在使用 em.persist(entity)em.detach(entity) 调用 em.refresh(entity) 时提出的建议。结果是Object: EntityTypeImpl@22507010:MyView [ javaType: class org.my.rest.MyView descriptor: RelationalDescriptor(org.my.rest.MyView --&gt; [DatabaseTable(my_view)]), mappings: 12] is not a known entity type. 刷新时,不需要persist(没有什么要保存,只是接收)。 em.detach() 将分离实体(从而将其从上下文中删除)。只有 em.merge() 应该是将实体添加到上下文的方法。【参考方案4】:

我建议添加一个 @Startup @Singleton 类,该类建立与 PostgreSQL 数据库的 JDBC 连接并使用 LISTEN and NOTIFY 来处理缓存失效。

更新:Here's another interesting approach, using pgq and a collection of workers for invalidation.

失效信号

在正在更新的表上添加一个触发器,该触发器在实体更新时发送NOTIFY。在 PostgreSQL 9.0 及更高版本上,NOTIFY 可以包含有效负载,通常是行 ID,因此您不必使整个缓存无效,只需使已更改的实体无效。在不支持有效负载的旧版本中,您可以将无效的条目添加到带时间戳的日志表中,当助手类获得NOTIFY 时查询该日志表,或者只是使整个缓存无效。

您的助手类现在在触发器发送的NOTIFY 事件上使用LISTENs。当它收到NOTIFY 事件时,它可以使单个缓存条目无效(见下文),或刷新整个缓存。您可以使用PgJDBC's listen/notify support 收听来自数据库的通知。您将需要解开任何受 java.sql.Connection 管理的连接池以访问底层 PostgreSQL 实现,以便将其转换为 org.postgresql.PGConnection 并在其上调用 getNotifications()

作为LISTENNOTIFY 的替代方案,您可以在计时器上轮询更改日志表,并在问题表上设置触发器,将更改的行ID 和更改时间戳附加到更改日志表中。除了需要为每种数据库类型使用不同的触发器之外,这种方法将是可移植的,但它效率低且不及时。它需要频繁的低效轮询,并且仍然存在侦听/通知方法所没有的时间延迟。在 PostgreSQL 中,您可以使用 UNLOGGED 表来稍微降低这种方法的成本。

缓存级别

EclipseLink/JPA 有几个级别的缓存。

一级缓存位于EntityManager 级别。如果一个实体通过persist(...)merge(...)find(...) 等附加到EntityManager,则EntityManager 需要返回该实体的相同实例在同一个会话中再次访问,无论您的应用程序是否仍然具有对它的引用。如果您的数据库内容已更改,则此附加实例将不是最新的。

二级缓存是可选的,位于EntityManagerFactory 级别,是一种更传统的缓存。目前尚不清楚您是否启用了二级缓存。检查您的 EclipseLink 日志和您的 persistence.xml。您可以通过EntityManagerFactory.getCache() 访问二级缓存;见Cache

@thedayofcondor 展示了如何刷新二级缓存:

em.getEntityManagerFactory().getCache().evictAll();

但您也可以使用 evict(java.lang.Class cls, java.lang.Object primaryKey) 调用逐出单个对象:

em.getEntityManagerFactory().getCache().evict(theClass, thePrimaryKey);

您可以从 @Startup @Singleton NOTIFY 侦听器中使用它来仅使那些已更改的条目无效。

一级缓存并不容易,因为它是应用程序逻辑的一部分。您将想了解EntityManager、附加和分离实体等如何工作。一种选择是始终对有问题的表使用分离的实体,在获取实体时使用新的EntityManager。这个问题:

Invalidating JPA EntityManager session

对处理实体管理器缓存的失效进行了有用的讨论。但是,EntityManager 缓存不太可能是您的问题,因为 RESTful Web 服务通常使用短的EntityManager 会话来实现。仅当您使用扩展的持久性上下文,或者您正在创建和管理自己的 EntityManager 会话而不是使用容器管理的持久性时,这才可能成为问题。

【讨论】:

原来我问的问题没有正确反映我面临的潜在问题(请参阅 OP 中的更新)。但是,这无疑是对我提出的问题的最佳解释。

以上是关于后端数据库异步更改时如何刷新JPA实体?的主要内容,如果未能解决你的问题,请参考以下文章

取消引用的休眠 (JPA) 实体会发生啥情况?

从后端异步更新/通知 HTML,无需 AJAX 轮询

使用 Spring JPA “刷新”实体后未反映更改

直接从querydsl更新数据时如何刷新Spring JPA?

EntityManager 刷新

Ajax异步传值以及后端接收参数的4种方式