ObjectRepository este un model de repository .NET în memorie pentru proiectele tale personale

De ce să stocăm toate datele în memorie?

Pentru stocarea datelor site-ului sau backend-ului, prima opțiune pe care o aleg majoritatea oamenilor normali este o bază de date SQL. 

Dar uneori apare gândul că modelul de date nu se potrivește pentru SQL: de exemplu, în construirea căutării sau a unui grafic social, este nevoie de căutări pe relații complexe între obiecte. 

Cea mai neplăcută situație este atunci când lucrați într-o echipă, iar un coleg nu știe să construiască interogări rapide. Cât timp ați petrecut rezolvând problemele N+1 și construind indice suplimentari, astfel încât SELECT-ul de pe pagina principală să ruleze într-un timp rezonabil?

O altă abordare populară este NoSQL. Cu câțiva ani în urmă, a fost un mare hype în jurul acestei teme - pentru orice caz convenabil, s-a instalat MongoDB și s-au bucurat de răspunsurile sub formă de documente json (de altfel, câte soluții temporare a fost nevoie să implementați din cauza legăturilor circulare în documente?).

Vă propun să încercăm încă o metodă alternativă - de ce să nu încercăm să stocăm toate datele în memoria aplicației, salvându-le periodic într-un depozit arbitrar (fișier, bază de date remote)? 

Memoria a devenit ieftină, iar datele posibile ale majorității proiectelor mici și medii se pot încadra în 1 GB de memorie. (De exemplu, proiectul meu preferat de acasă - un tracker financiar, care înregistrează statistici zilnice și istoricul cheltuielilor, soldurilor și tranzacțiilor de un an și jumătate, consumă doar 45 MB de memorie.)

Pro:

  • Accesul la date devine mai simplu - nu trebuie să vă faceți griji cu privire la interogări, încărcare leneșă, particularitățile ORM, iar lucrul se desfășoară cu obiecte C# obișnuite;
  • Nu există probleme legate de accesul din fire diferite;
  • Foarte rapid - nu sunt cereri de rețea, nu există traducerea codului în limbaj de interogare, nu este necesară (de)serializarea obiectelor;
  • Acceptabil să stocăm date în orice format - fie în XML pe disk, fie în SQL Server, fie în Azure Table Storage.

Dezavantaje:

  • Se pierde scalabilitatea orizontală și, ca urmare, nu se poate realiza implementarea fără timp de nefuncționare;
  • Dacă aplicația se oprește - se pot pierde parțial date. (Dar, desigur, aplicația noastră nu va cădea niciodată, nu-i așa?)

Cum funcționează?

Algoritmul este următorul:

  • La început, se stabilește o conexiune cu depozitul de date și se încarcă datele;
  • Se construiește un model de obiecte, indecși primari și indecși de relații (1:1, 1:Many);
  • Se creează o subscripție la modificările proprietăților obiectelor (INotifyPropertyChanged) și la adăugarea sau eliminarea elementelor din colecție (INotifyCollectionChanged);
  • Când subscripția este activată, obiectul modificat este adăugat în coada de scriere în depozitul de date;
  • Periodic (pe bază de temporizator), modificările sunt salvate în fundal în depozit;
  • De asemenea, modificările sunt salvate în depozit la închiderea aplicației.

Exemplu de cod

Adăugăm dependențele necesare

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

Descriem modelul de date care va fi salvat în depozit

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

Apoi, modelul de obiecte:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Exemplu de relație 1:Many
    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);
    }
    
    // Acces cu căutare prin index
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Și, în cele din urmă, clasa-repositoriu pentru accesul la date:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Pentru teste, permite neefectuarea modificărilor în baza de date
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Dacă se folosește Hangfire și este necesară stocarea modelului de date pentru Hangfire în ObjectRepository
        // this.RegisterHangfireScheme();  
    
        Initialize();
    }
}

Creăm o instanță a ObjectRepository:

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

Dacă în proiect va fi folosit HangFire

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

Inserarea unui nou obiect:

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

În acest apel, obiectul ParentModel se adaugă atât în cache-ul local, cât și în coada de scriere în baza de date. Prin urmare, această operațiune durează O(1), iar acest obiect poate fi utilizat imediat.

