ObjectRepository - Modeli i depove në memorie .NET për projektet e shtëpisë tuaj

Pse të ruani të gjitha të dhënat në memorie?

Për të ruajtur të dhënat e faqes në internet ose backend, dëshira e parë e shumicës së njerëzve të arsyeshëm do të jetë të zgjedhin një bazë të dhënash SQL. 

Por ndonjëherë vjen në mendje mendimi se modeli i të dhënave nuk është i përshtatshëm për SQL: për shembull, kur ndërtoni një grafik kërkimi ose social, duhet të kërkoni për marrëdhënie komplekse midis objekteve. 

Situata më e keqe është kur punoni në një ekip dhe një koleg nuk di të ndërtojë pyetje të shpejta. Sa kohë keni shpenzuar për zgjidhjen e problemeve N+1 dhe ndërtimin e indekseve shtesë në mënyrë që SELECT në faqen kryesore të përfundojë në një kohë të arsyeshme?

Një tjetër qasje e njohur është NoSQL. Disa vite më parë pati shumë zhurmë rreth kësaj teme - për çdo rast të përshtatshëm ata vendosën MongoDB dhe ishin të kënaqur me përgjigjet në formën e dokumenteve json (meqë ra fjala, sa paterica ju është dashur të fusni për shkak të lidhjeve rrethore në dokumente?).

Unë sugjeroj të provoni një metodë tjetër alternative - pse të mos provoni t'i ruani të gjitha të dhënat në kujtesën e aplikacionit, duke i ruajtur periodikisht në ruajtje të rastësishme (skedar, bazë të dhënash në distancë)? 

Kujtesa është bërë e lirë dhe çdo e dhënë e mundshme për shumicën e projekteve të vogla dhe të mesme do të përshtatet në 1 GB memorie. (Për shembull, projekti im i preferuar i shtëpisë është gjurmues financiar, e cila ruan statistikat dhe historinë ditore të shpenzimeve, bilanceve dhe transaksioneve të mia për një vit e gjysmë, konsumon vetëm 45 MB memorie.)

Pro:

  • Qasja në të dhëna bëhet më e lehtë - nuk keni nevojë të shqetësoheni për pyetjet, ngarkimin dembel, veçoritë ORM, ju punoni me objekte të zakonshme C#;
  • Nuk ka probleme që lidhen me aksesin nga temat e ndryshme;
  • Shumë i shpejtë - pa kërkesa në rrjet, pa përkthim të kodit në një gjuhë pyetëse, nuk ka nevojë për (ç)serializimin e objekteve;
  • Është e pranueshme që të ruhen të dhënat në çdo formë - qoftë në XML në disk, ose në SQL Server, ose në Azure Table Storage.

Cons:

  • Shkallëzimi horizontal humbet dhe si rezultat, vendosja e kohës së ndërprerjes zero nuk mund të bëhet;
  • Nëse aplikacioni rrëzohet, mund të humbni pjesërisht të dhënat. (Por aplikacioni ynë nuk rrëzohet kurrë, apo jo?)

Si funksionon kjo gjë?

Algoritmi është si më poshtë:

  • Në fillim, vendoset një lidhje me ruajtjen e të dhënave dhe ngarkohen të dhënat;
  • Ndërtohet një model objekti, indekse parësore dhe indekse relacionale (1:1, 1:Shumë);
  • Krijohet një abonim për ndryshime në vetitë e objektit (INotifyPropertyChanged) dhe për shtimin ose heqjen e elementeve në koleksion (INotifyCollectionChanged);
  • Kur aktivizohet abonimi, objekti i ndryshuar shtohet në radhë për t'u shkruar në ruajtjen e të dhënave;
  • Ndryshimet në hapësirën ruajtëse ruhen periodikisht (në një kohëmatës) në një thread në sfond;
  • Kur dilni nga aplikacioni, ndryshimet ruhen gjithashtu në memorie.

Shembull kodi

Shtimi i varësive të nevojshme

// Основная библиотека
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

Ne përshkruajmë modelin e të dhënave që do të ruhen në ruajtje

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; }
}

Pastaj modeli i objektit:

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;
}

Dhe së fundi, vetë klasa e depove për aksesimin e të dhënave:

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();
    }
}

Krijo një shembull ObjectRepository:

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

Nëse projekti do të përdorë HangFire

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

Futja e një objekti të ri:

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

Me këtë thirrje, objekti Modeli i prindërve shtohet si në cache lokale ashtu edhe në radhë për të shkruar në bazën e të dhënave. Prandaj, ky operacion merr O(1), dhe me këtë objekt mund të punohet menjëherë.

Për shembull, për të gjetur këtë objekt në depo dhe për të verifikuar që objekti i kthyer është i njëjti shembull:

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

