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:Many);
  • Се креира претплата за промени во својствата на објектот (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);

Со овој повик, објектот Родителски модел се додава и во локалниот кеш и во редот за запишување во базата на податоци. Затоа, оваа операција зема O(1), и со овој објект може да се работи веднаш.

На пример, за да го пронајдете овој објект во складиштето и да потврдите дека вратениот објект е истиот пример:

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

Што се случува? Поставете () се враќа Табеларен речник, која содржи Истовремен речник и обезбедува дополнителна функционалност на примарните и секундарните индекси. Ова ви овозможува да имате методи за пребарување по ID (или други произволни кориснички индекси) без целосно повторување на сите објекти.

При додавање објекти на ObjectRepository се додава претплата за промена на нивните својства, така што секоја промена во својствата исто така резултира со додавање на овој објект во редот за запишување. 
Ажурирањето на својствата однадвор изгледа исто како и работата со објект POCO:

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

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

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

Ова исто така го додава објектот во редот за бришење.

Како функционира штедењето?

ObjectRepository кога набљудуваните објекти се менуваат (или додаваат или бришат, или менуваат својства), покренува настан Променет моделпретплатени на Складирање. Имплементации Складирање кога ќе се случи некој настан Променет модел промените се ставаат во 3 редици - за додавање, за ажурирање и за бришење.

Исто така имплементации Складирање по иницијализацијата, тие создаваат тајмер што предизвикува промените да се зачувуваат на секои 5 секунди. 

Покрај тоа, постои API за присилување на повик за зачувување: ObjectRepository.Save().

Пред секое зачувување, бесмислените операции прво се отстрануваат од редиците (на пример, дупликат настани - кога објектот е двапати променет или брзо додадени/отстранети објекти), а дури потоа самото зачувување. 

Во сите случаи, целиот тековен објект е зачуван, така што е можно објектите да се зачувуваат по различен редослед отколку што биле променети, вклучително и понови верзии на објекти отколку во времето кога биле додадени во редот.

Што има друго?

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

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

Кој го користи?

Лично, почнав да го користам овој пристап во сите хоби проекти затоа што е погодно и не бара големи трошоци за пишување слој за пристап до податоци или за распоредување тешка инфраструктура. Лично, чувањето податоци во litedb или датотека обично ми е доволно. 

Но, во минатото, кога сега веќе непостоечкиот стартап EscapeTeams (Мислев еве ги парите - но не, искуство повторно) - се користи за складирање податоци во складирање на табели Azure.

Планови за иднината

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

Од техничка гледна точка, ја гледам следнава шема како можна:

  • Чувајте ги EventLog и Snapshot наместо модел на објект
  • Најдете други примероци (додадете крајни точки на сите примероци на поставките? udp discovery? master/slave?)
  • Реплицирајте помеѓу примерите на EventLog преку кој било алгоритам за консензус, како што е RAFT.

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

Извор

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

Извор: www.habr.com

Додадете коментар