Cómo hacer un almacenamiento en caché altamente personalizable en un proyecto y evitar que los colegas escriban el mismo tipo de código

"Si la esencia del trabajo del programador es automatizar el trabajo de otras personas, entonces, ¿por qué mi trabajo es tan poco automatizado?", Pensé, una vez más copiando todos los enlaces necesarios en el proyecto para agregar una nueva entidad a la base de datos. Y decidí deshacerme de esta rutina de agregar clases de plantilla, haciendo al mismo tiempo "bueno" al proyecto, descargando la base de datos de operaciones de lectura innecesarias.

Una pequeña digresión sobre el sistema que estamos desarrollando y su estado en el momento en que comenzó este experimento:

  • Un sistema en el que el 90% de los datos se cambia y procesa activamente (transacciones, datos personales, agregaciones calculadas), pero rara vez se lee, y el 10% restante rara vez cambia, pero se usa para leer en cada oportunidad
  • Servicio casi monolítico en .Net Framework, en el que se implementa todo esto
  • Nhibernate con enlace mínimo, utilizado para acceder a la base de datos y cierta cantidad de cachés basados ​​en ella (suscripciones para cambios en entidades BO, manejadores llamados al confirmar la transacción)
  • Una docena de reglas tácitas "cómo escribir código para acceder a la base de datos sin afectar el rendimiento con las funciones de NHibernate", que se encuentra regularmente en Core Review
  • Una base de datos algo aburrida que necesita optimización

Al pensar en la situación con la base de datos, surgió la idea: si eliminar de la carga en la base de datos el mismo 10% de las solicitudes (y al mismo tiempo abrir las conexiones a la base de datos que son necesarias para ellas, mantener transacciones abiertas y usar el código de plantilla para acceder a la base de datos a través de nuestros repositorios). En este caso, fue necesario considerar:

  • Ya intentamos trabajar con el caché Nhibernate, pero descubrimos que su comportamiento no siempre es explícito y predecible.
  • No quería cambiar nada fundamentalmente en la plataforma o infraestructura sin una buena razón
  • Como resultado, la cantidad de código escrito a mano, como cualquier programador perezoso, debería haber disminuido, escribiendo cientos de líneas de envoltorios y suscripciones para cachés existentes con sus manos

Un ejemplo de implementación de uno de estos viejos cachés
/// <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); } } } 


en ese momento ya un poco cansado.
- Una nueva característica no debería afectar globalmente a otros desarrolladores, la migración a un nuevo enfoque debería ocurrir sin problemas y suavemente.

Al mismo tiempo, quería asegurarme de que agregar una nueva entidad a los mecanismos de almacenamiento en caché requería un mínimo esfuerzo, el código seguía siendo legible y directo, y al recuperar datos de la memoria caché, uno podía pensar mínimamente en qué código auxiliar debería escribirse. Los últimos dos puntos reducen enormemente el rango de opciones. De hecho, debe marcar la diferencia con la reflexión y las clases genéricas, o recurrir a la buena y antigua metaprogramación.

Como la principal herramienta de desarrollo es Visual Studio, pero no quería gastar mucha energía en el hecho de que no era un gran efecto, decidí tomar la decisión "en la frente" con las herramientas más estándar y solo en la etapa de la Prueba de concepto terminada en un par de las más utilizadas entidades: presenten una decisión a los colegas en la corte.

El siguiente fue un dilema moral. Ya sea para usar como fuente alguna clase colgada de atributos para todas las ocasiones (en el estilo de mapeo fluido de Nhibernate en esteroides), o escribir un buen XML ordenado. Recordando que la pereza es el mejor amigo de un programador, y describir clases con atributos lleva más tiempo que escribir un pequeño XML, opté por lo último.

En esencia, ¿qué necesitaba de la descripción de las entidades?

  • Descripción de los campos en caché
  • La capacidad de especificar las propiedades por las cuales haremos muestras a partir de estos datos, si es necesario, la capacidad de optimizar estas muestras eliminando el paso lineal a través de listas (si juega en la optimización, tan completo)
  • Cualquier conveniencia adicional para distribuir clases en diferentes carpetas y usar los desarrollos existentes en el código para reducir su número

Tenemos esta estructura 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> 


Y una gran plantilla de T4 aterradora para analizarla y generar clases variadas, pero muy necesarias:

  • Un tipo "en caché" con los mismos campos que BO pero no editable
  • Implementación de un caché para un tipo con métodos de selección de filtro
  • Un registro de cachés para suscribirse a los mecanismos de Nhibernate para notificaciones y registro en DI (Spring en nuestro caso)
  • Interfaces para todo esto para ocultar el interior y la capacidad, si es necesario, para reemplazar el código generado por escrito a mano y viceversa.
  • Como una bonificación agradable no planificada desde el principio, si el trabajo con las entidades principales ya estaba listo, hice medio paso a la suerte, y agregué a un lado la capacidad de generar tipos de BO simples y mapeos para darles a los colegas la oportunidad de agregar nuevas clases con media patada.

La plantilla en sí, desde un punto de vista lógico, consta de dos partes: analizar el xml original en clases que describen la estructura de clase deseada (para reducir el riesgo de cualquier comportamiento implícito, en este caso, se decidió hacerlo mediante el análisis explícito de las etiquetas, y no mediante el mapeo atributos.

Clases estructurales
 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); } 


El código de la plantilla resultante.
 <#@ 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); } #> 


Y donde sin una cierta cantidad de código genérico complicado para suscribirse para realizar cambios en la base de datos, actualizar cachés a pedido y otros placeres de la vida. Aquí, el genérico resultó ser suficiente para cerrar todos los casos de uso, junto con los que ya se utilizan con

Código común para acceder a datos
  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; } } 



Interesante, en mi opinión, resultó ser una clase que monitoreaba la consistencia de la caché y administraba su actualización, el lugar principal donde tenía que pensar en implementar bloqueos para que todo fuera óptimo y mínimamente bloqueado, pero al mismo tiempo protegido

Realmente implementación
  [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; } } 


Jambas interesantes que se revelaron durante el desarrollo y la expansión en la batalla:

  • Es solo que no puede extraer y almacenar en caché entidades relacionadas con enlaces directos entre ellas. De lo contrario, cuando modificamos una de las entidades, la copia desactualizada permanece en los objetos que hacen referencia a ella. En lugar de hacer una lógica engañosa de invalidar tales conexiones, decidimos simplemente al acceder por propiedad para obtener siempre la información más reciente del caché
  • , , ( , , , ). ,
  • «» ( ) , ,

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


All Articles