ObjectRepository - үй жобалары үшін жадтағы .NET репозиторий үлгісі

Неліктен барлық деректерді жадта сақтау керек?

Веб-сайтты немесе серверлік деректерді сақтау үшін көптеген ақыл-ойы бар адамдардың бірінші қалауы SQL дерекқорын таңдау болады. 

Бірақ кейде деректер моделі SQL үшін қолайлы емес деген ой келеді: мысалы, іздеу немесе әлеуметтік графикті құру кезінде объектілер арасындағы күрделі қатынастарды іздеу керек. 

Ең нашар жағдай - командада жұмыс істеу және әріптесіңіз жылдам сұрауларды қалай құру керектігін білмейді. Негізгі беттегі ТАҢДАУ қолайлы уақыт ішінде аяқталуы үшін N+1 есептерін шешуге және қосымша индекстер құруға қанша уақыт жұмсадыңыз?

Тағы бір танымал әдіс - NoSQL. Бірнеше жыл бұрын бұл тақырып төңірегінде көп шу болды - кез келген ыңғайлы жағдайда олар MongoDB-ті орналастырды және json құжаттары түріндегі жауаптарға риза болды. (айтпақшы, құжаттардағы дөңгелек сілтемелерге байланысты сізге қанша балдақ салу керек болды?).

Мен басқа, балама әдісті қолданып көруді ұсынамын - неге барлық деректерді қолданба жадында сақтауға, оны кездейсоқ жадқа (файл, қашықтағы дерекқор) мерзімді түрде сақтауға тырыспасқа? 

Жад арзан болды және көптеген шағын және орта жобалар үшін кез келген мүмкін деректер 1 ГБ жадқа сыйды. (Мысалы, менің сүйікті үй жобасы қаржылық трекер, ол бір жарым жыл бойы менің шығындарымның, баланстарымның және транзакцияларымның күнделікті статистикасы мен тарихын сақтайды, небәрі 45 МБ жадты жұмсайды.)

Артықшылықтары:

  • Деректерге қол жеткізу оңайырақ - сұраулар, жалқау жүктеу, ORM мүмкіндіктері туралы алаңдамаудың қажеті жоқ, сіз қарапайым C# нысандарымен жұмыс жасайсыз;
  • Әртүрлі ағындардан қол жеткізуге байланысты проблемалар жоқ;
  • Өте жылдам – желілік сұраулар жоқ, кодты сұрау тіліне аудару жоқ, объектілерді сериялау (де) қажет емес;
  • Деректерді кез келген пішінде сақтауға болады - ол дискідегі XML немесе SQL серверінде немесе Azure кесте қоймасында болсын.

Кемшіліктері:

  • Көлденең масштабтау жоғалады және нәтижесінде нөлдік тоқтау уақытын орналастыру мүмкін емес;
  • Қолданба бұзылса, деректерді ішінара жоғалтуыңыз мүмкін. (Бірақ біздің қолданба ешқашан бұзылмайды, солай ма?)

Бұл қалай жұмыс істейді?

Алгоритм келесідей:

  • Бастапқыда деректерді сақтау орнымен байланыс орнатылып, деректер жүктеледі;
  • Нысан үлгісі, бастапқы индекстер және реляциялық индекстер (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));

Бұл жағдайда не болады? Орнату () қайтарады Кесте сөздігі, құрамында ConcurrentDictionary және бастапқы және қосымша индекстердің қосымша функционалдығын қамтамасыз етеді. Бұл барлық нысандарды толығымен қайталамай, Id (немесе басқа ерікті пайдаланушы индекстері) бойынша іздеу әдістерін алуға мүмкіндік береді.

Объектілерді қосқанда ObjectRepository жазылу олардың сипаттарын өзгерту үшін қосылады, сондықтан сипаттардағы кез келген өзгерту де осы нысанның жазу кезегіне қосылуына әкеледі. 
Сипаттарды сырттан жаңарту POCO нысанымен жұмыс істеу сияқты көрінеді:

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

Объектіні келесі жолдармен жоюға болады:

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

Бұл сонымен қатар нысанды жою кезегіне қосады.

Сақтау қалай жұмыс істейді?

ObjectRepository бақыланатын нысандар өзгергенде (қосу немесе жою немесе сипаттарды өзгерту) оқиғаны тудырады Үлгі өзгертілдіжазылды ISсақтау. Іске асыру ISсақтау оқиға болған кезде Үлгі өзгертілді өзгертулер 3 кезекке қойылады - қосу, жаңарту және жою үшін.

Сондай-ақ іске асыру ISсақтау инициализациялау кезінде олар өзгерістерді әрбір 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 табу? негізгі/жұмдық?)
  • RAFT сияқты кез келген консенсус алгоритмі арқылы EventLog даналары арасында қайталаңыз.

Мені алаңдататын тағы бір мәселе бар - каскадты жою немесе басқа объектілерден сілтемелері бар объектілерді жою жағдайларын анықтау. 

Бастапқы код

Егер сіз осы жерге дейін оқысаңыз, кодты оқу ғана қалады; оны GitHub сайтынан табуға болады:
https://github.com/DiverOfDark/ObjectRepository

Ақпарат көзі: www.habr.com

пікір қалдыру