Clear LINQ To SQL Cache
context.GetType().InvokeMember( "ClearCache", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, context, null);
Copyright 2012 Shailen Sukul
seen from United States
seen from United States
seen from United States
seen from United States
seen from Australia
seen from Hong Kong SAR China

seen from Russia

seen from South Korea
seen from Germany
seen from United States

seen from Poland
seen from Germany

seen from Malaysia

seen from Malaysia
seen from United States
seen from China
seen from United States
seen from Poland
seen from Poland

seen from China
Clear LINQ To SQL Cache
context.GetType().InvokeMember( "ClearCache", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, context, null);
Copyright 2012 Shailen Sukul

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
Слой кэширования поверх Linq to SQL. Версия 1.0.
Введение было в предыдущей статье
Для того, чтобы полноценно использовать в рамках текущего DataContext’а сущности загруженные в другом DataContext’е, их необходимо клонировать и зааттачить. При аттаче сущность добавляется в IdentityMap текущего DataContext’а. Зааттачить одну и ту же сущность к двум разным DataContext’ам нельзя (поэтому каждый клон может использоваться только в одном DataContext’е, то есть по факту в одной бизнес транзакции). MemberwiseClone для клонирования использовать нельзя, так как при этом в клоне будут ссылки на сущности зааттаченные к DataContext’у, в котором сущность была загружена, а не к текущем DataContext’у.
В итоге для того, чтобы можно было использовать сущности из статического кэша, был написан следующий код, использующий Linq MetaModel и немного Reflection’а:
public TEntity CloneAndAttach(TEntity sourceEntity) { if (sourceEntity == null) throw new ArgumentNullException("sourceEntity"); var entityType = sourceEntity.GetType(); var entityMetaType = GetClonningMetaType(entityType); var entityKey = entityMetaType.GetKey(sourceEntity); // Проверка, не позволяющая клонировать одну и ту же сущность более одного раза. // Кроме прочего помогает разобраться с циклическими ссылками. if (attachedEntities.ContainsKey(entityKey)) { return attachedEntities[entityKey]; } var clonedObject = Activator.CreateInstance(entityType); var clonedEntity = (TEntity)clonedObject; attachedEntities.Add(entityKey, clonedEntity); // клонируем поля foreach (var dataMember in entityMetaType.Fields) { var value = dataMember.StorageAccessor.GetBoxedValue(sourceEntity); dataMember.StorageAccessor.SetBoxedValue(ref clonedObject, value); } // клонируем EntityRef'ы foreach (var dataMember in entityMetaType.EntityRefs) { var thisKeyValues = dataMember.Association.ThisKey .Select(keyDataMember => keyDataMember.StorageAccessor.GetBoxedValue(sourceEntity)) .ToArray(); if (thisKeyValues.All(keyValue => keyValue == null)) { continue; } var repository = Repositories.GetRepositoryCheckingBaseTypeFor(dataMember.Type); var value = repository.CloneAndAttach(dataMember.MemberAccessor.GetBoxedValue(sourceEntity)); dataMember.MemberAccessor.SetBoxedValue(ref clonedObject, value); } // клонируем EntitySet'ы foreach (var dataMember in entityMetaType.EntitySets) { var repository = Repositories .GetRepositoryCheckingBaseTypeFor(dataMember.Type.GenericTypeArguments[0]); var sourceEntitySet = (IList)dataMember.MemberAccessor.GetBoxedValue(sourceEntity); var clonedEntitySet = (IList)Activator.CreateInstance(dataMember.Type); foreach (var sourceItem in sourceEntitySet) { var clonedItem = repository.CloneAndAttach(sourceItem); clonedEntitySet.Add(clonedItem); } dataMember.MemberAccessor.SetBoxedValue(ref clonedObject, clonedEntitySet); } table.Attach(clonedEntity); return clonedEntity; } private ClonningMetaType GetClonningMetaType(Type type) { ClonningMetaType result; // Информация о клонируемом типе кэшируется в статическом ConcurrentDictionary, чтобы не юзать Reflection каждый раз if (!clonningMetaTypes.TryGetValue(type, out result)) { result = clonningMetaTypes.GetOrAdd(type, key => new ClonningMetaType(MetaModel.GetMetaType(key))); } return result; } private class ClonningMetaType { private readonly MetaType metaType; private readonly IReadOnlyCollection keys; public ClonningMetaType(MetaType metaType) { this.metaType = metaType; keys = metaType.DataMembers .Where(dataMember => dataMember.IsPrimaryKey) .ToArray(); Fields = metaType.DataMembers .Where(dataMember => dataMember.IsPersistent) .Where(dataMember => !dataMember.IsAssociation) .ToArray(); EntityRefs = metaType.DataMembers .Where(dataMember => dataMember.IsPersistent) .Where(dataMember => dataMember.IsAssociation) .Where(dataMember => !dataMember.Association.IsMany) .Where(dataMember => !dataMember.Member.HasCustomAttribute()) .ToArray(); EntitySets = metaType.DataMembers .Where(dataMember => dataMember.IsPersistent) .Where(dataMember => dataMember.IsAssociation) .Where(dataMember => dataMember.Association.IsMany) .Where(dataMember => !dataMember.Member.HasCustomAttribute()) .ToArray(); } public IReadOnlyCollection Fields { get; private set; } public IReadOnlyCollection EntityRefs { get; private set; } public IReadOnlyCollection EntitySets { get; private set; } public ItcEntityKey GetKey(object entity) { return new ItcEntityKey( metaType, keys.Select(dataMember => dataMember.StorageAccessor.GetBoxedValue(entity)).ToArray()); } }
Для того, чтобы при клонировании проходясь по EntityRef’ам и EntitySet’ам не выгрузить половину БД, я помечаю часть EntityRef’ов и EntitySet’ов с помощью специального атрибута NonCacheableAssociationAttribute. Я больше склонялся к тому, чтобы наоборот помечать особым атрибутом CacheableAssociationAttribute те ассоциации, которые надо клонировать, но в итоге коллеги меня уговорили на текущий вариант, та как так меньше полей придется помечать, да и ошибки, при которых лишнее выгружается, легче заметить.
Так как кэш загружается в отдельном DataContext’е и клонирование происходит уже после его Dispose, мне также пришлось написать метод который подгружает все кэшируемые ассоциации у загружаемой сущности:
public void LoadEntityForClone(TEntity entity) { // Проверка, устраняющая проблемы с циклическими ссылками if (loadedEntities.Contains(entity)) { return; } loadedEntities.Add(entity); var entityType = entity.GetType(); var entityMetaType = GetClonningMetaType(entityType); // Загружаем все EntityRef'ы foreach (var dataMember in entityMetaType.EntityRefs) { var repository = Repositories.GetRepositoryCheckingBaseTypeFor(dataMember.Type); var value = dataMember.MemberAccessor.GetBoxedValue(entity); if (value != null) { repository.LoadEntityForClone(value); } } // Загружаем все EntitySet'ы foreach (var dataMember in entityMetaType.EntitySets) { var repository = Repositories .GetRepositoryCheckingBaseTypeFor(dataMember.Type.GenericTypeArguments[0]); var entitySet = (IList)dataMember.MemberAccessor.GetBoxedValue(entity); foreach (var item in entitySet) { repository.LoadEntityForClone(item); } } }
Конечно же, если использовать DataLoadOptions, то понадобится меньше запросов для загрузки EntityRef’ов. Но я даже не пытался заморачиваться с динамическим построением DataLoadOptions, потому что в Linq to SQL есть бага, из-за которой при использовании DataLoadOptions для загрузки EntitySet’ов некорректно загружается тип сущности (дискриминатор типа игнорируется и вместо дочернего типа загружается родительский).
Так как доступ к записям в БД у нас происходит через репозитории, обращения к кэшу я также вынес туда. При попытке получить кампанию или элемент кампании происходит примерно следующее:
Проверяем, была ли кампания уже добавлена в текущий DataContext из кэша. Если да то возращаем объект, который уже был склонирован и зааттачен.
Смотрим есть ли кампания в кэше. Если нет, загружаем. Если есть, сравниваем дату кампании в БД с датой кампании в кэше. Если не равны, перезагружаем кампанию.
Клонируем и аттачим кампанию и все ее элементы к текущему DataContext’у
При таком подходе мы аттачим только те кампании, которые нам нужны и проверяем актуальность кэша (дату изменения камании) только один раз за транзакцию (один раз за время жизни DataContext’а). Так как в репозиториях есть множество методов для получения элементов кампаний (по Id, по кампании, по уникальному строковому идентифкатору) струкутра данных в кэша получилась довольно таки сложной, но с этим можно было жить.
Новая архитектура кэширования прошла все интеграционные тесты и была успешно выложена на Production на одном из проектов. Все стабильно работало и ничто не предвещало беды. Но потом что-то пошло не так (об этом в следующей статье).
Beginning LINQ to SQL in VB.NET
Content will be available soon.
LINQ to SQL Connection Strings with class library projects
By default, creating a new LINQ to SQL model (.dbml) will put the connection strings in both the application settings file and also web.config / app.config. This is not so much of a problem for web projects, but what about class library projects? Personally I would rather just define my connection string once, in web.config.
To achieve this, first you need to change your Data Context so that the connection string is not stored in an Application Settings file. Open up your DBML and click the designer service. Expand the Connection property and set "Application Settings" to False.
Doing this hard codes your connection string in the default constructor of your DataContext. If you open up <yourdatacontext>.designer.cs you will see something like:
public AdventureWorksDataContext() : base("Your connection string", mappingSource) { OnCreated(); }
To retrieve the connection string from web.config you could simply change this constructor to:
public AdventureWorksDataContext() : base(ConfigurationManager.ConnectionStrings["Linq2SqlConnectionString"].ConnectionString, mappingSource) { OnCreated(); }
Or better yet, create a new partial class that extends your data context. To do this you will need to remove the default constructor from the generated designer code and add this to your new partial class.
Whilst both methods will work (as far as grabbing the connection string from web.config), as soon as you make any changes to your DBML through the designer, your Data Context constructor will be reset and you will likely get compilation or runtime errors.
My preferred solution is to create a custom Data Context class that inherits from the generated Data Context:
public class DB : AdventureWorksDataContext { public DB() : base(ConfigurationManager.ConnectionStrings["Linq2SqlConnectionString"].ConnectionString) { } }
Then I just work with my custom Data Context in my data access layer
public IQueryable<Customer> Customers() { DB _db = new DB(); return _db.Customers; }
I can also change my Data Context as much as I like without breaking anything.
One thing worth noting is that even with the above method your connection string is still present in the default constructor of the generated data context class. If you are selling your assembly or perhaps including it in a public project, you may want to change this.