"Si l'essence du travail du programmeur est d'automatiser le travail des autres, alors pourquoi mon travail est-il si peu automatisé", ai-je pensé, en copiant à nouveau toutes les liaisons nécessaires dans le projet pour ajouter une nouvelle entité à la base de données. Et j'ai décidé de me débarrasser de cette routine d'ajout de classes de modèles, en faisant en même temps «du bien» au projet, en déchargeant la base de données des opérations de lecture inutiles.
Une petite digression sur le système que nous développons et son état au début de cette expérience:
- Un système dans lequel 90% des données sont activement modifiées et traitées (transactions, données personnelles, agrégations calculées), mais rarement lues, et les 10% restants changent très rarement, mais utilisées pour la lecture à chaque occasion
- Service quasi monolithique sur .Net Framework, dans lequel tout cela est implémenté
- Nhibernate avec une liaison minimale, utilisé pour accéder à la base de données et à un certain nombre de caches basés sur celle-ci (abonnements pour les modifications des entités BO, gestionnaires appelés lors de la validation de la transaction)
- Une douzaine de règles tacites "comment écrire du code pour accéder à la base de données sans nuire aux performances avec les fonctionnalités NHibernate", régulièrement capturées sur Core Review
- Une base de données quelque peu ennuyeuse qui a besoin d'optimisation
En pensant à la situation avec la base de données, la pensée s'est posée: faut-il supprimer de la charge de la base de données ces mêmes 10% des demandes (et en même temps ouvrir les connexions à la base de données dont elles ont besoin, maintenir les transactions ouvertes et utiliser le code modèle pour accéder à la base de données via nos référentiels). Dans ce cas, il fallait considérer:
- Nous avons déjà essayé de travailler avec le cache Nhibernate, mais nous avons constaté que son comportement n'était pas toujours explicite et prévisible.
- Je ne voulais rien changer fondamentalement dans la plateforme ou l'infrastructure sans raison valable
- Le nombre de codes manuscrits, comme tout programmeur paresseux, aurait dû diminuer en conséquence, en écrivant des centaines de lignes d'encapsuleurs et d'abonnements pour les caches existants avec vos mains
Un exemple d'implémentation de l'un de ces anciens caches à cette époque déjà un peu fatigué.
- Une nouvelle fonctionnalité ne devrait pas affecter globalement les autres développeurs, la migration vers une nouvelle approche devrait se faire en douceur et en douceur.
Dans le même temps, je voulais m'assurer que l'ajout d'une nouvelle entité aux mécanismes du cache prenait un minimum d'efforts, le code restait lisible et simple, et lors de la récupération des données du cache, vous pouviez penser de manière minimale au code auxiliaire à écrire. Les deux derniers points réduisent considérablement la gamme d'options. En fait, vous devez soit faire la différence avec la réflexion et les classes génériques, soit vous tourner vers la bonne vieille métaprogrammation.
Étant donné que l'outil de développement principal est Visual Studio, mais je ne voulais pas consacrer beaucoup d'énergie au fait que ce n'était pas un effet énorme, j'ai décidé de prendre la décision «sur le front» avec les outils les plus standard et uniquement au stade de la preuve de concept terminée sur quelques-uns des plus utilisés. entités - présenter une décision à des collègues du tribunal.
Vient ensuite un dilemme moral. Que ce soit pour utiliser comme source une classe avec des attributs pour toutes les occasions (dans le style de la cartographie Fluent de Nhibernate sur les stéroïdes), ou pour écrire un joli XML soigné. En me rappelant que la paresse est la meilleure amie d'un programmeur, et la description des classes avec des attributs prend plus de temps que d'écrire un peu de XML, j'ai opté pour ce dernier.
En substance, de quoi avais-je besoin dans la description des entités?
- Description des champs mis en cache
- La possibilité de spécifier les propriétés par lesquelles nous allons faire des échantillons à partir de ces données, si nécessaire, la possibilité d'optimiser ces échantillons en éliminant le passage linéaire dans les listes (si vous jouez en optimisation, si complet)
- Toute commodité supplémentaire afin de distribuer des classes dans différents dossiers et d'utiliser les développements existants dans le code pour réduire son nombre
Nous avons cette structure 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>
Et un grand modèle T4 effrayant pour l'analyser et générer des classes variées mais indispensables:
- Un type "mis en cache" avec les mêmes champs que BO mais non modifiables
- Implémentation d'un cache pour un type avec des méthodes de sélection de filtre
- Un registre de caches pour s'abonner aux mécanismes Nhibernate pour les notifications et l'enregistrement dans DI (Spring dans notre cas)
- Interfaces pour tout cela pour cacher l'intérieur et la possibilité, si nécessaire, de remplacer le code généré par manuscrit et inversé.
- Comme un bonus imprévu agréable depuis le début, si le travail avec les principales entités était déjà prêt - j'ai fait un demi-pas vers la chance et ajouté à la possibilité de générer des types BO et des mappages simples pour donner aux collègues la possibilité d'ajouter de nouvelles classes avec un demi-coup de pied.
D'un point de vue logique, le modèle lui-même se compose de deux parties: l'analyse du XML d'origine en classes qui décrivent la structure de classe souhaitée (pour réduire le risque de tout comportement implicite, dans ce cas, il a été décidé de le faire par une analyse explicite des balises et non par un mappage attributs.
Classes structurelles 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); }
Le code du modèle résultant <#@ 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); } #>
Et où, sans une certaine quantité de code générique difficile à souscrire pour apporter des modifications à la base de données, mettre à jour les caches à la demande et autres plaisirs de la vie Ici, le générique s'est avéré suffisant pour fermer tous les cas d'utilisation, couplé à ceux déjà utilisés avec
Code commun pour accéder aux données 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; } }); } }
Intéressant, à mon avis, il s'est avéré être une classe qui surveille la cohérence du cache et gère sa mise à jour, le principal endroit où je devais penser à implémenter des verrous pour que tout soit optimal et minimalement bloquant, mais protégé
En fait la mise en œuvre [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);
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; } }
, :
- . , , , . , —
- , , ( , , , ). ,
- «» ( ) , ,