Skip to content

Commit

Permalink
Set which entities classes should never be cached, even indirectly (#…
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
4 people committed Mar 20, 2022
1 parent 21859e9 commit 8b74774
Show file tree
Hide file tree
Showing 55 changed files with 1,180 additions and 190 deletions.
18 changes: 18 additions & 0 deletions doc/reference/modules/configuration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,24 @@ var session = sessions.OpenSession(conn);
</para>
</entry>
</row>
<row>
<entry>
<literal>query.throw_never_cached</literal>
</entry>
<entry>
Should queries set as cacheable raise an error if they reference an entity using the cache
<xref linkend="performance-cache-never" /> (the default is enabled).
<para>
<emphasis role="strong">eg.</emphasis>
<literal>true</literal> | <literal>false</literal>
</para>
<para>
Disabling this setting causes NHibernate to ignore the caching of such queries without
raising an error. Furthermore NHibernate will log a warning on cacheable queries
referencing an entity using the <literal>never</literal> cache strategy.
</para>
</entry>
</row>
<row>
<entry>
<literal>query.factory_class</literal>
Expand Down
29 changes: 25 additions & 4 deletions doc/reference/modules/performance.xml
Original file line number Diff line number Diff line change
Expand Up @@ -733,17 +733,18 @@ using(var iter = session
<area id="cache1" coords="2 70"/>
<area id="cache2" coords="3 70"/>
</areaspec>
<programlisting><![CDATA[<cache
usage="read-write|nonstrict-read-write|read-only"
<programlisting><![CDATA[<cache
usage="read-write|nonstrict-read-write|read-only|never"
region="RegionName"
/>]]></programlisting>
<calloutlist>
<callout arearefs="cache1">
<para>
<literal>usage</literal> specifies the caching strategy:
<literal>read-write</literal>,
<literal>nonstrict-read-write</literal> or
<literal>read-only</literal>
<literal>nonstrict-read-write</literal>,
<literal>read-only</literal> or
<literal>never</literal>
</para>
</callout>
<callout arearefs="cache2">
Expand Down Expand Up @@ -824,6 +825,26 @@ using(var iter = session

</sect2>

<sect2 id="performance-cache-never">
<title>Strategy: never</title>

<para>
By default, without a cache configuration, entities are not cacheable. But they may still be referenced
in cacheable queries, which results will then be cached according to the values these non cacheable
entities have. So, their data may be indirectly cached through cacheable queries.
</para>

<para>
By using the cache strategy <literal>never</literal>, such indirect caching of these entities data will
be forbidden by NHibernate. Setting as cacheable a query referencing entities with strategy
<literal>never</literal> will be treated as an error by default. Alternatively, the
<literal>query.throw_never_cached</literal> <link linkend="configuration-optional">setting</link> can be
set to <literal>false</literal>: instead of raising an error, it will disable the query cache on such
queries, and log a warning.
</para>

</sect2>

<para>
The following table shows which providers are compatible with which concurrency strategies.
</para>
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate.Test/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<property name="cache.use_query_cache">true</property>

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

<property name="adonet.batch_size">10</property>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NHibernate.Cache;
using NHibernate.Cfg;
using NHibernate.Impl;
using NHibernate.Linq;
using NHibernate.Test.SecondLevelCacheTests;
using NSubstitute;
using NUnit.Framework;

namespace NHibernate.Test.SecondLevelCacheTest
{
using System.Threading.Tasks;
using System.Threading;
[TestFixture]
public class NeverCachedEntityTestsAsync : TestCase
{
protected override string CacheConcurrencyStrategy => null;
protected override string MappingsAssembly => "NHibernate.Test";

protected override string[] Mappings => new[] { "SecondLevelCacheTest.Item.hbm.xml" };

protected override void Configure(Configuration configuration)
{
configuration.SetProperty(Environment.CacheProvider, typeof(HashtableCacheProvider).AssemblyQualifiedName);
configuration.SetProperty(Environment.UseQueryCache, "true");
}

[Test]
public async Task NeverInvalidateEntitiesAsync()
{
var debugSessionFactory = (DebugSessionFactory) Sfi;

var cache = Substitute.For<UpdateTimestampsCache>(Sfi.Settings, new Dictionary<string, string>());

var updateTimestampsCacheField = typeof(SessionFactoryImpl).GetField(
"updateTimestampsCache",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

updateTimestampsCacheField.SetValue(debugSessionFactory.ActualFactory, cache);

//"Received" assertions can not be used since the collection is reused and cleared between calls.
//The received args are cloned and stored
var preInvalidations = new List<IReadOnlyCollection<string>>();
var invalidations = new List<IReadOnlyCollection<string>>();

await (cache.PreInvalidateAsync(Arg.Do<IReadOnlyCollection<string>>(x => preInvalidations.Add(x.ToList())), CancellationToken.None));
await (cache.InvalidateAsync(Arg.Do<IReadOnlyCollection<string>>(x => invalidations.Add(x.ToList())), CancellationToken.None));

using (var session = OpenSession())
{
List<int> ids = new List<int>();
//Add NeverItem
using (var tx = session.BeginTransaction())
{
foreach (var i in Enumerable.Range(1, 10))
{
var item = new NeverItem { Name = "Abatay" };
item.Childrens.Add(new NeverChildItem()
{
Name = "Child",
Parent = item
});
await (session.SaveAsync(item));
ids.Add(item.Id);
}

await (tx.CommitAsync());
}

//Update NeverItem
using (var tx = session.BeginTransaction())
{
foreach (var i in ids)
{
var item = await (session.GetAsync<NeverItem>(i));
item.Name = item.Id.ToString();
}

await (tx.CommitAsync());
}

//Delete NeverItem
using (var tx = session.BeginTransaction())
{
foreach (var i in ids)
{
var item = await (session.GetAsync<NeverItem>(i));
await (session.DeleteAsync(item));
}

await (tx.CommitAsync());
}

//Update NeverItem using HQL
using (var tx = session.BeginTransaction())
{
await (session.CreateQuery("UPDATE NeverItem SET Name='Test'").ExecuteUpdateAsync());

await (tx.CommitAsync());
}

//Update NeverItem using LINQ
using (var tx = session.BeginTransaction())
{
await (session.Query<NeverItem>()
.UpdateBuilder()
.Set(x => x.Name, "Test")
.UpdateAsync());

await (tx.CommitAsync());
}
}

//Should receive none preinvalidation when Cache is configured as never
Assert.That(preInvalidations, Has.Count.EqualTo(0));

//Should receive none invalidation when Cache is configured as never
Assert.That(invalidations, Has.Count.EqualTo(0));
}

[Test]
public async Task QueryCache_ThrowsExceptionAsync()
{
using (var session = OpenSession())
{
//Linq
using (var tx = session.BeginTransaction())
{
Assert.ThrowsAsync<QueryException>(() => session
.Query<NeverItem>().WithOptions(x => x.SetCacheable(true)).ToListAsync());

await (tx.CommitAsync());
}

//Linq Multiple with error message we will quarantied that gets 2 class in error message
using (var tx = session.BeginTransaction())
{
Assert.ThrowsAsync<QueryException>(() => session
.Query<NeverItem>().Where(x => x.Childrens.Any())
.WithOptions(x => x.SetCacheable(true))
.ToListAsync(),
$"Never cached entity:{string.Join(", ", typeof(NeverItem).FullName, typeof(NeverChildItem).FullName)} cannot be used in cacheable query");

await (tx.CommitAsync());
}

//Hql
using (var tx = session.BeginTransaction())
{
Assert.ThrowsAsync<QueryException>(() => session
.CreateQuery("from NeverItem").SetCacheable(true).ListAsync<NeverItem>());

await (tx.CommitAsync());
}

//ICriteria
using (var tx = session.BeginTransaction())
{
Assert.ThrowsAsync<QueryException>(() => session
.CreateCriteria<NeverItem>()
.SetCacheable(true)
.ListAsync<NeverItem>());

await (tx.CommitAsync());
}

//Native Sql
using (var tx = session.BeginTransaction())
{
Assert.ThrowsAsync<QueryException>(() => session
.CreateSQLQuery("select * from NeverItem")
.AddSynchronizedQuerySpace("NeverItem")
.SetCacheable(true)
.ListAsync<NeverItem>());

await (tx.CommitAsync());
}
}
}

[Test]
public async Task ShouldAutoFlushAsync()
{
using (var session = OpenSession())
using (session.BeginTransaction())
{
var e1 = new NeverItem { Name = "Abatay" };
e1.Childrens.Add(new NeverChildItem()
{
Name = "Child",
Parent = e1
});
await (session.SaveAsync(e1));

var result = await ((from e in session.Query<NeverItem>()
where e.Name == "Abatay"
select e).ToListAsync());

Assert.That(result.Count, Is.EqualTo(1));
}
}

protected override void OnTearDown()
{
using (var s = OpenSession())
using (var tx = s.BeginTransaction())
{
s.Delete("from NeverItem");
tx.Commit();
}
}
}
}
4 changes: 3 additions & 1 deletion src/NHibernate.Test/CfgTest/EntityCacheUsageParserFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public void CovertToString()
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.ReadWrite), Is.EqualTo("read-write"));
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.NonStrictReadWrite), Is.EqualTo("nonstrict-read-write"));
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.Transactional), Is.EqualTo("transactional"));
Assert.That(EntityCacheUsageParser.ToString(EntityCacheUsage.Never), Is.EqualTo("never"));
}

