Entity Framework 4 und der 2nd-Level Cache
Von Haus aus bringt das Entity Framework keine erweiterte Cache-Funktionalität mit. Wer für Projekte solche Szenarien benötigt, kann aber auf die EFProviderWrappers zurückgreifen. In früheren Beiträgen habe ich aus diesem Toolkit der Möglichkeiten des EFTracingProviders beschrieben. Vor allem gefällt mir hier, dass ich das generierte SQL an spezifische Vorstellungen anpassen kann. Es gibt DBA's die sind in dieser Hinsicht wenig kompromissbereit.
Es lassen sich durch dieses Toolkit auch eigene Provider realisieren.
Mit dem EFCachingProvider besteht die Möglichkeit, dem Entity Framework einen 2nd-Level Cache zu verpassen. So kann es auch weiter zu NHibernate aufschliessen.
Cachevarianten die zur Verfügung stehen sind:
- InMemoryCache (einfache Variante, die nicht sehr leistungsfähig ist)
- Der ASP.NET Cache
- und Velocity (AppFabric)
Gerade mit Velocity steht dann ein sehr mächtiges Caching zur Verfügung. Haufig werden jedoch Probleme mit dem Caching berichtet, wenn View-Generation benutzt wird, da dass Caching mit den Hashes nicht umgehen kann. Dieses Phänomen konnte ich nicht reproduzieren, es liegt evtl. daran, dass ich diese Views mittels T4 und dem zur Verfügungen stehenden EntityViewGenerator erstellen lasse.
Mit der CachingPolicy besteht zudem die Möglichkeit, dass ich definieren kann, ob einzelne Entitäten oder alle zwischengespeichert werden sollen, es ist jedoch nicht möglich, eine Art "partitioniertes" Caching auf einzelne Entitäten eines Sets zu definieren.
Damit das Caching funktioniert, muss der entsprechende Wrapper um die normale Verbindung "gewickelt" werden, das geschieht beim Konstruktor mit folgender Helperklasse:
Provider-Wrapper C#
public ORMSamplesEntities()
: base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
ConnectionString,
"EFTracingProvider",
"EFCachingProvider"
), ContainerName)
{
Initialize();
}
In diesem Szenario wird die Verbindung mit dem Tracing- und Caching-Provider umhüllt. In meinem Context habe ich zudem 2 statische Eigenschaften angelegt, die den Zugriff auf die Einstellungen an die Basisklasse weiterleiten:
Eigenschaften für die Cache-Initialisierung C#
public static ICache DefaultCache
{
get
{
return EFCachingProviderConfiguration.DefaultCache;
}
set
{
EFCachingProviderConfiguration.DefaultCache = value;
}
}
public static CachingPolicy DefaultCachingPolicy
{
get
{
return EFCachingProviderConfiguration.DefaultCachingPolicy;
}
set
{
EFCachingProviderConfiguration.DefaultCachingPolicy = value;
}
}
Ein interessantes Detail, diese Eigenschaften müssen vor der Erstellung einer Instanz des Kontexts gesetzt werden, da mit diesen Angaben die Wrapper in den Konstruktoren gefüttert werden.
Machen wir ein kleines Testbeispiel mithilfe des SQL-Profilers und einem Unit-Test. Es soll 4-mal die gleiche Order geladen werden. Mit den Standardmöglichkeiten des Entity Frameworks wird dann die gleiche Abfrage 4-mal an den Datenbankserver gesendet.
Test ohne Cache C#
[TestMethod]
public void NonCachingProviderTest()
{
// Arrange
var ctx = new ORMSamplesEntities();
// Act
var result = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
var result2 = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
var result3 = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
var result4 = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
// Assert
// see Sql-Profiler log ;-)
}
Im SQL-Profiler wurden während diesem Test 4 Abfragen mit dem exakt gleichen Inhalt protokolliert.

- Abbildung 1 Protokoll der Abfragen die bei der Ausführung der Unit-Tests verursacht worden sind.
Führen wir den Test erneut aus, diesmal jedoch mit aktivierten 2nd-Level Cache und dem InMemory-Cache.
Test mit Cache C#
[TestMethod]
public void CachingProviderInMemoryTest()
{
ORMSamplesEntities.DefaultCache = new InMemoryCache();
ORMSamplesEntities.DefaultCachingPolicy = CachingPolicy.CacheAll;
// Arrange
var ctx = new ORMSamplesEntities();
// Act
var result = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
var result2 = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
var result3 = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
var result4 = (from o in ctx.Orders.Include("OrderItems")
where o.Id == 1
select o).ToList();
InMemoryCache cache = ORMSamplesEntities.DefaultCache as InMemoryCache;
// Assert
Assert.AreEqual(3, cache.CacheHits);
Assert.AreEqual(1, cache.CacheMisses);
Assert.AreEqual(0, cache.CacheItemInvalidations);
Assert.AreEqual(1, cache.CacheItemAdds);
// see Sql-Profiler log
}
Der SQL-Profiler hat nun während der Ausführung nur eine Abfrage protokolliert. Beim 2., 3. und 4. Aufruf wurden die Linq-Abfragen aus dem Cache bedient.

- Abbildung 2 Log SQL Server Profiler während des Test. Die 4 Linq-Abfragen führen nur noch zu einer Abfrage an den Datenbankserver.
Die Provider-Wrapper sind nicht nur ein "nettes Spielzeug", sondern auch sehr hilfreich, da diese fehlende Funktionalitäten für das Entity Framework bereitstellen.
Ich gehe stark davon aus, das mit der nächsten Version - ich rede hier jetzt nicht von der EF CTP 4 - diese Funktionalitäten fester Bestandteil des Entity Frameworks werden.
- 0 Kommentar(e)


Mein Kommentar