Çfarë po ndodh? Set () kthehet Tabela Fjalor, i cili përmban Fjalori i njëkohshëm dhe ofron funksionalitet shtesë të indekseve parësore dhe dytësore. Kjo ju lejon të keni metoda për kërkimin sipas ID-së (ose indekseve të tjera arbitrare të përdoruesve) pa u përsëritur plotësisht mbi të gjitha objektet.

Kur shtoni objekte në Depoja e Objekteve shtohet një abonim për të ndryshuar vetitë e tyre, kështu që çdo ndryshim në vetitë rezulton gjithashtu që ky objekt të shtohet në radhën e shkrimit. 
Përditësimi i vetive nga jashtë duket i njëjtë si të punosh me një objekt POCO:

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

Ju mund të fshini një objekt në mënyrat e mëposhtme:

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

Kjo gjithashtu shton objektin në radhën e fshirjes.

Si funksionon kursimi?

Depoja e Objekteve kur objektet e monitoruara ndryshojnë (qoftë duke shtuar ose fshirë, ose duke ndryshuar vetitë), ngre një ngjarje Modeli i Ndryshuartë abonuar në IStorage. Zbatimet IStorage kur ndodh një ngjarje Modeli i Ndryshuar ndryshimet vendosen në 3 radhë - për shtim, për përditësim dhe për fshirje.

Gjithashtu zbatimet IStorage pas inicializimit, ata krijojnë një kohëmatës që bën që ndryshimet të ruhen çdo 5 sekonda. 

Përveç kësaj, ekziston një API për të detyruar një thirrje ruajtëse: ObjectRepository.Save().

Përpara çdo ruajtjeje, operacionet e pakuptimta hiqen fillimisht nga radhët (për shembull, ngjarje të kopjuara - kur një objekt ndryshohet dy herë ose objektet u shtuan/hiqeshin shpejt), dhe vetëm më pas vetë ruajtja. 

Në të gjitha rastet, i gjithë objekti aktual ruhet, kështu që është e mundur që objektet të ruhen në një renditje të ndryshme nga ajo që u ndryshuan, duke përfshirë versionet më të reja të objekteve sesa në kohën kur u shtuan në radhë.

Çfarë tjetër ka?

  • Të gjitha bibliotekat bazohen në .NET Standard 2.0. Mund të përdoret në çdo projekt modern .NET.
  • API është i sigurt në lidhje. Koleksionet e brendshme zbatohen në bazë të Fjalori i njëkohshëm, mbajtësit e ngjarjeve ose kanë bravë ose nuk kanë nevojë për to. 
    E vetmja gjë që ia vlen të mbani mend është të telefononi ObjectRepository.Save();
  • Indekset arbitrare (kërkojnë unike):

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

Kush e përdor atë?

Personalisht, fillova ta përdor këtë qasje në të gjitha projektet e hobi, sepse është i përshtatshëm dhe nuk kërkon shpenzime të mëdha për të shkruar një shtresë të aksesit të të dhënave ose për vendosjen e infrastrukturës së rëndë. Personalisht, ruajtja e të dhënave në litedb ose një skedar zakonisht është e mjaftueshme për mua. 

Por në të kaluarën, kur startup-i tashmë i pafuqishëm EscapeTeams (Mendova se këtu janë paratë - por jo, përsëri përvojë) - përdoret për të ruajtur të dhënat në Azure Table Storage.

Planet për të ardhmen

Do të doja të rregulloja një nga disavantazhet kryesore të kësaj qasjeje - shkallëzimin horizontal. Për ta bërë këtë, ju nevojiten ose transaksione të shpërndara (sic!), ose merrni një vendim me dëshirë të fortë që të njëjtat të dhëna nga instanca të ndryshme nuk duhet të ndryshojnë, ose lërini ato të ndryshojnë sipas parimit "kush ka të drejtë i fundit".

Nga pikëpamja teknike, unë e shoh si të mundshme skemën e mëposhtme:

  • Ruani EventLog dhe Snapshot në vend të modelit të objektit
  • Gjeni shembuj të tjerë (shtoni pikat fundore të të gjitha rasteve te cilësimet? zbulim udp? master/skllav?)
  • Repliko midis rasteve të EventLog nëpërmjet çdo algoritmi konsensusi, si p.sh. RAFT.

Ekziston edhe një problem tjetër që më shqetëson - fshirja në kaskadë, ose zbulimi i rasteve të fshirjes së objekteve që kanë lidhje nga objekte të tjera. 

Kodi i burimit

Nëse keni lexuar deri këtu, atëherë gjithçka që mbetet është të lexoni kodin; ai mund të gjendet në GitHub:
https://github.com/DiverOfDark/ObjectRepository

Burimi: www.habr.com

Shto një koment