[Test]
Expand All @@ -22,6 +23,7 @@ public void Parse()
Assert.That(EntityCacheUsageParser.Parse("read-write"), Is.EqualTo(EntityCacheUsage.ReadWrite));
Assert.That(EntityCacheUsageParser.Parse("nonstrict-read-write"), Is.EqualTo(EntityCacheUsage.NonStrictReadWrite));
Assert.That(EntityCacheUsageParser.Parse("transactional"), Is.EqualTo(EntityCacheUsage.Transactional));
Assert.That(EntityCacheUsageParser.Parse("never"), Is.EqualTo(EntityCacheUsage.Never));
}
}
}
}
10 changes: 10 additions & 0 deletions src/NHibernate.Test/DebugSessionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ ICollectionPersister ISessionFactoryImplementor.GetCollectionPersister(string ro
return ActualFactory.GetCollectionPersister(role);
}

public ISet<IEntityPersister> GetEntityPersisters(ISet<string> spaces)
{
return ActualFactory.GetEntityPersisters(spaces);
}

public ISet<ICollectionPersister> GetCollectionPersisters(ISet<string> spaces)
{
return ActualFactory.GetCollectionPersisters(spaces);
}

IType[] ISessionFactoryImplementor.GetReturnTypes(string queryString)
{
return ActualFactory.GetReturnTypes(queryString);
Expand Down
26 changes: 25 additions & 1 deletion src/NHibernate.Test/SecondLevelCacheTest/Item.hbm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,32 @@
<property name="Name"/>
<property name="Description"/>
</class>

<class name="NHibernate.Test.SecondLevelCacheTests.NeverItem, NHibernate.Test">
<cache usage="never"/>
<id name="Id">
<generator class="native"/>
</id>
<property name="Name"/>
<property name="Description"/>
<bag name="Childrens" cascade="all" inverse="true">
<cache usage="never"/>
<key column="ParentId"/>
<one-to-many class="NHibernate.Test.SecondLevelCacheTests.NeverChildItem, NHibernate.Test"/>
</bag>
</class>

<class name="NHibernate.Test.SecondLevelCacheTests.NeverChildItem, NHibernate.Test">
<cache usage="never"/>
<id name="Id">
<generator class="native"/>
</id>
<property name="Name"/>
<many-to-one name="Parent" column="ParentId"
class="NHibernate.Test.SecondLevelCacheTests.NeverItem, NHibernate.Test"/>
</class>

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

0 comments on commit 8b74774

Please sign in to comment.