ObjectRepository - .NET модел на хранилище в паметта за вашите домашни проекти

Защо да съхранявате всички данни в паметта?

За да съхраняват уебсайт или бекенд данни, първото желание на повечето здрави хора ще бъде да изберат SQL база данни. 

Но понякога на ум идва мисълта, че моделът на данни не е подходящ за SQL: например, когато изграждате търсене или социална графика, трябва да търсите сложни връзки между обекти. 

Най-лошата ситуация е, когато работите в екип и колегата не знае как да изгражда бързи заявки. Колко време отделихте за решаване на N+1 задачи и изграждане на допълнителни индекси, така че SELECT на главната страница да завърши за разумен период от време?

Друг популярен подход е NoSQL. Преди няколко години имаше много шум около тази тема - за всеки удобен случай те внедриха MongoDB и бяха доволни от отговорите под формата на json документи (между другото, колко патерици трябваше да поставите заради кръговите връзки в документите?).

Предлагам да опитате друг, алтернативен метод - защо не опитате да съхраните всички данни в паметта на приложението, като периодично ги записвате в произволно хранилище (файл, отдалечена база данни)? 

Паметта стана евтина и всички възможни данни за повечето малки и средни проекти ще се поберат в 1 GB памет. (Например любимият ми проект за дома е финансов тракер, който поддържа ежедневна статистика и история на моите разходи, баланси и транзакции за година и половина, консумира само 45 MB памет.)

плюсове:

  • Достъпът до данни става по-лесен - не е нужно да се притеснявате за заявки, отложено зареждане, ORM функции, работите с обикновени C# обекти;
  • Няма проблеми, свързани с достъпа от различни нишки;
  • Много бързо - без мрежови заявки, без превод на код на език за заявки, без нужда от (де)сериализация на обекти;
  • Приемливо е да се съхраняват данни във всякаква форма - било то в XML на диск, или в SQL Server, или в Azure Table Storage.

против:

  • Хоризонталното мащабиране се губи и в резултат на това не може да се извърши внедряване с нулево време на престой;
  • Ако приложението се срине, можете частично да загубите данни. (Но нашето приложение никога не се срива, нали?)

Как действа тя?

Алгоритъмът е следният:

  • В началото се установява връзка със съхранението на данни и данните се зареждат;
  • Изграждат се обектен модел, първични индекси и релационни индекси (1:1, 1:Много);
  • Създава се абонамент за промени в свойствата на обекта (INotifyPropertyChanged) и за добавяне или премахване на елементи към колекцията (INotifyCollectionChanged);
  • При задействане на абонамента променения обект се добавя към опашката за запис в хранилището на данни;
  • Промените в хранилището се записват периодично (на таймер) във фонова нишка;
  • Когато излезете от приложението, промените се записват и в хранилището.

Примерен код

Добавяне на необходимите зависимости

// Основная библиотека
Install-Package OutCode.EscapeTeams.ObjectRepository
    
// Хранилище данных, в котором будут сохраняться изменения
// Используйте то, которым будете пользоваться.
Install-Package OutCode.EscapeTeams.ObjectRepository.File
Install-Package OutCode.EscapeTeams.ObjectRepository.LiteDb
Install-Package OutCode.EscapeTeams.ObjectRepository.AzureTableStorage
    
// Опционально - если нужно хранить модель данных для Hangfire
// Install-Package OutCode.EscapeTeams.ObjectRepository.Hangfire

Описваме модела на данните, който ще се съхранява в хранилището

public class ParentEntity : BaseEntity
{
    public ParentEntity(Guid id) => Id = id;
}
    
public class ChildEntity : BaseEntity
{
    public ChildEntity(Guid id) => Id = id;
    public Guid ParentId { get; set; }
    public string Value { get; set; }
}

Тогава обектният модел:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Пример связи 1:Many
    public IEnumerable<ChildModel> Children => Multiple<ChildModel>(x => x.ParentId);
    
    protected override BaseEntity Entity { get; }
}
    
public class ChildModel : ModelBase
{
    private ChildEntity _childEntity;
    
    public ChildModel(ChildEntity entity)
    {
        _childEntity = entity;
    }
    
    public ChildModel() 
    {
        _childEntity = new ChildEntity(Guid.NewGuid());
    }
    
    public Guid ParentId
    {
        get => _childEntity.ParentId;
        set => UpdateProperty(() => _childEntity.ParentId, value);
    }
    
    public string Value
    {
        get => _childEntity.Value;
        set => UpdateProperty(() => _childEntity.Value, value
    }
    
    // Доступ с поиском по индексу
    public ParentModel Parent => Single<ParentModel>(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

И накрая, самия клас хранилище за достъп до данни:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Для тестов, позволяет не сохранять изменения в базу
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Если используется Hangfire и необходимо хранить модель данных для Hangfire в ObjectRepository
        // this.RegisterHangfireScheme(); 
    
        Initialize();
    }
}

Създайте екземпляр на ObjectRepository:

var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
    
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();

Ако проектът ще използва HangFire

public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
    services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}

Вмъкване на нов обект:

var newParent = new ParentModel()
repository.Add(newParent);

С това извикване обектът ParentModel се добавя както към локалния кеш, така и към опашката за запис в базата данни. Следователно тази операция отнема O(1) и с този обект може да се работи веднага.

Например, за да намерите този обект в хранилището и да проверите дали върнатият обект е същия екземпляр:

var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));

Какво се случва? Комплект () се завръща TableDictionary, което съдържа ConcurrentDictionary и осигурява допълнителна функционалност на първични и вторични индекси. Това ви позволява да имате методи за търсене по Id (или други произволни потребителски индекси), без да итерирате изцяло всички обекти.

При добавяне на обекти към ObjectRepository добавя се абонамент за промяна на техните свойства, така че всяка промяна в свойствата също води до добавяне на този обект към опашката за запис. 
Актуализирането на свойства отвън изглежда същото като работа с POCO обект:

myParent.Children.First().Property = "Updated value";

Можете да изтриете обект по следните начини:

repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());

Това също добавя обекта към опашката за изтриване.

Как работи спестяването?

ObjectRepository когато наблюдаваните обекти се променят (или добавяне, или изтриване, или промяна на свойства), предизвиква събитие Моделът е промененабониран за IStorage. Реализации IStorage когато се случи събитие Моделът е променен промените се поставят в 3 опашки - за добавяне, за актуализиране и за изтриване.

Също така реализации IStorage при инициализация те създават таймер, който кара промените да се записват на всеки 5 секунди. 

Освен това има API за принудително извикване за запазване: ObjectRepository.Save().

Преди всяко записване първо се премахват безсмислените операции от опашките (например дублиращи се събития - когато даден обект е променен два пъти или бързо добавени/премахнати обекти), и едва след това самото запазване. 

Във всички случаи се записва целият текущ обект, така че е възможно обектите да бъдат записани в различен ред, отколкото са били променени, включително по-нови версии на обекти от момента, в който са били добавени към опашката.

Какво друго има там?

  • Всички библиотеки са базирани на .NET Standard 2.0. Може да се използва във всеки съвременен .NET проект.
  • API е безопасен за нишки. Вътрешните колекции се изпълняват въз основа на ConcurrentDictionary, манипулаторите на събития или имат ключалки, или не се нуждаят от тях. 
    Единственото нещо, което си струва да запомните, е да се обадите ObjectRepository.Save();
  • Произволни индекси (изискват уникалност):

repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");

Кой го използва?

Лично аз започнах да използвам този подход във всички хоби проекти, защото е удобен и не изисква големи разходи за писане на слой за достъп до данни или разполагане на тежка инфраструктура. Лично за мен обикновено е достатъчно съхраняването на данни в litedb или файл. 

Но в миналото, когато вече несъществуващата стартираща компания EscapeTeams (Мислех, че ето ги парите - но не, пак опит) - използва се за съхраняване на данни в Azure Table Storage.

Планове за бъдещето

Бих искал да коригирам един от основните недостатъци на този подход - хоризонталното мащабиране. За да направите това, имате нужда или от разпределени транзакции (sic!), или да вземете волево решение, че едни и същи данни от различни инстанции не трябва да се променят, или да ги оставите да се променят според принципа „който е последен, е прав“.

От техническа гледна точка виждам следната схема като възможна:

  • Съхранявайте EventLog и Snapshot вместо обектен модел
  • Намерете други екземпляри (добавете крайни точки на всички екземпляри към настройките? udp откриване? главен/подчинен?)
  • Репликирайте между екземплярите на EventLog чрез всеки консенсусен алгоритъм, като например RAFT.

Има и друг проблем, който ме тревожи - каскадно изтриване, или откриване на случаи на изтриване на обекти, които имат връзки от други обекти. 

Изходен код

Ако сте прочели целия път до тук, тогава всичко, което остава, е да прочетете кода; той може да бъде намерен в GitHub:
https://github.com/DiverOfDark/ObjectRepository

Източник: www.habr.com

Добавяне на нов коментар