De exemplu, pentru a găsi acest obiect în depozit și a verifica că obiectul returnat este aceeași instanță:

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

Ce se întâmplă în acest caz? Set() întoarce TableDictionary, care conține ConcurrentDictionary și oferă funcționalitate suplimentară pentru indecșii primari și secunzi. Acest lucru permite metode pentru căutarea după Id (sau alte indecși personalizați) fără a parcurge întreaga colecție de obiecte.

Când se adaugă obiecte în ObjectRepository se adaugă o subscriere la modificarea proprietăților lor, astfel încât orice modificare a proprietăților determină, de asemenea, adăugarea acestui obiect în coada de scriere. 
Actualizarea proprietăților din exterior arată la fel ca și lucrul cu un obiect POCO:

myParent.Children.First().Property = "Valoare actualizată";

Un obiect poate fi șters în următoarele moduri:

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

În acest caz, se face de asemenea adăugarea obiectului în coada de ștergere.

Cum funcționează salvarea?

ObjectRepository la modificarea obiectelor urmărite (cum ar fi adăugarea sau ștergerea, precum și modificarea proprietăților) declanșează un eveniment ModelChanged, la care este abonat IStorage. Implementările IStorage la apariția evenimentului ModelChanged îngăduie modificările în 3 cozi — pentru adăugare, pentru actualizare și pentru ștergere.

De asemenea, implementările IStorage la inițializare creează un temporizator, care la fiecare 5 secunde apelează salvarea modificărilor. 

În plus, există o API pentru apelarea forțată a salvării: ObjectRepository.Save().

Înainte de fiecare salvare, se face mai întâi eliminarea din cozi a operațiunilor inutile (de exemplu, duplicate de evenimente — când obiectul a fost modificat de două ori sau adăugări/ștergeri rapide de obiecte), și abia apoi salvarea propriu-zisă. 

În toate cazurile, se salvează întregul obiect actual, astfel încât este posibil ca obiectele să fie salvate într-o ordine diferită față de cea în care au fost modificate, inclusiv să fie salvate versiuni mai noi ale obiectelor decât în momentul adăugării în coadă.

Ce mai există?

  • Toate bibliotecile se bazează pe .NET Standard 2.0. Se pot utiliza în orice proiect modern .NET.
  • API este thread-safe. Colecțiile interne sunt implementate pe baza ConcurrentDictionary, iar handlerii de evenimente au fie blocări, fie nu necesită acestea. 
    Singurul lucru de reținut este că, la finalizarea aplicației, trebuie să apelați ObjectRepository.Save();
  • Indici arbitrarie (care necesită unicitate):

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

Cine folosește asta?

Personal, am început să folosesc această abordare în toate proiectele de hobby, deoarece este convenabilă și nu necesită costuri mari pentru a scrie un strat de acces la date sau să desfășor o infrastructură greoaie. De obicei, pentru mine, suficientă este stocarea datelor în litedb sau într-un fișier. 

Dar în trecut, când, împreună cu echipa, am realizat startupul acum dispărut EscapeTeams (credeam că iată banii — dar nu, din nou experiența) — am utilizat Azure Table Storage pentru stocarea datelor.

Planuri de viitor

Vreau să repar unul dintre principalele dezavantaje ale acestei abordări — scalabilitatea orizontală. Pentru asta avem nevoie fie de tranzacții distribuite (sic!), fie de a lua decizia fermă că aceleași date din instanțe diferite nu ar trebui să se schimbe, fie să se schimbe după principiul «cine e ultimul — acela e corect».

Din punct de vedere tehnic, văd o schemă posibilă:

  • A stoca în locul modelului de obiect EventLog și Snapshot
  • A găsi alte instanțe (adăugând endpoint-uri pentru toate instanțele în setările? descoperire udp? master/slave?)
  • A replica între instanțe EventLog prin orice algoritm de consens, de exemplu RAFT.

De asemenea, există o altă problemă care mă preocupă — ștergerea în cascadă sau detectarea cazurilor de ștergere a obiectelor la care există referințe din alte obiecte. 

Codul sursă

Dacă ați citit până aici — atunci rămâne doar să citiți codul, pe care îl puteți găsi pe GitHub:
https://github.com/DiverOfDark/ObjectRepository

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster