Skip to content

Commit 8b74774

Browse files
gokhanabataygokhanabataymaca88fredericDelaporte
authored
Set which entities classes should never be cached, even indirectly (#2744)
Co-authored-by: gokhanabatay <gokhan.abatay@gmail.com> Co-authored-by: maca88 <bostjan.markezic@siol.net> Co-authored-by: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com>
1 parent 21859e9 commit 8b74774

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1180
-190
lines changed

doc/reference/modules/configuration.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,24 @@ var session = sessions.OpenSession(conn);
722722
</para>
723723
</entry>
724724
</row>
725+
<row>
726+
<entry>
727+
<literal>query.throw_never_cached</literal>
728+
</entry>
729+
<entry>
730+
Should queries set as cacheable raise an error if they reference an entity using the cache
731+
<xref linkend="performance-cache-never" /> (the default is enabled).
732+
<para>
733+
<emphasis role="strong">eg.</emphasis>
734+
<literal>true</literal> | <literal>false</literal>
735+
</para>
736+
<para>
737+
Disabling this setting causes NHibernate to ignore the caching of such queries without
738+
raising an error. Furthermore NHibernate will log a warning on cacheable queries
739+
referencing an entity using the <literal>never</literal> cache strategy.
740+
</para>
741+
</entry>
742+
</row>
725743
<row>
726744
<entry>
727745
<literal>query.factory_class</literal>

doc/reference/modules/performance.xml

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -733,17 +733,18 @@ using(var iter = session
733733
<area id="cache1" coords="2 70"/>
734734
<area id="cache2" coords="3 70"/>
735735
</areaspec>
736-
<programlisting><![CDATA[<cache
737-
usage="read-write|nonstrict-read-write|read-only"
736+
<programlisting><![CDATA[<cache
737+
usage="read-write|nonstrict-read-write|read-only|never"
738738
region="RegionName"
739739
/>]]></programlisting>
740740
<calloutlist>
741741
<callout arearefs="cache1">
742742
<para>
743743
<literal>usage</literal> specifies the caching strategy:
744744
<literal>read-write</literal>,
745-
<literal>nonstrict-read-write</literal> or
746-
<literal>read-only</literal>
745+
<literal>nonstrict-read-write</literal>,
746+
<literal>read-only</literal> or
747+
<literal>never</literal>
747748
</para>
748749
</callout>
749750
<callout arearefs="cache2">
@@ -824,6 +825,26 @@ using(var iter = session
824825

825826
</sect2>
826827

828+
<sect2 id="performance-cache-never">
829+
<title>Strategy: never</title>
830+
831+
<para>
832+
By default, without a cache configuration, entities are not cacheable. But they may still be referenced
833+
in cacheable queries, which results will then be cached according to the values these non cacheable
834+
entities have. So, their data may be indirectly cached through cacheable queries.
835+
</para>
836+
837+
<para>
838+
By using the cache strategy <literal>never</literal>, such indirect caching of these entities data will
839+
be forbidden by NHibernate. Setting as cacheable a query referencing entities with strategy
840+
<literal>never</literal> will be treated as an error by default. Alternatively, the
841+
<literal>query.throw_never_cached</literal> <link linkend="configuration-optional">setting</link> can be
842+
set to <literal>false</literal>: instead of raising an error, it will disable the query cache on such
843+
queries, and log a warning.
844+
</para>
845+
846+
</sect2>
847+
827848
<para>
828849
The following table shows which providers are compatible with which concurrency strategies.
829850
</para>

src/NHibernate.Test/App.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<property name="cache.use_query_cache">true</property>
3939

4040
<property name="query.startup_check">false</property>
41+
<property name="query.throw_never_cached">true</property>
4142
<property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>
4243

4344
<property name="adonet.batch_size">10</property>
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System.Collections.Generic;
12+
using System.Linq;
13+
using System.Reflection;
14+
using NHibernate.Cache;
15+
using NHibernate.Cfg;
16+
using NHibernate.Impl;
17+
using NHibernate.Linq;
18+
using NHibernate.Test.SecondLevelCacheTests;
19+
using NSubstitute;
20+
using NUnit.Framework;
21+
22+
namespace NHibernate.Test.SecondLevelCacheTest
23+
{
24+
using System.Threading.Tasks;
25+
using System.Threading;
26+
[TestFixture]
27+
public class NeverCachedEntityTestsAsync : TestCase
28+
{
29+
protected override string CacheConcurrencyStrategy => null;
30+
protected override string MappingsAssembly => "NHibernate.Test";
31+
32+
protected override string[] Mappings => new[] { "SecondLevelCacheTest.Item.hbm.xml" };
33+
34+
protected override void Configure(Configuration configuration)
35+
{
36+
configuration.SetProperty(Environment.CacheProvider, typeof(HashtableCacheProvider).AssemblyQualifiedName);
37+
configuration.SetProperty(Environment.UseQueryCache, "true");
38+
}
39+
40+
[Test]
41+
public async Task NeverInvalidateEntitiesAsync()
42+
{
43+
var debugSessionFactory = (DebugSessionFactory) Sfi;
44+
45+
var cache = Substitute.For<UpdateTimestampsCache>(Sfi.Settings, new Dictionary<string, string>());
46+
47+
var updateTimestampsCacheField = typeof(SessionFactoryImpl).GetField(
48+
"updateTimestampsCache",
49+
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
50+
51+
updateTimestampsCacheField.SetValue(debugSessionFactory.ActualFactory, cache);
52+
53+
//"Received" assertions can not be used since the collection is reused and cleared between calls.
54+
//The received args are cloned and stored
55+
var preInvalidations = new List<IReadOnlyCollection<string>>();
56+
var invalidations = new List<IReadOnlyCollection<string>>();
57+
58+
await (cache.PreInvalidateAsync(Arg.Do<IReadOnlyCollection<string>>(x => preInvalidations.Add(x.ToList())), CancellationToken.None));
59+
await (cache.InvalidateAsync(Arg.Do<IReadOnlyCollection<string>>(x => invalidations.Add(x.ToList())), CancellationToken.None));
60+
61+
using (var session = OpenSession())
62+
{
63+
List<int> ids = new List<int>();
64+
//Add NeverItem
65+
using (var tx = session.BeginTransaction())
66+
{
67+
foreach (var i in Enumerable.Range(1, 10))
68+
{
69+
var item = new NeverItem { Name = "Abatay" };
70+
item.Childrens.Add(new NeverChildItem()
71+
{
72+
Name = "Child",
73+
Parent = item
74+
});
75+
await (session.SaveAsync(item));
76+
ids.Add(item.Id);
77+
}
78+
79+
await (tx.CommitAsync());
80+
}
81+
82+
//Update NeverItem
83+
using (var tx = session.BeginTransaction())
84+
{
85+
foreach (var i in ids)
86+
{
87+
var item = await (session.GetAsync<NeverItem>(i));
88+
item.Name = item.Id.ToString();
89+
}
90+
91+
await (tx.CommitAsync());
92+
}
93+
94+
//Delete NeverItem
95+
using (var tx = session.BeginTransaction())
96+
{
97+
foreach (var i in ids)
98+
{
99+
var item = await (session.GetAsync<NeverItem>(i));
100+
await (session.DeleteAsync(item));
101+
}
102+
103+
await (tx.CommitAsync());
104+
}
105+
106+
//Update NeverItem using HQL
107+
using (var tx = session.BeginTransaction())
108+
{
109+
await (session.CreateQuery("UPDATE NeverItem SET Name='Test'").ExecuteUpdateAsync());
110+
111+
await (tx.CommitAsync());
112+
}
113+
114+
//Update NeverItem using LINQ
115+
using (var tx = session.BeginTransaction())
116+
{
117+
await (session.Query<NeverItem>()
118+
.UpdateBuilder()
119+
.Set(x => x.Name, "Test")
120+
.UpdateAsync());
121+
122+
await (tx.CommitAsync());
123+
}
124+
}
125+
126+
//Should receive none preinvalidation when Cache is configured as never
127+
Assert.That(preInvalidations, Has.Count.EqualTo(0));
128+
129+
//Should receive none invalidation when Cache is configured as never
130+
Assert.That(invalidations, Has.Count.EqualTo(0));
131+
}
132+
133+
[Test]
134+
public async Task QueryCache_ThrowsExceptionAsync()
135+
{
136+
using (var session = OpenSession())
137+
{
138+
//Linq
139+
using (var tx = session.BeginTransaction())
140+
{
141+
Assert.ThrowsAsync<QueryException>(() => session
142+
.Query<NeverItem>().WithOptions(x => x.SetCacheable(true)).ToListAsync());
143+
144+
await (tx.CommitAsync());
145+
}
146+
147+
//Linq Multiple with error message we will quarantied that gets 2 class in error message
148+
using (var tx = session.BeginTransaction())
149+
{
150+
Assert.ThrowsAsync<QueryException>(() => session
151+
.Query<NeverItem>().Where(x => x.Childrens.Any())
152+
.WithOptions(x => x.SetCacheable(true))
153+
.ToListAsync(),
154+
$"Never cached entity:{string.Join(", ", typeof(NeverItem).FullName, typeof(NeverChildItem).FullName)} cannot be used in cacheable query");
155+
156+
await (tx.CommitAsync());
157+
}
158+
159+
//Hql
160+
using (var tx = session.BeginTransaction())
161+
{
162+
Assert.ThrowsAsync<QueryException>(() => session
163+
.CreateQuery("from NeverItem").SetCacheable(true).ListAsync<NeverItem>());
164+
165+
await (tx.CommitAsync());
166+
}
167+
168+
//ICriteria
169+
using (var tx = session.BeginTransaction())
170+
{
171+
Assert.ThrowsAsync<QueryException>(() => session
172+
.CreateCriteria<NeverItem>()
173+
.SetCacheable(true)
174+
.ListAsync<NeverItem>());
175+
176+
await (tx.CommitAsync());
177+
}
178+
179+
//Native Sql
180+
using (var tx = session.BeginTransaction())
181+
{
182+
Assert.ThrowsAsync<QueryException>(() => session
183+
.CreateSQLQuery("select * from NeverItem")
184+
.AddSynchronizedQuerySpace("NeverItem")
185+
.SetCacheable(true)
186+
.ListAsync<NeverItem>());
187+
188+
await (tx.CommitAsync());
189+
}
190+
}
191+
}
192+
193+
[Test]
194+
public async Task ShouldAutoFlushAsync()
195+
{
196+
using (var session = OpenSession())
197+
using (session.BeginTransaction())
198+
{
199+
var e1 = new NeverItem { Name = "Abatay" };
200+
e1.Childrens.Add(new NeverChildItem()
201+
{
202+
Name = "Child",
203+
Parent = e1
204+
});
205+
await (session.SaveAsync(e1));
206+
207+
var result = await ((from e in session.Query<NeverItem>()
208+
where e.Name == "Abatay"
209+
select e).ToListAsync());
210+
211+
Assert.That(result.Count, Is.EqualTo(1));
212+
}
213+
}
214+
215+
protected override void OnTearDown()
216+
{
217+
using (var s = OpenSession())
218+
using (var tx = s.BeginTransaction())
219+
{
220+
s.Delete("from NeverItem");
221+
tx.Commit();
222+
}
223+
}
224+
}
225+
}

src/NHibernate.Test/CfgTest/EntityCacheUsageParserFixture.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public void CovertToString()
1313
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.ReadWrite), Is.EqualTo("read-write"));
1414
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.NonStrictReadWrite), Is.EqualTo("nonstrict-read-write"));
1515
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.Transactional), Is.EqualTo("transactional"));
16+
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.Never), Is.EqualTo("never"));
1617
}
1718

