ObjectRepository — një model depozite në memorie .NET për projektet tuaja në shtëpi.

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

Për ruajtjen e të dhënave të faqes ose backend-it, dëshira e parë e shumicës së njerëzve me mendje të shëndoshë do të jetë të zgjidhin një bazë të dhënash SQL. 

Por ndonjëherë në mendje vjen ideja që modeli i të dhënave nuk i përshtatet SQL: për shembull, kur ndërtosh kërkimin ose grafikën sociale, nevojitet kërkimi mbi lidhjet komplekse midis objekteve. 

Situata më e keqe është kur punoni në një ekip dhe një koleg nuk di të ndërtojë kërkesa të shpejta. Sa kohë keni harxhuar për të zgjidhur problemet N+1 dhe për ndërtimin e indekseve të tjera, që SELECT në faqen kryesore të punojë brenda një kohe të arsyeshme?

Një qasje tjetër e njohur është NoSQL. Para disa vitesh kishte një bum të madh rreth kësaj teme — për çdo rast të përshtatshëm, u instalonte MongoDB dhe gëzoheshin me përgjigjet në formën e dokumenteve json (p.s. sa shumë ndihma iu desh të vendosni për shkak të lidhjeve ciklike në dokumente?).

Unë sugjeroj të provoni një mënyrë tjetër, alternative — pse të mos provoni të ruani të gjitha të dhënat në memorie të aplikacionit, duke i ruajtur periudhësisht në një depo të rastësishme (skedari, bazë të dhënash të largët)? 

Memoria është bërë e lirë, dhe çdo të dhënë e mundshme për shumicën e projekteve të vogla dhe të mesme do të hyjë në 1 GB memorie. (Për shembull, projekti im i preferuar në shtëpi — ndjekësi financiar, i cili mban statistikat dhe historinë e shpenzimeve, balancave dhe transaksioneve të mia për një vit e gjysmë, konsumon vetëm 45 MB memorie.)

Avantazhet:

  • Qakses në të dhëna bëhet më e lehtë — nuk nevojitet të merremi me kërkesat, ngarkimin e ngadalshëm, veçoritë e ORM-së, puna bëhet me objekte të zakonshme C#;
  • Nuk ka probleme të lidhura me qasjen nga fibra të ndryshme;
  • Shumë shpejt — nuk ka kërkesa në rrjet, nuk ka translacion të kodit në gjuhën e kërkesave, nuk është e nevojshme të (de)serializoni objektet;
  • Pranohet të ruani të dhënat në çdo format — qoftë në XML në disku, qoftë në SQL Server, qoftë në Azure Table Storage.

Disavantazhet:

  • Humbet shkallëzimi horizontal dhe si pasojë nuk mund të realizoni një rikthim zero në ndjekje;
  • Nëse aplikacioni dështon — mund të humbni pjesërisht të dhënat. (Por aplikacioni ynë kurrë nuk dështon, apo jo?)

Si funksionon kjo?

Algoritmi është si më poshtë:

  • Fillimisht krijohet lidhja me depot e të dhënave, dhe ndodh ngarkimi i të dhënave;
  • Ndërtohet modeli objektor, indekset primare dhe indekset e lidhjeve (1:1, 1:M many).
  • Krijohet një abonim për ndryshimet e pronave të objekteve (INotifyPropertyChanged) dhe për shtimin ose heqjen e elementeve në koleksion (INotifyCollectionChanged);
  • Kur ndodh abonimi — objekti i ndryshuar shtohet në radhën për regjistrim në magazinën e të dhënave;
  • Periodikisht (në një timer) në procesin e prapambetjes regjistrohen ndryshimet në magazinë;
  • Edhe kur del nga aplikacioni regjistrohen ndryshimet në magazinë.

Shembulli i kodit

Shtojmë varësitë e 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

Përshkruajmë modelin e të dhënave që do të ruhet në magazinë

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 objektiv:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Shembulli i lidhjes 1:M
    public IEnumerable Children => Multiple(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
    }
    
    // Qasje me kërkesë sipas indeksit
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Dhe përfundimisht klasa-repozita për qasje në të dhëna:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Për teste, lejon që të mos ruajë ndryshimet në bazë
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Nëse përdoret Hangfire dhe duhet të mbahen modelet e të dhënave për Hangfire në ObjectRepository
        // this.RegisterHangfireScheme();  
    
        Initialize();
    }
}

Krijojmë një instancë të 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));
}

Shtimi i një objekti të ri:

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

Me kjo thirrje objekti ParentModel shtohet si në cache lokal ashtu edhe në radhën për shkrim në bazë. Kështu që kjo operacion zë O(1), dhe mund të punoni menjëherë me këtë objekt.

