So erstellen Sie hochgradig anpassbares Caching in einem Projekt und ersparen Kollegen das Schreiben des gleichen Codetyps

„Wenn die Arbeit des Programmierers im Wesentlichen darin besteht, die Arbeit anderer zu automatisieren, warum ist meine Arbeit dann so wenig automatisiert?“, Dachte ich und kopierte erneut alle im Projekt erforderlichen Bindungen, um der Datenbank eine neue Entität hinzuzufügen. Und ich beschloss, diese Routine des Hinzufügens von Vorlagenklassen loszuwerden, gleichzeitig dem Projekt „Gutes“ zu tun und die Datenbank von unnötigen Lesevorgängen zu entladen.

Ein kleiner Exkurs über das System, das wir entwickeln, und seinen Zustand zu Beginn dieses Experiments:

  • Ein System, in dem 90% der Daten aktiv geändert und verarbeitet werden (Transaktionen, personenbezogene Daten, berechnete Aggregationen), aber selten gelesen werden und die restlichen 10% sich sehr selten ändern, aber bei jeder Gelegenheit zum Lesen verwendet werden
  • Fast monolithischer Dienst auf .Net Framework, in dem all dies implementiert ist
  • Nhibernate mit minimaler Bindung, verwendet für den Zugriff auf die Datenbank und einige darauf basierende Caches (Abonnements für Änderungen an BO-Entitäten, Handler, die beim Transaktions-Commit aufgerufen werden)
  • Ein Dutzend unausgesprochener Regeln, "wie man Code schreibt, um auf die Datenbank zuzugreifen, ohne die Leistung mit NHibernate-Funktionen zu beeinträchtigen", werden regelmäßig in Core Review behandelt
  • Eine etwas langweilige Datenbank, die optimiert werden muss

Beim Nachdenken über die Situation mit der Datenbank entstand der Gedanke: ob diese 10% der Anforderungen aus der Datenbank entfernt werden sollen (und gleichzeitig die für sie erforderlichen Verbindungen zur Datenbank geöffnet werden sollen, offene Transaktionen gehalten und der Vorlagencode verwendet werden soll, um über unsere Repositorys auf die Datenbank zuzugreifen). In diesem Fall musste Folgendes berücksichtigt werden:

  • Wir haben bereits versucht, mit dem Nhibernate-Cache zu arbeiten, haben jedoch festgestellt, dass sein Verhalten nicht immer explizit und vorhersehbar ist.
  • Ich wollte ohne guten Grund nichts grundlegend an der Plattform oder Infrastruktur ändern
  • Die Anzahl der handgeschriebenen Codes sollte, wie bei jedem faulen Programmierer, dadurch verringert werden, dass Hunderte von Zeilen mit Wrappern und Abonnements für vorhandene Caches mit Ihren Händen geschrieben werden

Eine Beispielimplementierung eines dieser alten Caches
/// <summary> /// Base class for caches, containing rarely changed entities. Updated by subscription to nHibernate session commits. /// </summary> /// <typeparam name="T">Type, used as a base for the cache. Sessions, containing changes to any instance of this class will cause cache refresh.</typeparam> /// <typeparam name="K">Key used for cache search.</typeparam> /// <typeparam name="V">Value, stored in cache.</typeparam> public abstract class RareChangedObjectsCache<T, K, V> : EmptySessionNotificationListener, ITransactionNotificationListener, IRareChangedObjectsCache<K, V> where T : class { [NotNull] private static readonly ILog Log = IikoBizLogManager.GetLogger(typeof(RareChangedObjectsCache<T, K, V>)); [NotNull] protected readonly ReaderWriterLockSlim LockObj = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); [NotNull] protected readonly Dictionary<K, V> Cache = new Dictionary<K, V>(); [NotNull] protected abstract QueryOver<T> GetQuery(); [NotNull] private readonly ConcurrentDictionary<String, bool> changesByTransaction = new ConcurrentDictionary<string, bool>(); private DateTime lastRefreshTime = DateTime.MinValue; [NotNull] public HibernateSessionManager SessionManager { get; set; } /// <summary> /// Interval of automatic data renewal for cases of read access to cache. Also, cache is forcibly refreshed on any commit, changing base entities of this cache. /// </summary> public TimeSpan AutoRefreshInterval { get; set; } public void Reset() { lastRefreshTime = DateTime.MinValue; } protected void ReloadCacheIfNeeded(ISession session = null) { if (SystemTime.Now - lastRefreshTime <= AutoRefreshInterval) return; LockObj.EnterWriteLock(); try { if (SystemTime.Now - lastRefreshTime <= AutoRefreshInterval) return; IList<object> result; if (session == null) result = SessionManager.CallTransacted(s => GetQuery().GetExecutableQueryOver(s).List<object>()); else //TODO At that moment, the transaction may have been closed, so a new transaction opens implicitly result = GetQuery().GetExecutableQueryOver(session).List<object>(); Cache.Clear(); ProcessResult(result); lastRefreshTime = SystemTime.Now; } catch (Exception e) { Log.Error("Exception on cache invalidation: ", e); } finally { LockObj.ExitWriteLock(); } } protected abstract void ProcessResult(IList<object> result); public override void OnSaveOrUpdate(ISession session, object entity, Guid id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) { if (entity is T) { var transactionName = HibernateSessionManager.GetTransactionName(); if (!string.IsNullOrEmpty(transactionName)) { changesByTransaction.TryAdd(transactionName, true); } } } public override void OnDelete(ISession session, object entity, Guid id) { if (entity is T) { var transactionName = HibernateSessionManager.GetTransactionName(); if (!string.IsNullOrEmpty(transactionName)) { changesByTransaction.TryAdd(transactionName, true); } } } void ITransactionNotificationListener.AfterCommit(ISession session, string transactionName) { bool tmp; if (changesByTransaction.TryRemove(transactionName, out tmp)) Reset(); ReloadCacheIfNeeded(session); } void ITransactionNotificationListener.AfterRollback(ISession session, string transactionName) { } public bool Contains(K key) { ReloadCacheIfNeeded(); LockObj.EnterReadLock(); try { return Cache.ContainsKey(key); } finally { LockObj.ExitReadLock(); } } public virtual V TryGet(K key) { ReloadCacheIfNeeded(); LockObj.EnterReadLock(); try { return Cache.TryGetValue(key, out var value) ? value : default; } finally { LockObj.ExitReadLock(); } } protected TV GetOrAddEntry<TK, TV>(IDictionary<TK, TV> dictionary, TK key) where TV : new() { TV list; if (!dictionary.TryGetValue(key, out list)) dictionary[key] = (list = new TV()); return list; } } /// <summary> /// Caches all guest categories, grouped by organization or network they belong. /// </summary> [UsedImplicitly] public sealed class GuestCategoryCache : RareChangedObjectsCache<GuestCategory, Guid, HashSet<GuestCategory>> { private static readonly QueryOver<GuestCategory> SelectAllEntitiesQuery = QueryOver.Of<GuestCategory>() .Fetch(gc => gc.Organization).Eager .Fetch(gc => gc.Network).Eager; private readonly Dictionary<Guid, string> invertedCache = new Dictionary<Guid, string>(); public bool TryGetNetworkExternalId(Guid id, out string extId) { ReloadCacheIfNeeded(); LockObj.EnterReadLock(); try { return invertedCache.TryGetValue(id, out extId); } finally { LockObj.ExitReadLock(); } } protected override QueryOver<GuestCategory> GetQuery() { return SelectAllEntitiesQuery; } protected override void ProcessResult(IList<object> result) { invertedCache.Clear(); foreach (GuestCategory gc in result) { var orgOrNetworkId = gc.Organization?.Id ?? gc.Network.Id; GetOrAddEntry(Cache, orgOrNetworkId).Add(gc); } } } 


damals schon etwas müde.
- Eine neue Funktion sollte andere Entwickler nicht global beeinflussen. Die Migration auf einen neuen Ansatz sollte reibungslos und reibungslos erfolgen.

Gleichzeitig wollte ich sicherstellen, dass das Hinzufügen einer neuen Entität zu den Caching-Mechanismen einen minimalen Aufwand erfordert, der Code lesbar und unkompliziert bleibt und beim Abrufen von Daten aus dem Cache nur minimal darüber nachgedacht werden kann, welcher Hilfscode geschrieben werden soll. Die letzten beiden Punkte haben die Auswahlmöglichkeiten erheblich eingeschränkt. Tatsächlich müssen Sie entweder mit Reflexion und generischen Klassen einen Unterschied machen oder sich der guten alten Metaprogrammierung zuwenden.

Da das Hauptentwicklungstool Visual Studio ist, ich aber nicht viel Energie darauf verwenden wollte, dass es keinen großen Effekt hat, habe ich beschlossen, die Entscheidung „auf der Stirn“ mit den meisten Standardwerkzeugen und erst in der Phase des fertigen Proof Of Concept für einige der am häufigsten verwendeten zu treffen Unternehmen - präsentieren eine Entscheidung vor Kollegen vor Gericht.

Als nächstes kam ein moralisches Dilemma. Ob Sie eine Klasse als Quelle verwenden möchten, die für alle Gelegenheiten mit Attributen versehen ist (im Stil von Nhibernates Fluent-Mapping auf Steroiden) oder ein schönes, ordentliches XML schreiben möchten. Ich erinnerte mich daran, dass Faulheit der beste Freund eines Programmierers ist und das Beschreiben von Klassen mit Attributen zeitaufwändiger ist als das Schreiben von ein wenig XML. Ich entschied mich für Letzteres.

Was brauchte ich im Wesentlichen für die Beschreibung von Entitäten?

  • Beschreibung der zwischengespeicherten Felder
  • Die Fähigkeit, die Eigenschaften anzugeben, mit denen wir aus diesen Daten Samples erstellen, falls erforderlich, die Fähigkeit, diese Samples zu optimieren, indem der lineare Durchgang durch Listen eliminiert wird (wenn Sie in der Optimierung spielen, also vollständig)
  • Zusätzliche Bequemlichkeit, um Klassen in verschiedenen Ordnern zu verteilen und die vorhandenen Entwicklungen im Code zu verwenden, um seine Anzahl zu reduzieren

