15

我有一个相当简单的标准查询来获取子集合,如下所示:

var order = Session.CreateCriteria<Order>()
    .Add(Restrictions.Eq("Id", id))
    .SetFetchMode("Customer", FetchMode.Eager)
    .SetFetchMode("Products", FetchMode.Eager)
    .SetFetchMode("Products.Category", FetchMode.Eager)
    .SetCacheable(true)
    .UniqueResult<Order>();

使用 NH Prof,我已经验证了这只需使用冷缓存来一次往返数据库(如预期的那样);但是,在连续执行时,它仅从Order缓存中检索 ,然后为图中的每个子实体使用 SELECT(N+1) 访问数据库,如下所示:

Cached query: SELECT ... FROM Order this_ left outer join Customer customer2 [...]
SELECT ... FROM Customer WHERE Id = 123;
SELECT ... FROM Products WHERE Id = 500;
SELECT ... FROM Products WHERE Id = 501;
...
SELECT ... FROM Categories WHERE Id = 3;

等等等等。显然,它没有缓存整个查询或图形,只缓存根实体。第一个“缓存查询”行实际上具有join它应该具有的所有条件 - 它肯定是正确地缓存查询本身,而不是实体,显然。

我已经尝试过使用 SysCache、SysCache2 甚至 HashTable 缓存提供程序,但我似乎总是得到同样的行为(NH 版本 3.2.0)。

谷歌搜索发现了许多古老的问题,例如:

但是,这些似乎都是很久以前修复的,无论我使用哪个提供程序,我都会遇到同样的不良行为。

我已经阅读了关于 SysCache 和 SysCache2 的 nhibernate.info 文档,似乎没有任何我遗漏的东西。我已经尝试为查询中涉及的所有表添加cacheRegion行到Web.config文件中,但它不会改变任何东西(并且AFAIK这些元素只是为了使缓存无效,所以它们无论如何都不重要)。

由于所有这些似乎都已修复/解决的超级老问题,我认为这不可能仍然是 NHibernate 中的错误,它一定是我做错了。但是什么?

将 NHibernate 中的 fetch 指令与二级缓存相结合时,我需要做些什么特别的事情吗?我在这里想念什么?

4

2 回答 2

37

我确实设法弄清楚了这一点,所以其他人终于可以得到一个直接的答案:

总结一下,对于二级缓存和查询缓存的区别,我已经困惑了一段时间;杰森的回答在技术上是正确的,但不知何故并没有为我点击。这是我将如何解释它:

  • 查询缓存跟踪查询发出的实体。它不会缓存整个结果集。这相当于Session.Load对延迟加载的实体执行 a ;它知道/期望一个存在,但除非特别要求,否则它不会跟踪任何其他信息,此时它将实际加载真实实体。

  • 二级缓存跟踪每个实体的实际数据。当 NHibernate 需要通过其 ID 加载任何实体时(通过Session.Load, Session.Get, 延迟加载关系,或者在上面的例子中,实体“引用”是缓存查询的一部分),它会在第二级查找先缓存。

当然,事后看来这完全有道理,但是当您听到“查询缓存”和“二级缓存”这两个术语在很多地方几乎可以互换使用时,这并不是那么明显。

本质上,您需要配置两组,每组两个设置,以便通过查询缓存查看预期结果:

1.启用两个缓存

在 XML 配置中,这意味着添加以下两行:

<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache" >true</property>

在 Fluent NHibernate 中,是这样的:

.Cache(c => c
    .UseQueryCache()
    .UseSecondLevelCache()
    .ProviderClass<SysCacheProvider>())

请注意UseSecondLevelCache上述内容,因为(在本文发布时)Fluent NHibernate wiki 页面上从未提及它;有几个启用查询缓存但不启用二级缓存的示例!

2.为每个实体启用缓存

仅启用二级缓存几乎没有任何作用,这就是我被绊倒的地方。二级缓存不仅要启用,而且要为您想要缓存的每个单独的实体类进行配置

在 XML 中,这是在<class>元素内部完成的:

<cache usage="read-write"/>

在 Fluent NHibernate(非自动映射)中,它是在ClassMap构造函数中完成的,或者在您放置其余映射代码的任何地方完成:

Cache.ReadWrite().Region("Configuration");

必须对每个要缓存的实体执行此操作。作为惯例,可能可以在一个地方设置,但是您几乎错过了使用区域的能力(在大多数系统中,您不希望缓存事务数据和配置数据一样多)。

就是这样。确实不难做到,但要找到一个好的、完整的例子却出奇地难,尤其是对于 FNH。


最后一点:这样做的自然结果是,当与查询缓存一起使用时,它会使急切的连接/获取策略变得非常不可预测。显然,如果 NHibernate 发现查询被缓存,它不会首先检查是否所有或什至任何实际实体都被缓存。它几乎只是假设它们是,并尝试单独加载每个。

这就是 SELECT N+1 灾难的原因;如果 NH 注意到实体不在二级缓存中,并且只是按照所写的方式正常执行查询,并使用 fetches 和 futures 等等,这不会有什么大不了的。但它不会那样做;相反,它尝试一次加载每个实体及其关系、子关系和子子关系,依此类推。

因此,使用查询缓存几乎没有意义,除非您为整个图中的所有实体显式启用缓存,即使那样,您也需要非常小心(通过到期、依赖关系等)。 ) 缓存查询不会比它们应该检索的实体更持久,否则您最终只会使性能变得更糟。

于 2012-01-07T01:01:06.587 回答
4

缓存查询仅存储实体的 ID,而不存储实体的值。在缓存实体中,仅缓存相关实体的 ID。因此,如果您不缓存所有涉及的实体以及将相关实体标记为已缓存,您最终可能会选择 n+1。

于 2012-01-06T16:51:57.890 回答