Për shembull, për të gjetur këtë objekt në depo dhe për t'u siguruar që objekti i kthyer është po ai ekzemplar:

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

Çfarë ndodh gjatë kësaj? Set() false, nëse elementi që do të zëvendësohet është TableDictionary, e cila përmban brenda saj ConcurrentDictionary dhe ofron funksionalitete shtesë për indekset primare dhe sekondare. Kjo lejon që të kemi metoda për kërkimin sipas Id (ose indekseve të tjera të rastit) pa kërkime të plota mbi të gjithë objektet.

Kur shtohen objektet në ObjectRepository shtohet një abonim për ndryshimin e pronave të tyre, prandaj çdo ndryshim i pronave gjithashtu çon në shtimin e këtij objekti në radhën për shkrim. 
Përditësimi i pronave nga jashtë duket si puna me një objekt POCO:

myParent.Children.First().Property = "Vlera e përditësuar";

Objekti mund të fshihet në mënyra të ndryshme:

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

Gjatë kësaj ndodh gjithashtu shtimi i objektit në radhën për fshirje.

Si funksionon ruajtja?

ObjectRepository ndryshimi i objekteve të ndjekura (si shtimi apo fshirja, ashtu edhe ndryshimi i pronave) thërret një ngjarje ModelChanged, në të cilën është regjistruar IStorage. Zbatimet IStorage kur ndodh ngjarja ModelChanged grupojnë ndryshimet në 3 radhë - për shtim, për përditësim, dhe për fshirje.

Po ashtu, zbatimet IStorage në fillim krijojnë një timer, i cili çdo 5 sekonda thërret ruajtjen e ndryshimeve. 

Për më tepër, ka një API për thirrjen e detyruar të ruajtjes: ObjectRepository.Save().

Para çdo ruajtjeje, së pari ndodh fshirja nga radhët e operacioneve të pavlera (për shembull, dublikatet e ngjarjeve - kur objekti është ndërruar dy herë ose shtimi/fshirja e shpejtë e objekteve), dhe vetëm pastaj ruajtja vetë. 

Në të gjitha rastet ruhet objekti aktual në tërësi, prandaj është e mundur që objektet të ruhen në një rend tjetër nga sa u ndërruan, duke përfshirë ruajtjen e versioneve më të reja të objekteve, sesa në momentin e shtimit në radhë.

Çfarë tjetër ka?

  • Të gjitha bibliotekat janë të bazuara në .NET Standard 2.0. Mund të përdoren në çdo projekt modern .NET.
  • API është i sigurt për përdorim në mënyrë të përbashkët. Koleksionet e brendshme janë zbatuar mbi ConcurrentDictionary, handlerat e ngjarjeve kanë gjithashtu bllokime ose nuk kanë nevojë për to. 
    E vetmja gjë që duhet të mbani mend është të thërrisni ObjectRepository.Save();
  • Indekset arbitrare (kërkojnë unikësinë):

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

Kush e përdor këtë?

Personalish, kam filluar ta përdor këtë qasje në të gjitha projektet e mia si hobi, sepse është e përshtatshme dhe nuk kërkon shumë investime në zhvillimin e një Leveli të Qasjes në të Dhëna apo krijimin e një infrastrukture të rëndë. Më përgjithësi, mjafton për mua ruajtja e të dhënave në litedb ose në një skedar. 

Por në të kaluarën, kur me ekipin bëjmë start-up-in tani të ndalur EscapeTeams (mendoja se këtu janë, paratë – por jo, përsëri përvojë) – përdornim Azure Table Storage për ruajtjen e të dhënave.

Planet për të ardhmen

Dëshirojmë të rregullojmë një nga disavantazhet kryesore të këtij qasje — shkallëzimi horizontal. Për këtë kërkohen ose transaksione të shpërndara (sic!), ose duhet të merret një vendim i fortë që të dhënat e njëjta nga instancat e ndryshme nuk duhet të ndryshojnë, ose le të ndryshojnë sipas parimit "ai që është i fundit — ai ka të drejtë".

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

  • Të ruajmë në vend të modelit objektiv EventLog dhe Snapshot
  • Të gjejmë instanca të tjera (të shtojmë në cilësimet e pikave të fundit të të gjitha instancave? zbulimi UDP? master/slave?)
  • Të riplikojmë midis instancave EventLog përmes ndonjë prej algoritmeve të konsensusit, për shembull RAFT.

Ka gjithashtu një problem tjetër që më shqetëson – është fshirja kaskadë, ose zbulimi i rasteve të fshirjes së objekteve që kanë referenca nga objekte të tjera. 

Kodi burimor

Nëse keni arritur deri këtu – atëherë e ardhmja mbetet vetëm të lexoni kodin, mund ta gjeni në GitHub:
https://github.com/DiverOfDark/ObjectRepository

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster