كيفية جعل التخزين المؤقت للتخصيص للغاية في مشروع وحفظ الزملاء من كتابة نفس النوع من التعليمات البرمجية

"إذا كان جوهر عمل المبرمج هو أتمتة عمل الأشخاص الآخرين ، فلماذا لا يكون عملي مؤتمتًا إلا قليلاً" ، حسب اعتقادي ، ونسخًا مرة أخرى جميع الارتباطات اللازمة في المشروع لإضافة كيان جديد إلى قاعدة البيانات. وقررت التخلص من هذا الروتين المتمثل في إضافة فئات القوالب ، مع القيام في الوقت نفسه بـ "حسن" المشروع ، وإلغاء تحميل قاعدة البيانات من عمليات القراءة غير الضرورية.

انحدار صغير حول النظام الذي نطوره وحالته في وقت بدء هذه التجربة:

  • نظام يتم فيه تغيير ومعالجة 90٪ من البيانات بنشاط (المعاملات ، البيانات الشخصية ، المجموعات المحسوبة) ، ولكن نادرًا ما يتم قراءتها ، و 10٪ المتبقية نادراً ما تتغير ، ولكنها تستخدم للقراءة في كل فرصة
  • خدمة متجانسة تقريبا على .Net Framework ، حيث يتم تنفيذ كل هذا
  • Nhibernate مع الحد الأدنى من الربط ، وتستخدم للوصول إلى قاعدة البيانات وبعض كمية من ذاكرة التخزين المؤقت على أساس ذلك (اشتراكات التغييرات في كيانات BO ، ودعا معالجات الالتزام الالتزام بالمعاملة)
  • هناك أكثر من عشرة قواعد غير معلن عنها "كيفية كتابة التعليمات البرمجية للوصول إلى قاعدة البيانات دون قتل الأداء باستخدام ميزات NHibernate" ، التي يتم اكتشافها بانتظام في Core Review
  • قاعدة بيانات مملة إلى حد ما في حاجة إلى التحسين

أثناء التفكير في الموقف مع قاعدة البيانات ، نشأت الفكرة: ما إذا كانت ستزيل 10٪ من الطلبات من التحميل على قاعدة البيانات (وفي نفس الوقت تفتح الاتصالات بقاعدة البيانات الضرورية لهم ، وتجري المعاملات المفتوحة وتستخدم رمز القالب للوصول إلى قاعدة البيانات من خلال مستودعاتنا). في هذه الحالة ، كان من الضروري مراعاة:

  • لقد حاولنا بالفعل العمل مع ذاكرة التخزين المؤقت Nhibernate ، ولكن وجدنا أن سلوكها ليس دائمًا واضحًا ويمكن التنبؤ به.
  • لم أكن أريد تغيير أي شيء بشكل أساسي في النظام الأساسي أو البنية التحتية دون سبب وجيه
  • يجب أن يكون عدد الرموز المكتوبة بخط اليد ، مثل أي مبرمج كسول ، قد انخفض نتيجة لذلك ، حيث كتب مئات الأسطر من الأغلفة والاشتراكات في ذاكرات التخزين المؤقت الموجودة بأيديكم

مثال على تنفيذ أحد هذه المخابئ القديمة
/// <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); } } } 


في ذلك الوقت بالفعل متعب قليلا.
- لا ينبغي أن تؤثر الميزة الجديدة على المستوى العالمي على المطورين الآخرين ، ويجب أن تتم عملية الترحيل إلى نهج جديد بسلاسة ولطف.

في نفس الوقت ، أردت التأكد من أن إضافة كيان جديد إلى آليات التخزين المؤقت استغرق أقل جهد ممكن ، وظلت الشفرة قابلة للقراءة ومباشرة ، وعند استرجاع البيانات من ذاكرة التخزين المؤقت ، يمكن للمرء أن يفكر إلى الحد الأدنى فيما يتعلق بما يجب كتابة التعليمات البرمجية الإضافية. قطعت النقطتان الأخيرتان إلى حد كبير نطاق الخيارات. في الواقع ، عليك إما أن تحدث فرقًا في الفصول العاكسة والفصول العامة ، أو أن تتحول إلى metaprogramming القديمة الجيدة.

