I've got a fairly simple criteria query that fetches child collections, like so:
我有一个相当简单的条件查询来获取子集合,如下所示:
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>();
Using NH Prof, I've verified that this makes just one round trip to the database (as expected) with a cold cache; however, on successive executions, it retrieves only the Order
from the cache and then hits the database with a SELECT(N+1) for every child entity in the graph, as in:
使用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;
And so on and so forth. Clearly it's not caching the whole query or graph, only the root entity. The first "cached query" line actually has all of the join
conditions that it's supposed to - it's definitely caching the query itself correctly, just not the entities, apparently.
等等等等。显然,它不是缓存整个查询或图形,只缓存根实体。第一个“缓存查询”行实际上具有它应该具有的所有连接条件 - 它肯定正确地缓存了查询本身,显然不是实体。
I've tried this using the SysCache, SysCache2, and even HashTable cache providers and I always seem to get this same behaviour (NH version 3.2.0).
我已经尝试过使用SysCache,SysCache2甚至是HashTable缓存提供程序,我似乎总是得到同样的行为(NH版本3.2.0)。
Googling turned up a number of ancient issues, such as:
谷歌搜索出现了一些古老的问题,例如:
- NH-195: Child collections are not being stored in the second level cache
- Syscache2 2nd level cache: Child coll. objects requeried
- Weird differences between SysCache and SysCache2
- NHibernate – Beware of inadvisably applied caching strategies (Ayende - of course he only bothers to mention what not to do, not how to fix it...)
NH-195:子集合未存储在二级缓存中
Syscache2二级缓存:Child coll。对象被重新获得
SysCache和SysCache2之间的奇怪差异
NHibernate - 谨防不应用的缓存策略(Ayende - 当然他只是提到不该做什么,而不是如何解决它......)
However, these all seem to have been fixed a long time ago, and I get the same bad behaviour regardless of which provider I use.
但是,这些似乎都已经修复了很久以前,无论我使用哪个提供程序,我都会遇到相同的不良行为。
I've read through the nhibernate.info documentation on SysCache and SysCache2 and there doesn't seem to be anything I'm missing. I've tried adding cacheRegion
lines to the Web.config
file for all tables involved in the query, but it doesn't change anything (and AFAIK those elements are just to invalidate the cache, so they shouldn't matter anyway).
我已经阅读了关于SysCache和SysCache2的nhibernate.info文档,似乎没有任何我遗漏的内容。我已经尝试将cacheRegion行添加到查询中涉及的所有表的Web.config文件中,但它不会更改任何内容(而AFAIK这些元素只是为了使缓存无效,所以无论如何它们都无关紧要)。
With all of these super-old issues that all seem to be fixed/resolved, I figure this can't possibly still be a bug in NHibernate, it must be something that I'm doing wrong. But what?
所有这些超级老问题似乎都得到修复/解决,我认为这不可能仍然是NHibernate中的一个错误,它必定是我做错的事情。但是什么?
Is there something special I need to do when combining fetch instructions in NHibernate with the second-level cache? What am I missing here?
将NHibernate中的提取指令与二级缓存组合时,我需要做些什么特别的事情吗?我在这里想念的是什么?
2 个解决方案
#1
36
I did manage to figure this out, so other folks can finally get a straight answer:
我确实设法解决了这个问题,所以其他人终于可以得到一个直接的答案:
To sum it up, I've been confused for a while on the difference between the second-level cache and the query cache; Jason's answer is technically correct but it somehow didn't click for me. Here is how I would explain it:
总结一下,我对第二级缓存和查询缓存之间的区别感到困惑;杰森的答案在技术上是正确的,但它不知何故没有点击我。以下是我将如何解释它:
-
The query cache keeps track of which entities are emitted by a query. It does not cache the entire result set. It's the equivalent of doing a
Session.Load
on a lazy-loaded entity; it knows/expects that one exists but doesn't track any other information about it unless specifically asked, at which point it will actually load the real entity.查询缓存会跟踪查询发出的实体。它不会缓存整个结果集。它相当于在延迟加载的实体上执行Session.Load;它知道/期望一个存在,但不会跟踪任何其他有关它的信息,除非特别要求,此时它将实际加载真实实体。
-
The second-level cache tracks the actual data for each entity. When NHibernate needs to load any entity by its ID (by virtue of a
Session.Load
,Session.Get
, lazy-loaded relationship, or, in the case above, an entity "reference" that's part of a cached query), it will look in the second-level cache first.二级缓存跟踪每个实体的实际数据。当NHibernate需要通过其ID加载任何实体时(凭借Session.Load,Session.Get,延迟加载的关系,或者在上面的情况下,是一个实体“引用”,它是缓存查询的一部分),它将首先查看二级缓存。
Of course this makes perfect sense in hindsight, but it's not so obvious when you hear the terms "query cache" and "second-level cache" being used almost interchangeably in so many places.
当然,事后看来这很有道理,但是当你听到“查询缓存”和“二级缓存”在很多地方几乎可以互换使用时,它就不那么明显了。
Essentially there are two sets of two settings each that you need to configure in order to see the expected results with query caching:
基本上,您需要配置两组两个设置,以便通过查询缓存查看预期结果:
1. Enable both caches
In XML configuration, this means adding the following two lines:
在XML配置中,这意味着添加以下两行:
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache" >true</property>
In Fluent NHibernate, it's this:
在Fluent NHibernate中,它是这样的:
.Cache(c => c
.UseQueryCache()
.UseSecondLevelCache()
.ProviderClass<SysCacheProvider>())
Please note the UseSecondLevelCache
above because (at the time of this posting) it is never mentioned on the Fluent NHibernate wiki page; there are several examples of enabling the query cache but not the second-level cache!
请注意上面的UseSecondLevelCache,因为(在发布时)它从未在Fluent NHibernate wiki页面上提及过;有几个启用查询缓存但不启用二级缓存的示例!
2. Enable caching for each entity
Just enabling the second-level cache does pretty much nothing, and this is where I got tripped up. The second-level cache has to be not only enabled but configured for every single individual entity class that you want cached.
只是启用二级缓存几乎没有什么,这就是我被绊倒的地方。不仅要启用二级缓存,还要为要缓存的每个单个实体类配置二级缓存。
In XML, this is done inside the <class>
element:
在XML中,这是在
<cache usage="read-write"/>
In Fluent NHibernate (non-automap), it's done in the ClassMap
constructor or wherever you put the rest of your mapping code:
在Fluent NHibernate(非自动化)中,它在ClassMap构造函数中完成,或者在您放置其余映射代码的任何地方完成:
Cache.ReadWrite().Region("Configuration");
This has to be done for every entity that is going to be cached. It's probably possible to set up in one place as a convention, but then you pretty much miss out on the ability to use regions (and in most systems you don't want to cache transactional data as much as configuration data).
必须为将要缓存的每个实体执行此操作。可能可以在一个地方设置作为约定,但是你几乎可以错过使用区域的能力(在大多数系统中,你不希望像配置数据一样缓存事务数据)。
And that's it. It's really not that hard to do but surprisingly difficult to find a good, complete example, especially for FNH.
就是这样。这真的不是那么难,但却很难找到一个好的,完整的例子,特别是对于FNH。
One last point: The natural consequence of this is that it makes eager join/fetching strategies very unpredictable when used with the query cache. Apparently, if NHibernate sees that a query is cached, it will make no effort whatsoever to check first if all or even any of the actual entities are cached. It pretty much just assumes that they are, and tries to load each one up individually.
最后一点:这样做的自然结果是,当与查询缓存一起使用时,它使得急切的加入/获取策略非常难以预测。显然,如果NHibernate看到一个查询被缓存,那么如果所有甚至任何实际实体都被缓存,它将毫不费力地检查。它几乎只是假设它们是,并试图单独加载每个。
This is the reason for the SELECT N+1 disaster; it wouldn't be that big of a deal if NH noticed that the entities weren't in the second-level cache and just executed the query normally, as written, with fetches and futures and so on. But it doesn't do that; instead it tries to load every entity, and its relations, and its sub-relations, and its sub-sub-relations, and so on, one at a time.
这就是SELECT N + 1灾难的原因;如果NH注意到实体不在二级缓存中并且正常执行查询,正如所写,带有提取和期货等等,那么这将不是那么大的交易。但它没有那样做;相反,它试图一次一个地加载每个实体及其关系,子关系及其子子关系等。
So there is almost no point in using the query cache unless you've explicitly enabled caching for all of the entities in the entire graph, and even then, you'll want to be very careful (by way of expirations, dependencies, etc.) that cached queries don't outlast the entities that they are supposed to retrieve, otherwise you will just end up making the performance worse.
因此,除非您已经为整个图中的所有实体显式启用了缓存,否则几乎没有必要使用查询缓存,即使这样,您也要非常小心(通过过期,依赖等等)。 )缓存的查询不会超出他们应该检索的实体,否则你最终会使性能变差。
#2
4
a cached query only stores the IDs of the entities, not the values of the entity. within a cached entity only the IDs of related entities are cached. therefore if you don't cache all the involved entities as well as marking the related entities as cached you could end up with select n+1.
缓存查询仅存储实体的ID,而不存储实体的值。在缓存实体中,仅缓存相关实体的ID。因此,如果您不缓存所有涉及的实体以及将相关实体标记为缓存,则最终可能会选择n + 1。
#1
36
I did manage to figure this out, so other folks can finally get a straight answer:
我确实设法解决了这个问题,所以其他人终于可以得到一个直接的答案:
To sum it up, I've been confused for a while on the difference between the second-level cache and the query cache; Jason's answer is technically correct but it somehow didn't click for me. Here is how I would explain it:
总结一下,我对第二级缓存和查询缓存之间的区别感到困惑;杰森的答案在技术上是正确的,但它不知何故没有点击我。以下是我将如何解释它:
-
The query cache keeps track of which entities are emitted by a query. It does not cache the entire result set. It's the equivalent of doing a
Session.Load
on a lazy-loaded entity; it knows/expects that one exists but doesn't track any other information about it unless specifically asked, at which point it will actually load the real entity.查询缓存会跟踪查询发出的实体。它不会缓存整个结果集。它相当于在延迟加载的实体上执行Session.Load;它知道/期望一个存在,但不会跟踪任何其他有关它的信息,除非特别要求,此时它将实际加载真实实体。
-
The second-level cache tracks the actual data for each entity. When NHibernate needs to load any entity by its ID (by virtue of a
Session.Load
,Session.Get
, lazy-loaded relationship, or, in the case above, an entity "reference" that's part of a cached query), it will look in the second-level cache first.二级缓存跟踪每个实体的实际数据。当NHibernate需要通过其ID加载任何实体时(凭借Session.Load,Session.Get,延迟加载的关系,或者在上面的情况下,是一个实体“引用”,它是缓存查询的一部分),它将首先查看二级缓存。
Of course this makes perfect sense in hindsight, but it's not so obvious when you hear the terms "query cache" and "second-level cache" being used almost interchangeably in so many places.
当然,事后看来这很有道理,但是当你听到“查询缓存”和“二级缓存”在很多地方几乎可以互换使用时,它就不那么明显了。
Essentially there are two sets of two settings each that you need to configure in order to see the expected results with query caching:
基本上,您需要配置两组两个设置,以便通过查询缓存查看预期结果:
1. Enable both caches
In XML configuration, this means adding the following two lines:
在XML配置中,这意味着添加以下两行:
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache" >true</property>
In Fluent NHibernate, it's this:
在Fluent NHibernate中,它是这样的:
.Cache(c => c
.UseQueryCache()
.UseSecondLevelCache()
.ProviderClass<SysCacheProvider>())
Please note the UseSecondLevelCache
above because (at the time of this posting) it is never mentioned on the Fluent NHibernate wiki page; there are several examples of enabling the query cache but not the second-level cache!
请注意上面的UseSecondLevelCache,因为(在发布时)它从未在Fluent NHibernate wiki页面上提及过;有几个启用查询缓存但不启用二级缓存的示例!
2. Enable caching for each entity
Just enabling the second-level cache does pretty much nothing, and this is where I got tripped up. The second-level cache has to be not only enabled but configured for every single individual entity class that you want cached.
只是启用二级缓存几乎没有什么,这就是我被绊倒的地方。不仅要启用二级缓存,还要为要缓存的每个单个实体类配置二级缓存。
In XML, this is done inside the <class>
element:
在XML中,这是在
<cache usage="read-write"/>
In Fluent NHibernate (non-automap), it's done in the ClassMap
constructor or wherever you put the rest of your mapping code:
在Fluent NHibernate(非自动化)中,它在ClassMap构造函数中完成,或者在您放置其余映射代码的任何地方完成:
Cache.ReadWrite().Region("Configuration");
This has to be done for every entity that is going to be cached. It's probably possible to set up in one place as a convention, but then you pretty much miss out on the ability to use regions (and in most systems you don't want to cache transactional data as much as configuration data).
必须为将要缓存的每个实体执行此操作。可能可以在一个地方设置作为约定,但是你几乎可以错过使用区域的能力(在大多数系统中,你不希望像配置数据一样缓存事务数据)。
And that's it. It's really not that hard to do but surprisingly difficult to find a good, complete example, especially for FNH.
就是这样。这真的不是那么难,但却很难找到一个好的,完整的例子,特别是对于FNH。
One last point: The natural consequence of this is that it makes eager join/fetching strategies very unpredictable when used with the query cache. Apparently, if NHibernate sees that a query is cached, it will make no effort whatsoever to check first if all or even any of the actual entities are cached. It pretty much just assumes that they are, and tries to load each one up individually.
最后一点:这样做的自然结果是,当与查询缓存一起使用时,它使得急切的加入/获取策略非常难以预测。显然,如果NHibernate看到一个查询被缓存,那么如果所有甚至任何实际实体都被缓存,它将毫不费力地检查。它几乎只是假设它们是,并试图单独加载每个。
This is the reason for the SELECT N+1 disaster; it wouldn't be that big of a deal if NH noticed that the entities weren't in the second-level cache and just executed the query normally, as written, with fetches and futures and so on. But it doesn't do that; instead it tries to load every entity, and its relations, and its sub-relations, and its sub-sub-relations, and so on, one at a time.
这就是SELECT N + 1灾难的原因;如果NH注意到实体不在二级缓存中并且正常执行查询,正如所写,带有提取和期货等等,那么这将不是那么大的交易。但它没有那样做;相反,它试图一次一个地加载每个实体及其关系,子关系及其子子关系等。
So there is almost no point in using the query cache unless you've explicitly enabled caching for all of the entities in the entire graph, and even then, you'll want to be very careful (by way of expirations, dependencies, etc.) that cached queries don't outlast the entities that they are supposed to retrieve, otherwise you will just end up making the performance worse.
因此,除非您已经为整个图中的所有实体显式启用了缓存,否则几乎没有必要使用查询缓存,即使这样,您也要非常小心(通过过期,依赖等等)。 )缓存的查询不会超出他们应该检索的实体,否则你最终会使性能变差。
#2
4
a cached query only stores the IDs of the entities, not the values of the entity. within a cached entity only the IDs of related entities are cached. therefore if you don't cache all the involved entities as well as marking the related entities as cached you could end up with select n+1.
缓存查询仅存储实体的ID,而不存储实体的值。在缓存实体中,仅缓存相关实体的ID。因此,如果您不缓存所有涉及的实体以及将相关实体标记为缓存,则最终可能会选择n + 1。