1819
[Test]
@@ -22,6 +23,7 @@ public void Parse()
2223
Assert.That(EntityCacheUsageParser.Parse("read-write"), Is.EqualTo(EntityCacheUsage.ReadWrite));
2324
Assert.That(EntityCacheUsageParser.Parse("nonstrict-read-write"), Is.EqualTo(EntityCacheUsage.NonStrictReadWrite));
2425
Assert.That(EntityCacheUsageParser.Parse("transactional"), Is.EqualTo(EntityCacheUsage.Transactional));
26+
Assert.That(EntityCacheUsageParser.Parse("never"), Is.EqualTo(EntityCacheUsage.Never));
2527
}
2628
}
27-
}
29+
}

src/NHibernate.Test/DebugSessionFactory.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,16 @@ ICollectionPersister ISessionFactoryImplementor.GetCollectionPersister(string ro
316316
return ActualFactory.GetCollectionPersister(role);
317317
}
318318

319+
public ISet<IEntityPersister> GetEntityPersisters(ISet<string> spaces)
320+
{
321+
return ActualFactory.GetEntityPersisters(spaces);
322+
}
323+
324+
public ISet<ICollectionPersister> GetCollectionPersisters(ISet<string> spaces)
325+
{
326+
return ActualFactory.GetCollectionPersisters(spaces);
327+
}
328+
319329
IType[] ISessionFactoryImplementor.GetReturnTypes(string queryString)
320330
{
321331
return ActualFactory.GetReturnTypes(queryString);

src/NHibernate.Test/SecondLevelCacheTest/Item.hbm.xml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,32 @@
2626
<property name="Name"/>
2727
<property name="Description"/>
2828
</class>
29+
30+
<class name="NHibernate.Test.SecondLevelCacheTests.NeverItem, NHibernate.Test">
31+
<cache usage="never"/>
32+
<id name="Id">
33+
<generator class="native"/>
34+
</id>
35+
<property name="Name"/>
36+
<property name="Description"/>
37+
<bag name="Childrens" cascade="all" inverse="true">
38+
<cache usage="never"/>
39+
<key column="ParentId"/>
40+
<one-to-many class="NHibernate.Test.SecondLevelCacheTests.NeverChildItem, NHibernate.Test"/>
41+
</bag>
42+
</class>
43+
44+
<class name="NHibernate.Test.SecondLevelCacheTests.NeverChildItem, NHibernate.Test">
45+
<cache usage="never"/>
46+
<id name="Id">
47+
<generator class="native"/>
48+
</id>
49+
<property name="Name"/>
50+
<many-to-one name="Parent" column="ParentId"
51+
class="NHibernate.Test.SecondLevelCacheTests.NeverItem, NHibernate.Test"/>
52+
</class>
2953

3054
<query name="Stat" cacheable="true" read-only ="true" cache-region="Statistics">
3155
select ai.Name, count(*) from AnotherItem ai group by ai.Name
3256
</query>
33-
</hibernate-mapping>
57+
</hibernate-mapping>

0 commit comments

Comments
 (0)