نظرًا لأن أداة التطوير الرئيسية هي Visual Studio ، لكنني لا أرغب في إنفاق الكثير من الطاقة على حقيقة أنه لم يكن تأثيرًا كبيرًا ، فقد قررت اتخاذ القرار "على الجبهة" باستخدام أكثر الأدوات القياسية وفقط في مرحلة Proof Of Concept النهائي على اثنين من الأكثر استخدامًا الكيانات - تقديم قرار إلى الزملاء في المحكمة.

كان التالي بعض المعضلة الأخلاقية. سواء للاستخدام كمصدر ، فقد تم تعليق بعض الصفات بسمات لجميع المناسبات (بأسلوب تعيين Fluent في Nhibernate على المنشطات) ، أو كتابة XML أنيق. تذكر أن الكسل هو أفضل صديق للمبرمجين ، ووصف الفئات ذات السمات يستغرق وقتًا أطول من كتابة XML صغير ، لقد اخترت هذا الأخير.

في جوهرها ، ما الذي احتاجه من وصف الكيانات؟

  • وصف الحقول المخزنة مؤقتًا
  • القدرة على تحديد الخصائص التي سنعمل بها على أخذ عينات من هذه البيانات ، إذا لزم الأمر ، القدرة على تحسين هذه العينات عن طريق القضاء على المرور الخطي من خلال القوائم (إذا كنت تلعب في التحسين ، أكمل ذلك)
  • أي راحة إضافية من أجل توزيع الفئات في مجلدات مختلفة واستخدام التطورات الموجودة في التعليمات البرمجية لتقليل عددها

حصلنا على هذا الهيكل XML
 <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> 


و T4 قالب مخيف كبير لتحليله وتوليد فئات متنوعة ، ولكن هناك حاجة ماسة إليها:

  • نوع "مؤقت" مع نفس الحقول مثل BO ولكن غير قابل للتحرير
  • تطبيق ذاكرة التخزين المؤقت لنوع مع أساليب اختيار عامل التصفية
  • سجل مخبأ للاشتراك في آليات Nhibernate للإخطارات والتسجيل في DI (الربيع في حالتنا)
  • واجهات لجميع هذا لإخفاء الدواخل والقدرة ، إذا لزم الأمر ، لاستبدال التعليمات البرمجية التي تم إنشاؤها بخط اليد والعودة.
  • كمكافأة لطيفة غير مخططة من البداية ، إذا كان العمل مع الكيانات الرئيسية جاهزًا بالفعل - لقد فعلت ذلك بنصف خطوة إلى الحظ ، وأضفت إلى الجانب القدرة على إنشاء أنواع BO بسيطة وتعيينات لهم ، لمنح الزملاء فرصة لإضافة فصول جديدة بضربة حرة.

يتكون القالب نفسه ، من وجهة نظر منطقية ، من جزأين: تحليل xml الأصلي إلى فئات تصف بنية الفصل المرغوبة (للحد من مخاطر أي سلوك ضمني ، في هذه الحالة ، تقرر القيام بذلك من خلال تحليل العلامة الصريح ، وليس من خلال التعيين الصفات.

الطبقات الهيكلية
 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); } 


رمز القالب الناتج
 <#@ 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); } #> 


وحيث لا يوجد قدر معين من الشفرة العامة الصعبة للاشتراك في إجراء تغييرات على قاعدة البيانات ، قم بتحديث ذاكرات التخزين المؤقت عند الطلب ، وغيرها من متع الحياة. اتضح أن هذا النوع من الأدوية العامة يكفي لإغلاق جميع حالات الاستخدام ، إلى جانب تلك المستخدمة بالفعل

رمز مشترك للوصول إلى البيانات
  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; } } 



من المثير للاهتمام ، في رأيي ، أنه تحول إلى فصل يراقب تناسق ذاكرة التخزين المؤقت ويدير تحديثه ، وهو المكان الرئيسي الذي كان علي أن أفكر فيه في تطبيق الأقفال بحيث يكون كل شيء مثاليا ويمنع الحد الأدنى ، ولكن في نفس الوقت محمي

في الواقع التنفيذ
  [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; } } 


, :

  • . , , , . , —
  • , , ( , , , ). ,
  • «» ( ) , ,

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


All Articles