通过批量读取避免 N+One 选择和来自 eclipselink 的无效结果
Posted
技术标签:
【中文标题】通过批量读取避免 N+One 选择和来自 eclipselink 的无效结果【英文标题】:Avoiding N+One selects and Invalid results from eclipselink with batch read 【发布时间】:2011-08-10 18:09:51 【问题描述】:我正在尝试减少我的应用程序产生的 n+1 选择的数量,该应用程序使用 EclipseLink 作为 ORM,并且在尽可能多的地方我尝试将批量读取提示添加到查询中。在应用程序的许多地方,我并不总是确切地知道我将要遍历哪些关系(我的视图根据用户偏好显示字段)。那时我想运行一个查询来填充我的对象的所有这些关系。
我的梦想是调用类似 ReadAllRelationshipsQuery(Collection,RelationshipName) 并填充所有这些项目,以便以后调用:
Collection.get(0).getMyStuff 已经被填充并且不会导致数据库查询。我怎样才能做到这一点?我愿意编写任何我需要的代码,但我找不到与 eclipselink 框架配合使用的方法?
我为什么不批量读取所有可能的字段并让它们延迟加载?我发现实现批量读取的批量值持有者在 eclipselink 缓存中表现不佳。如果批量读取值持有者未“评估”并最终进入 eclipse 链接缓存,则它可能会变得陈旧并返回不正确的数据(此行为被记录为 eclipselink 错误但被拒绝...) 编辑:我在这里找到了该错误的链接:https://bugs.eclipse.org/bugs/show_bug.cgi?id=326197
如何避免对我已经引用的对象进行 N+1 次选择?
【问题讨论】:
【参考方案1】:您可以通过三种基本方式将数据从基于 JPA 的解决方案加载到对象中。它们是:
-
通过对象遍历动态加载(例如 myObject.getMyCollection().get())。
通过使用 JPA QL 动态预取来加载对象图(例如,the Oracle JPA tutorial 中所述的 FETCH JOIN)
通过设置获取模式加载(Is there a way to change the JPA fetch type on a method?)
这些都有优点和缺点。
-
通过对象横向动态加载将生成更多(高度针对性的查询)。这些查询通常很小(不是大型 SQL 语句,但可能会加载大量数据)并且往往与二级缓存很好地配合使用,但您可以获得大量的小查询。
使用 JPA QL 进行预取可以准确地为您提供所需的内容,但前提是您知道自己想要什么。
将获取模式设置为 EAGER 会自动为您加载大量数据,但根据配置和使用情况,这实际上可能没有多大帮助(或者可能会使事情变得更糟),因为您可能最终拖了很多数据库中的数据进入您的应用,这是您意想不到的。
无论如何,我强烈建议将 p6spy (http://sourceforge.net/projects/p6spy/) 与任何基于 JPA 的应用程序结合使用,以了解调整的效果。
不幸的是,JPA 让一些事情变得简单而一些事情变得困难——主要是你使用的副作用。例如,您可以通过将 fetch 模式设置为 eager 来解决一个问题,然后创建另一个问题,即 eager fetch 拉入过多数据。 EclipseLink 确实提供了工具来帮助解决这个问题 (EclipseLink Performance Tools)
理论上,如果您愿意,可以使用Apache BeanUtils 之类的东西编写一个通用的JavaBean 属性遍历器。通常只需在集合上调用 size() 之类的方法就足以强制它加载(尽管使用集合批量获取大小可能会使事情变得有点复杂)。
要特别注意的一件事是会话的范围和缓存的使用 (EclipseLink cache)。
您的帖子中不清楚的是会话的范围。会话是一次性事件(例如网页请求)还是长时间运行的事情(例如经典客户端/服务器 GUI 应用程序)?
【讨论】:
缓存在整个应用程序中是持久的,缓存的对象可以在这个应用程序中保持缓存数小时。 这是一个很好的解释,我不明白在没有成百上千的小查询的情况下查询初始集合后,这些中的任何一个如何允许我获取和填充一组关系。我确实知道在请求的后面哪些关系会成为潜在的陷阱,我很想为所有这些添加批量读取提示,但是唉...... 然后 eclipselink 缓存可以很容易地开始保持陈旧的关系。 EclipseLink 缓存看起来确实功能丰富但也非常复杂 (wiki.eclipse.org/Introduction_to_Cache_%28ELUG%29)。比批量读取提示更重要的是为实体配置只读与读/写。如果幸运的话,很多这些小读取都是只读的,这意味着您可以设置只读标志,它们将被加载到缓存中一次,您就可以开始了。顺便说一句,Hibernate(另一个 JPA 提供程序)确实支持一个很好的可插入缓存模型,我在 EclipseLink 上似乎没有看到。【参考方案2】:如果你不知道你需要什么关系,那么优化关系的检索是非常困难的。
如果您的应用程序正在请求它想要的关系,那么您必须在某种程度上知道您需要哪些关系,并且应该能够在您的对象查询中优化这些关系。
有关关系优化技术的概述,请参阅
http://java-persistence-performance.blogspot.com/2010/08/batch-fetching-optimizing-object-graph.html
对于 Batch Fetching,有 JOIN、EXISTS 和 IN 三种类型。您概述的对影响缓存批处理关系的原始查询的数据更改的问题仅适用于 JOIN 和 EXISTS,并且仅当您具有基于 updateale 字段的选择条件时(如果您正在优化的查询在 id 上,或所有实例你还行)。 IN 批量抓取没有这个问题,所以你可以对所有的关系使用 IN 批量抓取而不会有这个问题。
ReadAllRelationshipsQuery(Collection,RelationshipName)
怎么样,
Query query = em.createQuery("Select o from MyObject o where o.id in :ids");
query.setParameter(ids, ids);
query.setHint("eclipselink.batch", relationship);
【讨论】:
【参考方案3】:如果您知道所有可能的关系和用户偏好,为什么不在执行之前动态构建 JPQL 字符串(或 Criteria)?
喜欢:
String sql = "SELECT u FROM User u"; //use a StringBuilder, this is just for simplity's sake
if(loadAdress)
sql += " LEFT OUTER JOIN u.address as a"; //fetch join and left outer join have the same result in many cases, except that with left outer join you could load associations of address as well
...
编辑:由于结果将是一个叉积,因此您应该遍历实体并删除重复项。
【讨论】:
我不知道获得初始集合后将访问的所有关系。 所以你不知道你正在操作的模型?可能会在运行时创建无法使用 where 条件建模的关联(例如SELECT u, a FROM User u, Address a WHERE u.address = a
)?【参考方案4】:
在查询中,使用 FETCH JOIN 预取关系。
请记住,生成的行将是所有选定行的叉积,这很容易比 N+1 查询工作更多。
【讨论】:
这不回答问题,获取数据的查询已经运行。以上是关于通过批量读取避免 N+One 选择和来自 eclipselink 的无效结果的主要内容,如果未能解决你的问题,请参考以下文章
批量下载所有LYNDA课程的方法 (how to download full Lynda.com course in just one set-up)