我有一个相当简单的条件查询来获取子集合,如下所示:
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)。
谷歌搜索出现了许多古老的问题,例如:然而,这些似乎都很久以前就被解决了,无论我使用哪个提供商,我都会遇到同样的不良行为。
我已阅读nhibernate.info documentation on SysCache and SysCache2并且似乎没有任何我遗漏的内容。我已尝试将cacheRegion
行添加到查询中涉及的所有表的Web.config
文件中,但它不会更改任何内容(而AFAIK这些元素只是无效缓存,所以无论如何它们都无关紧要。)
所有这些超级老问题似乎都得到了修复/解决,我认为这不可能仍然是NHibernate中的一个错误,它必须是我正在做的事情错误。但是什么?
将NHibernate中的提取指令与二级缓存组合时,我需要做些什么特别的事情吗?我在这里缺少什么?
答案 0 :(得分:37)
我确实设法解决了这个问题,所以其他人终于可以得到一个直接的答案:
总结一下,我对第二级缓存和查询缓存之间的区别感到困惑;杰森的答案在技术上是正确的,但它不知何故没有点击我。以下是我将如何解释它:
查询缓存会跟踪查询发出的实体。它不缓存整个结果集。这相当于在延迟加载的实体上执行Session.Load
;它知道/期望一个存在但不跟踪任何有关它的其他信息,除非特别要求,此时它将实际加载真实的实体。
二级缓存会跟踪每个实体的实际数据。当NHibernate需要通过其ID加载任何实体时(凭借Session.Load
,Session.Get
,延迟加载的关系,或者在上面的情况下,是一个实体“引用”,它是缓存查询的一部分),它将首先查看二级缓存。
当然,事后看来这很有道理,但是当你听到“查询缓存”和“二级缓存”在很多地方几乎可以互换使用时,它就不那么明显了。
基本上,您需要配置两组两个设置,以便通过查询缓存查看预期结果:
在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 page上提到从不;有几个启用查询缓存而不是二级缓存的示例!
只是启用二级缓存几乎没有什么,这就是我被绊倒的地方。必须不仅启用二级缓存,而且还要为要缓存的每个单个实体类配置。
在XML中,这是在<class>
元素内完成的:
<cache usage="read-write"/>
在Fluent NHibernate(非自动化)中,它在ClassMap
构造函数中完成,或者在您放置其余映射代码的任何地方完成:
Cache.ReadWrite().Region("Configuration");
必须为要缓存的每个实体执行此操作。可能可以在一个地方设置作为约定,但是你几乎可以错过使用区域的能力(在大多数系统中,你不希望像配置数据一样缓存事务数据)。
就是这样。这真的不是那么难,但却很难找到一个好的,完整的例子,特别是对于FNH。
最后一点:这样做的自然结果是在与查询缓存一起使用时,急切的加入/获取策略非常难以预测。显然,如果NHibernate看到查询被缓存,它将会如果所有甚至任何的实际实体都被缓存,请首先检查无需付费。它几乎只是假设它们是,并试图单独加载每个。
这是SELECT N + 1灾难的原因;如果NH注意到实体不在二级缓存中并且只是正常执行查询,就像书面一样,使用提取和期货等等,那就没什么大不了的了。但它没有那样做;相反,它试图加载每个实体及其关系,其子关系及其子子关系等等,一次一个。
因此,使用查询缓存几乎没有任何意义,除非您已明确启用整个图中所有实体的缓存,即便如此,您我希望非常小心(通过到期,依赖等)缓存查询不会比它们应该检索的实体更久,否则你最终会使性能变差。
答案 1 :(得分:4)
缓存查询仅存储实体的ID,而不是实体的值。在缓存实体中,仅缓存相关实体的ID。因此,如果您不缓存所有涉及的实体以及将相关实体标记为缓存,则最终可能会选择n + 1.