Wir haben diese XML-Struktur
 <class name="GuestCategory" logicallyDeletable="true" revisioned="true" organizationNetworkBased="true" basedOn="BO.GuestCategory" > <emitBo emitMapping="true"/> <emitRepository dependsOn="guestCategoryCache">IGuestCategoryRepository</emitRepository> <field name="IsActive" type="bool"/> <field name="IsDefaultForNewGuests" type="bool"/> <field name="Name" type="string" notNull="true"/> </class> 


Und eine große, beängstigende T4-Vorlage zum Parsen und Generieren verschiedener, aber dringend benötigter Klassen:

  • Ein "zwischengespeicherter" Typ mit denselben Feldern wie BO, jedoch nicht bearbeitbar
  • Implementieren eines Caches für einen Typ mit Filterauswahlmethoden
  • Eine Registrierung von Caches zum Abonnieren von Nhibernate-Mechanismen für Benachrichtigungen und Registrierung in DI (in unserem Fall Spring)
  • Schnittstellen für all dies, um die Innenseiten zu verbergen und gegebenenfalls den generierten Code durch handgeschriebenen und zurück zu ersetzen.
  • Als angenehmer ungeplanter Bonus von Anfang an, wenn die Arbeit mit den Hauptentitäten bereits fertig war - ich habe einen halben Schritt zum Glück getan und nebenbei die Möglichkeit hinzugefügt, einfache BO-Typen und Zuordnungen zu generieren, um Kollegen die Möglichkeit zu geben, mit einem halben Tritt neue Klassen hinzuzufügen.

Die Vorlage selbst besteht aus logischer Sicht aus zwei Teilen: Parsen der ursprünglichen XML in Klassen, die die gewünschte Klassenstruktur beschreiben (um das Risiko eines impliziten Verhaltens zu verringern, wurde in diesem Fall beschlossen, dies durch explizites Parsen von Tags und nicht durch Zuordnen zu tun Attribute.

Strukturklassen
 class DalClass { public string Name { get; } public bool LogicallyDeletable { get; } public string BasedOn { get; } public string DalBOType { get; } public string[] Implements { get; } public string[] Include { get; } public bool CustomConsistencyManager { get; } public List<DalField> Fields { get; } public List<DalField> ExplicitlyDefinedFields { get; } public DalGetBy[] GetBy { get; } public DalGetBy[] GetAllBy { get; } public bool GenerateInterface { get; } public DalEmitBo EmitBo { get; } public DalEmitRepository EmitRepository { get; } public DalClass(XmlElement sourceXml) { Implements = sourceXml.SelectNodes("*[local-name()='implements']") .Cast<XmlElement>() .Select(f => f.InnerText + ",") .ToArray(); Include = sourceXml.SelectNodes("*[local-name()='include']") .Cast<XmlElement>() .Select(f => f.InnerText) .ToArray(); Name = sourceXml.GetAttribute("name"); LogicallyDeletable = sourceXml.HasAttribute("logicallyDeletable"); BasedOn = sourceXml.GetAttribute("basedOn"); DalBOType = sourceXml.HasAttribute("dalBoType") ? sourceXml.GetAttribute("dalBoType") : BasedOn; CustomConsistencyManager = sourceXml.HasAttribute("customConsistencyManager") ? sourceXml.GetAttribute("customConsistencyManager") == "true" : false; Fields = sourceXml.SelectNodes("*[local-name()='field']") .Cast<XmlElement>() .Select(f => new DalField(f)) .ToList(); ExplicitlyDefinedFields = Fields.ToList(); Fields = Fields.OrderBy(f=>f.Name).ToList(); GetBy = sourceXml.SelectNodes("*[local-name()='getBy']") .Cast<XmlElement>() .Select(f => new DalGetBy(f)) .ToArray(); GetAllBy = sourceXml.SelectNodes("*[local-name()='getAllBy']") .Cast<XmlElement>() .Select(f => new DalGetBy(f)) .ToArray(); EmitBo = sourceXml.SelectNodes("*[local-name()='emitBo']") .Cast<XmlElement>() .Select(f => new DalEmitBo(f)) .SingleOrDefault(); EmitRepository = sourceXml.SelectNodes("*[local-name()='emitRepository']") .Cast<XmlElement>() .Select(f => new DalEmitRepository(f, Name)) .SingleOrDefault(); GenerateInterface = true; } public string GetIncludedNamespaces() { return string.Join("/n", Include.Select(i => "using " + i + ";")); } public string GetBoClassDefinition() { return Name + " :\n\t\tBaseEntity,\n\t\t" + (LogicallyDeletable ? "ILogicallyDeletable,\n\t\t" : string.Empty) + (Implements.Any() ? string.Join("\n\t\t", Implements) + "\n\t\t" : string.Empty) + "I" + Name; } public string GetCachedClassDefinition() { return "Cached" + Name + " :\n\t\t" + (LogicallyDeletable ? "Deletable" : string.Empty) + "CachedEntity<" + BasedOn + ">,\n\t\t" + (Implements.Any() ? string.Join("\n\t\t", Implements) + "\n\t\t" : string.Empty) + "I" + Name; } public string TryGetIsDeletedParameter() { if (LogicallyDeletable) return ",bool getDeleted"; else return String.Empty; } public string TryGetIsDeletedFilter() { if (LogicallyDeletable) return @" if (!getDeleted) entities = entities.Where(e => !e.IsDeleted); "; else return String.Empty; } public string GetFilterParameters(DalGetBy getBy) { var filters = new List<FieldDescription>(); foreach (var filter in getBy.Filters) filters.Add(new FieldDescription { TypeName = Fields.Single(f => f.Name == filter.Key).FieldType, Alias = filter.Value }); return string.Join(", ", filters.Select(f => f.TypeName + " " + f.Alias)); } private struct FieldDescription { public string TypeName; public string Alias; } } class DalField { public string Name { get; } public string FieldType { get; } public string Source { get; } public string PropertySource { get; } public bool NotNull { get; } public DalField(string name, string type) { Name = name; FieldType = type; Source = name; } public DalField(XmlElement sourceXml) { Name = sourceXml.GetAttribute("name"); FieldType = sourceXml.GetAttribute("type"); Source = sourceXml.HasAttribute("source") ? sourceXml.GetAttribute("source") : sourceXml.GetAttribute("name"); PropertySource = sourceXml.HasAttribute("propertySource") ? sourceXml.GetAttribute("propertySource") : null; NotNull = sourceXml.HasAttribute("notNull") ? sourceXml.GetAttribute("notNull") == "true" : false; } public string GetConstructorInitValueExpression() { var fieldIsArray = FieldType.EndsWith("[]"); var fieldRealType = FieldType.Replace("[]", ""); if (PropertySource!=null && fieldRealType.StartsWith("I")) fieldRealType = "Cached" + fieldRealType.Substring(1); return UpperInitial(Name) + " = source." + Source + (fieldIsArray ? ".Select(i=>new " + fieldRealType + "(i)).ToArray()" : "") + ";"; } public string GetPropertyDefinitionExpression() { return "public " + GetType() + " " + UpperInitial(Name) + (PropertySource != null ? " => " + PropertySource + ";" : " { get; private set; }"); } public string GetBOPropertyDefinitionExpression() { return "public virtual " + GetBoType() + " " + UpperInitial(Name) + " { get; set; }"; } public string GetInterfacePropertyDefinitionExpression() { return GetNullAttribute() + GetType() + " " + UpperInitial(Name) + " { get; }"; } public string GetType() { var fieldIsArray = FieldType.EndsWith("[]"); if (fieldIsArray) return "IEnumerable<" + FieldType.Replace("[]", "") + ">"; return FieldType; } public string GetBoType() { var fieldIsArray = FieldType.EndsWith("[]"); if (fieldIsArray) return "IList<" + FieldType.Replace("[]", "") + ">"; return FieldType; } private string GetNullAttribute() => TypeCanBeNull() ? NotNull ? "[NotNull] " : "[CanBeNull] " : string.Empty; private static string[] NotNullTypes = { "Guid", "DateTime", "DateTimeOffset", "bool", "int", "long", "short", "ProgramType", "GuestSubscriptionTypes", "SenderType", "ApiClientType" }; public bool TypeCanBeNull() => !NotNullTypes.Contains(FieldType); private string UpperInitial(string name) { return name[0].ToString().ToUpperInvariant() + name.Substring(1); } } class DalGetBy { public bool IsTry { get; } public string Alias { get; } public Dictionary<string, string> Filters { get; } = new Dictionary<string, string>(); public DalGetBy(XmlElement sourceXml) { IsTry = sourceXml.HasAttribute("try"); Alias = sourceXml.GetAttribute("alias"); foreach (XmlElement filterNode in sourceXml.SelectNodes("*[local-name()='field']")) Filters.Add(filterNode.GetAttribute("field"), filterNode.GetAttribute("alias")); } public string GetConditions() { return string.Join(" && ", Filters.Select(f => $"e.{f.Key} == {f.Value}")); return string.Join(" && ", Filters.Select(f => $"e.{f.Key} == {f.Value}")); } } class DalEmitBo { public string Namespace { get; } public bool EmitMapping { get; } private XmlElement sourceXml; public DalEmitBo(XmlElement sourceXml) { Namespace = sourceXml.GetAttribute("ns"); EmitMapping = sourceXml.HasAttribute("emitMapping"); this.sourceXml = sourceXml; } public string GetMapping(DalField field) { bool notNull = !field.TypeCanBeNull() || field.NotNull; var overridenXml = sourceXml.SelectNodes("*[local-name()='column']").Cast<XmlElement>().SingleOrDefault(e => e.GetAttribute("name") == field.Name); var props = overridenXml != null ? new OverridenProperties(overridenXml) : null; switch(field.FieldType) { case "string": return $"<property name=\"{field.Name}\"><column name=\"{field.Name}\" {(notNull ? "not-null=\"true\"" : string.Empty)} {(props?.SqlType !=null ? "sql-type=\"" + props.SqlType+"\"" : string.Empty)}/></property>"; case "bool": return $"<property name=\"{field.Name}\" not-null=\"true\" type=\"boolean\"><column name=\"{field.Name}\" not-null=\"true\" default=\"{props?.DefaultValue??"0"}\" sql-type=\"bit\" /></property>"; case "DateTime": return $"<property name=\"{field.Name}\" not-null=\"true\"/>"; case "DateTime?": return $"<property name=\"{field.Name}\" />"; case "ProgramType": return $"<property name=\"{field.Name}\"><column name=\"{field.Name}\" default=\"{props?.DefaultValue??"0"}\" {(notNull ? "not-null=\"true\"" : string.Empty)}/></property>"; case "Guid?": return $"<property name=\"{field.Name}\" />"; default: throw new ArgumentOutOfRangeException($"Not supported type {field.FieldType}. Edit DAL.tt to add mapping definition."); } } private class OverridenProperties { public string DefaultValue { get; } public string SqlType { get; } public OverridenProperties(XmlElement sourceXml) { DefaultValue = sourceXml.GetAttribute("default"); SqlType = sourceXml.GetAttribute("sql-type"); } } } class DalEmitRepository { public string Interface { get; } public string DependsOn { get; } public DalEmitRepository(XmlElement sourceXml, string className) { Interface = string.IsNullOrEmpty(sourceXml.InnerText) ? "IRepository<"+className+">" : sourceXml.InnerText; DependsOn = sourceXml.GetAttribute("dependsOn"); } public string GetRepositoryClassName() => Interface.Substring(1); } 


Der Code für die resultierende Vorlage
 <#@ template debug="false" hostspecific="true" language="C#" #> <#@ assembly name="System.Xml" #> <#@ assembly name="System.Core" #> <#@ assembly name="EnvDTE" #> <#@ import namespace="System.Xml" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Linq" #> <#@ output extension="/" #> <# EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host) .GetService(typeof(EnvDTE.DTE)); XmlDocument doc = new XmlDocument(); doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "DAL.xml")); var classes = doc.SelectNodes("//*[local-name()='class']").Cast<XmlElement>().Select(classXml=>new DalClass(classXml)).ToArray(); //fields foreach(var classNode in classes) { #> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using JetBrains.Annotations; <#=classNode.GetIncludedNamespaces()#> namespace Domain.DALV2 { public class <#=classNode.GetCachedClassDefinition()#> { public Cached<#=classNode.Name#>(<#=classNode.DalBOType#> source) : base(source) { <#=string.Join("\n\t\t\t", classNode.Fields.Where(f => f.PropertySource == null).Select(f=>f.GetConstructorInitValueExpression()))#> } <#=string.Join("\n\n\t\t", classNode.Fields.Select(f=>f.GetPropertyDefinitionExpression()))#> } } <#if (classNode.GenerateInterface){#> namespace Domain.DAL { public interface I<#=classNode.Name#> <#if (classNode.LogicallyDeletable){#> :ILogicallyDeletableReadonly <#}#> { Guid Id { get; } <#=string.Join("\n\n\t\t", classNode.Fields.Select(f=>f.GetInterfacePropertyDefinitionExpression()))#> } } <#SaveOutput("Entities//gen//"+classNode.Name+".g.cs");#> <#}#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using Resto.Framework.Common; using JetBrains.Annotations; <#=classNode.GetIncludedNamespaces()#> namespace Domain.DALV2 { public partial class <#=classNode.Name#>DAL : DAL<Cached<#=classNode.Name#>, <#=classNode.BasedOn#>, <#=classNode.DalBOType#>> { internal <#=classNode.Name#>DAL(ISessionManager sessionManager, ICacheConsistencyManager<<#=classNode.BasedOn#>, <#=classNode.DalBOType#>> consistencyManager) : base(sessionManager, Repositories.<#=classNode.Name#>, consistencyManager) { } <#foreach (var getBy in classNode.GetBy){#> <#=getBy.IsTry?"[CanBeNull]":"[NotNull]"#> public Cached<#=classNode.Name#> <#=getBy.IsTry?"Try":""#>GetBy<#=getBy.Alias#>(<#=classNode.GetFilterParameters(getBy)#>) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return Cache.Values.Single<#=getBy.IsTry?"OrDefault":""#>(e => <#=getBy.GetConditions()#>); } } <#}#> <#foreach (var fieldNode in classNode.GetAllBy){#> [NotNull] [ItemNotNull] public IList<Cached<#=classNode.Name#>> GetAllBy<#=fieldNode.Alias#>(<#=classNode.GetFilterParameters(fieldNode)#>) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return Cache.Values.Where(e => <#=fieldNode.GetConditions()#>).ToList(); } } <#}#> [NotNull] protected override Cached<#=classNode.Name#> Convert(<#=classNode.DalBOType#> source) { return new Cached<#=classNode.Name#>(source); } } } <#SaveOutput("Repositories//gen//"+ classNode.Name+".g.cs");#> <#if(classNode.EmitBo != null){#> <?xml version="1.0" encoding="utf-8"?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Domain" namespace="<#=classNode.EmitBo.Namespace#>"> <class name="<#=classNode.Name#>" dynamic-update="true" dynamic-insert="true"> <id name="Id"> <generator class="assigned" /> </id> <#=string.Join("\n\t\t", classNode.ExplicitlyDefinedFields.Select(f=>classNode.EmitBo.GetMapping(f)))#> <#if (classNode.LogicallyDeletable){#> <property name="IsDeleted" not-null="true" type="boolean"> <column name="IsDeleted" not-null="true" default="0" /> </property> <property name="WhenDeleted" type="DateTime" not-null="false"/> <#}#> </class> </hibernate-mapping> <#SaveOutput("..\\..\\DAL.Hibernate\\Mapping\\gen\\"+classNode.Name+".hbm.xml");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using System; using Common.Domain.BO.DB; using Domain.DAL; using Domain.DALV2; using JetBrains.Annotations; <#=classNode.GetIncludedNamespaces()#> namespace <#=classNode.EmitBo.Namespace#> { public partial class <#=classNode.GetBoClassDefinition()#> { [UsedImplicitly] protected <#=classNode.Name#>(){} <#=string.Join("\n\n\t\t", classNode.ExplicitlyDefinedFields.Select(f=>f.GetBOPropertyDefinitionExpression()))#> <#if (classNode.LogicallyDeletable){#> public virtual bool IsDeleted { get; set; } public virtual DateTime? WhenDeleted { get; set; } <#}#> } } <# SaveOutput("..//BO//gen//"+ classNode.Name+".g.cs"); } } #> <!-- The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. --> <objects xmlns="http://www.springframework.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:db="http://www.springframework.net/database" xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd"> <# foreach(var classNode in classes.Where(c => !c.CustomConsistencyManager)) { #> <object id="<#=LowerInitial(classNode.Name)#>CacheManager" type="Domain.DALV2.TransactionSubscribedManager<<#=classNode.BasedOn#>, <#=classNode.DalBOType#>>, Domain" singleton="true"> </object> <#}#> <object id="dalGeneratedListeners" type="System.Collections.Generic.List<Common.Hibernate.DAL.ISessionNotificationListener>, mscorlib"> <constructor-arg> <list element-type="Common.Hibernate.DAL.ISessionNotificationListener, Common.Hibernate"> <# foreach(var classNode in classes) { #> <ref object="<#=LowerInitial(classNode.Name)#>CacheManager"/> <#}#> </list> </constructor-arg> </object> </objects> <#SaveOutput("..//Config//DALSpringDefinitions.g.xml");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using Common; using Common.Domain.DAL; using Domain.BO; using Domain.DALV2; using Spring.Context.Support; using JetBrains.Annotations; <#=string.Join("\n", classes.Select(c=>c.GetIncludedNamespaces()).Where(s=>!string.IsNullOrEmpty(s)))#> namespace Domain.DAL { public partial class DALs { private static readonly SafeLazy<DALs> instance = new SafeLazy<DALs>(() => new DALs()); private DALs() { var sessionManager = (ISessionManager)ContextRegistry.GetContext().GetObject("sessionManager"); <# foreach(var classNode in classes){ #> this.<#=LowerInitial(classNode.Name)#> = new <#=classNode.Name#>DAL(sessionManager, (ICacheConsistencyManager<<#=classNode.BasedOn#>, <#=classNode.DalBOType#>>)ContextRegistry.GetContext().GetObject("<#=LowerInitial(classNode.Name)#>CacheManager")); <# } #> } <# foreach(var classNode in classes) { #> [NotNull] private readonly <#=classNode.Name#>DAL <#=LowerInitial(classNode.Name)#>; [NotNull] public static <#=classNode.Name#>DAL <#=classNode.Name#> => instance.Value.<#=LowerInitial(classNode.Name)#>; <#}#> public static void ResetAll() => instance.Value.ResetAllImpl(); private void ResetAllImpl() { <#foreach(var classNode in classes){#> this.<#=LowerInitial(classNode.Name)#>.Reset(); <#}#> } } } <#SaveOutput("DALs.g.cs");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using JetBrains.Annotations; namespace Domain.DAL { public partial interface IRepositoryFactory { <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> [NotNull] <#=classNode.EmitRepository.Interface#> <#=classNode.Name#> { get; } <#}#> } } <#SaveOutput("IRepositoryFactory.g.cs");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using JetBrains.Annotations; namespace Domain.DAL { public static partial class Repositories { <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> [NotNull] public static <#=classNode.EmitRepository.Interface#> <#=classNode.Name#> => Instance.Value.<#=classNode.Name#>; <#}#> } } <#SaveOutput("Repositories.g.cs");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using Common.Domain.DAL; using Common.Hibernate.DAL; using DAL.Hibernate.Cache.CachingProviders; using Domain.BO; using Domain.DAL; using Domain.DAL.Cache; using Domain.SaveManager; using System; using System.Collections.Generic; using JetBrains.Annotations; using Engine.Main; <#=string.Join("\n", classes.Select(c=>c.GetIncludedNamespaces()).Where(s=>!string.IsNullOrEmpty(s)))#> namespace DAL.Hibernate.DAL { public partial class RepositoryFactory : IRepositoryFactory { public void Init([NotNull] IOrganizationsByNetworkCache organizationsByNetworkCache, [NotNull] INetworkCache networkCache, [NotNull] ITransactionSaveManager transactionSaveManager, [NotNull] ILoginEntryDataProvider loginEntryDataProvider, ListenerNotifier listenerNotifier) { <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> this.<#=classNode.Name#> = new <#=classNode.EmitRepository.GetRepositoryClassName()#>(<#=classNode.EmitRepository.DependsOn#>); <#}#> } <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> [NotNull] public <#=classNode.EmitRepository.Interface#> <#=classNode.Name#> { get; private set; } <#}#> } } <#SaveOutput("..\\..\\DAL.Hibernate\\DAL\\RepositoryFactory.g.cs");#> <#+ private string LowerInitial(string name) { return name[0].ToString().ToLowerInvariant() + name.Substring(1); } private string UpperInitial(string name) { return name[0].ToString().ToUpperInvariant() + name.Substring(1); } void SaveOutput(string outputFileName) { string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); string outputFilePath = Path.Combine(templateDirectory, outputFileName); File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length); } #> 


Und wo ohne eine gewisse Menge an kniffligem generischem Code abonniert werden muss, um Änderungen an der Datenbank vorzunehmen, Caches bei Bedarf zu aktualisieren und andere Freuden des Lebens. Hier stellte sich heraus, dass generisch genug ist, um alle Anwendungsfälle zu schließen, gepaart mit denen, die bereits mit verwendet wurden

Gemeinsamer Code für den Zugriff auf Daten
  public abstract class DAL<T, TBase, TCachedBo> where T : CachedEntity<TBase> where TBase : class, IEntity where TCachedBo : class, IIdEntity<Guid> { [NotNull] private readonly ILog log; [NotNull] protected readonly ISessionManager SessionManager; [NotNull] protected readonly IReadonlyRepository<TBase> BaseRepository; [NotNull] protected readonly ICacheConsistencyManager<TBase, TCachedBo> ConsistencyManager; [NotNull] protected Dictionary<Guid, T> Cache = new Dictionary<Guid, T>(); protected DAL([NotNull] ISessionManager sessionManager, [NotNull] IReadonlyRepository<TBase> baseRepository, [NotNull] ICacheConsistencyManager<TBase, TCachedBo> consistencyManager) { this.SessionManager = sessionManager; this.BaseRepository = baseRepository; this.ConsistencyManager = consistencyManager; log = LogManager.GetLogger(GetType()); } [CanBeNull] public T TryGetById(Guid id) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return Cache.GetOrDefault(id); } } [NotNull] public T GetById(Guid id) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return ValidateEntityFound(Cache.GetOrDefault(id), "{0} with id {1} not found", null, typeof(T), id); } } [NotNull] public TBase GetEntity([NotNull] ISession session, Guid id) { return BaseRepository.GetById(session, id); } [NotNull] public TBase GetEntity([NotNull] ISession session, [NotNull] T e) { return BaseRepository.GetById(session, e.Id); } [NotNull] public HashSet<T> GetByIds([NotNull] HashSet<Guid> ids) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return ids .Select(id => Cache.GetOrDefault(id)) .ToHashSet(); } } public void Reset() { ConsistencyManager.Reset(); } protected abstract T Convert(TCachedBo source); protected void UpdateCacheIfNeeded() { if (!ConsistencyManager.UpdateRequired) return; log.Debug($"{typeof(T).Name} DAL: Update required, getting writeLock"); using (var updateScope = ConsistencyManager.GetWriteLock()) { if (updateScope.UpdateRequired ?? ConsistencyManager.UpdateRequired) SessionManager.RunTransacted(session => { try { var updatedEntities = updateScope.GetUpdatedEntities(BaseRepository, session); foreach (var updatedEntity in updatedEntities) if (updatedEntity.Value != null) Cache[updatedEntity.Key] = Convert(updatedEntity.Value); else Cache.Remove(updatedEntity.Key); Reindex(); } catch (Exception) { ConsistencyManager.Reset(); throw; } }); } } /// <summary> /// Reevaluate specific indexes, used for search in cached entities. Called after cache has been updated. /// </summary> protected virtual void Reindex() { } [NotNull] protected T1 ValidateEntityFound<T1>([CanBeNull] T1 entity, [NotNull] string errorMessage, [NotNull] string frontMessage, [NotNull] params object[] p) { if (entity == null) throw new DataAccessException(Util.GetMessage(errorMessage, p), Util.GetMessage(frontMessage, p)); return entity; } } 



Interessanterweise stellte sich heraus, dass es sich um eine Klasse handelte, die die Cache-Konsistenz überwachte und deren Aktualisierung verwaltete. Dies war der Hauptpunkt, an dem ich über die Implementierung von Sperren nachdenken musste, damit alles optimal und minimal blockierend, aber gleichzeitig geschützt war

Eigentlich Umsetzung
  [UsedImplicitly] public class TransactionSubscribedManager<TBase, TCachedBo> : EmptySessionNotificationListener, ICacheConsistencyManager<TBase, TCachedBo>, ITransactionNotificationListener where TBase : class, IEntity where TCachedBo : class, IIdEntity<Guid> { public bool UpdateRequired => needFullUpdate || !entitiesToUpdate.IsEmpty; [NotNull] protected readonly ReaderWriterLockSlim LockObj = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); [NotNull] private readonly ConcurrentDictionary<string, ConcurrentBag<Guid>> changesByTransaction = new ConcurrentDictionary<string, ConcurrentBag<Guid>>(); [NotNull] protected ConcurrentBag<Guid> entitiesToUpdate = new ConcurrentBag<Guid>(); protected bool needFullUpdate = true; public virtual CacheWriteLockScope<TBase, TCachedBo> GetWriteLock() { try { LockObj.EnterWriteLock(); if (needFullUpdate) { needFullUpdate = false; return new CacheWriteLockScope<TBase, TCachedBo>(LockObj); } return new IdBasedCacheWriteLockScope<TBase, TCachedBo>(LockObj, ChangedEntitiesIdsProvider); } catch (Exception) { LockObj.ExitWriteLock(); throw; } } protected ICollection<Guid> ChangedEntitiesIdsProvider() { var ids = new List<Guid>(); var newBag = new ConcurrentBag<Guid>(); var oldBag = Interlocked.Exchange(ref entitiesToUpdate, newBag); while (oldBag.TryTake(out var id)) ids.Add(id); return ids; } public CacheReadLockScope GetReadLock() { return new CacheReadLockScope(LockObj); } public override void OnSaveOrUpdate(ISession session, object entity, Guid id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) { if (entity is TBase) { var transactionName = HibernateSessionManager.GetTransactionName(); if (!string.IsNullOrEmpty(transactionName)) { if (changesByTransaction.TryAdd(transactionName, new ConcurrentBag<Guid>() { id })) return; changesByTransaction.TryGetValue(transactionName, out var transactionChangedEntities); // ReSharper disable once PossibleNullReferenceException // R# does not have attributes telling that value in dictionary is not null. But we know it. transactionChangedEntities.Add(id); } } } public override void OnDelete(ISession session, object entity, Guid id) { if (entity is TBase) { var transactionName = HibernateSessionManager.GetTransactionName(); if (string.IsNullOrEmpty(transactionName)) return; if (changesByTransaction.TryAdd(transactionName, new ConcurrentBag<Guid>() { id })) return; changesByTransaction.TryGetValue(transactionName, out var transactionChangedEntities); // ReSharper disable once PossibleNullReferenceException // R# does not have attributes telling that value in dictionary is not null. But we know it. transactionChangedEntities.Add(id); } } public void Reset() { needFullUpdate = true; } void ITransactionNotificationListener.AfterCommit(ISession session, string transactionName) { if (changesByTransaction.TryRemove(transactionName, out var transactionChangedEntities) && !transactionChangedEntities.IsEmpty) while (transactionChangedEntities.TryTake(out var id)) entitiesToUpdate.Add(id); } void ITransactionNotificationListener.AfterRollback(ISession session, string transactionName) { changesByTransaction.TryRemove(transactionName, out _); } } 

  public class IdBasedCacheWriteLockScope<TBase, TCachedBo> : CacheWriteLockScope<TBase, TCachedBo> where TBase : class, IEntity where TCachedBo : class, IIdEntity<Guid> { [NotNull] private readonly ICollection<Guid> changedEntitiesIds; public override bool? UpdateRequired => changedEntitiesIds.Any(); public IdBasedCacheWriteLockScope([NotNull] ReaderWriterLockSlim lockObj, [NotNull] Func<ICollection<Guid>> changedEntitiesIdsProvider) : base(lockObj) { changedEntitiesIds = changedEntitiesIdsProvider() ?? throw new InvalidOperationException(nameof(changedEntitiesIdsProvider)); } public override IDictionary<Guid, TCachedBo> GetUpdatedEntities(IReadonlyRepository<TBase> repository, ISession session) { var entities = repository.GetByIds(session, changedEntitiesIds, true).ToDictionary(e => e.Id, be => be as TCachedBo); foreach (var id in changedEntitiesIds.Where(i => !entities.ContainsKey(i))) entities.Add(id, null); return entities; } } 


Interessante Pfosten, die während der Entwicklung und Erweiterung der Schlacht aufgedeckt wurden:

  • Es ist nur so, dass Sie verwandte Entitäten mit direkten Verknüpfungen nicht abrufen und zwischenspeichern können. Andernfalls verbleibt die veraltete Kopie in den Objekten, die darauf verweisen, wenn wir eine der Entitäten ändern. Anstatt eine knifflige Logik für die Ungültigmachung solcher Verbindungen zu erstellen, haben wir uns einfach beim Zugriff über die Eigenschaft entschieden, immer die neuesten Informationen aus dem Cache abzurufen
  • , , ( , , , ). ,
  • «» ( ) , ,

Source: https://habr.com/ru/post/de453382/


All Articles