ObjectRepository - .NET uzorak repozitorija u memoriji za vaše kućne projekte

Zašto pohraniti sve podatke u memoriju?

Za pohranjivanje web stranica ili pozadinskih podataka, prva želja većine zdravih ljudi bit će odabrati SQL bazu podataka. 

Ali ponekad vam padne na pamet pomisao da podatkovni model nije prikladan za SQL: na primjer, kada gradite grafikon pretraživanja ili društveni grafikon, trebate pretraživati ​​složene odnose između objekata. 

Najgore je kad radite u timu, a kolega ne zna graditi brze upite. Koliko ste vremena potrošili na rješavanje N+1 problema i izgradnju dodatnih indeksa kako bi SELECT na glavnoj stranici završio u razumnom roku?

Još jedan popularan pristup je NoSQL. Prije nekoliko godina bilo je puno pompe oko ove teme - za svaku pogodnu priliku postavili su MongoDB i bili su zadovoljni odgovorima u obliku json dokumenata (usput, koliko ste štaka morali ubaciti zbog kružnih poveznica u dokumentima?).

Predlažem da isprobate drugu, alternativnu metodu - zašto ne pokušate pohraniti sve podatke u memoriju aplikacije, povremeno ih spremajući u nasumičnu pohranu (datoteka, udaljena baza podataka)? 

Memorija je postala jeftina, a svi mogući podaci za većinu malih i srednjih projekata stat će u 1 GB memorije. (Na primjer, moj omiljeni kućni projekt je financijski tragač, koji godinu i pol dana vodi dnevnu statistiku i povijest mojih troškova, stanja i transakcija, troši samo 45 MB memorije.)

Pros:

  • Pristup podacima postaje lakši - ne morate brinuti o upitima, lijenom učitavanju, ORM značajkama, radite s običnim C# objektima;
  • Nema problema povezanih s pristupom iz različitih niti;
  • Vrlo brzo - nema mrežnih zahtjeva, nema prijevoda koda u jezik upita, nema potrebe za (de)serijalizacijom objekata;
  • Prihvatljivo je pohranjivati ​​podatke u bilo kojem obliku - bilo u XML-u na disku, bilo u SQL Serveru ili u Azure Table Storageu.

Cons:

  • Horizontalno skaliranje je izgubljeno, i kao rezultat toga, ne može se izvršiti implementacija bez prekida rada;
  • Ako se aplikacija sruši, možete djelomično izgubiti podatke. (Ali naša se aplikacija nikad ne ruši, zar ne?)

Kako se to radi?

Algoritam je sljedeći:

  • U početku se uspostavlja veza s pohranom podataka i podaci se učitavaju;
  • Grade se objektni model, primarni indeksi i relacijski indeksi (1:1, 1:Mnogo);
  • Pretplata se kreira za promjene u svojstvima objekta (INotifyPropertyChanged) i za dodavanje ili uklanjanje elemenata u kolekciju (INotifyCollectionChanged);
  • Kada se pretplata pokrene, promijenjeni objekt se dodaje u red čekanja za pisanje u pohranu podataka;
  • Promjene pohrane povremeno se spremaju (na mjeraču vremena) u pozadinskoj niti;
  • Kada izađete iz aplikacije, promjene se također spremaju u pohranu.

Primjer koda

Dodavanje potrebnih ovisnosti

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

Opisujemo podatkovni model koji će biti pohranjen u pohranu

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

Zatim objektni model:

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

I na kraju, sama klasa repozitorija za pristup podacima:

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

Stvorite instancu ObjectRepository:

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

Ako će projekt koristiti HangFire

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

Umetanje novog objekta:

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

Ovim pozivom objekt ParentModel dodaje se u lokalnu predmemoriju i red čekanja za pisanje u bazu podataka. Stoga ova operacija traje O(1) i s ovim se objektom može odmah raditi.

Na primjer, da pronađete ovaj objekt u repozitoriju i potvrdite da je vraćeni objekt ista instanca:

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

Što se događa? set () vraća TableDictionary, koji sadrži ConcurrentDictionary te pruža dodatnu funkcionalnost primarnih i sekundarnih indeksa. To vam omogućuje da imate metode za pretraživanje prema ID-u (ili drugim proizvoljnim korisničkim indeksima) bez potpunog ponavljanja svih objekata.

Prilikom dodavanja objekata u ObjectRepository dodaje se pretplata za promjenu njihovih svojstava, tako da svaka promjena svojstava također rezultira dodavanjem ovog objekta u red čekanja za pisanje. 
Ažuriranje svojstava izvana izgleda isto kao rad s POCO objektom:

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

Objekt možete izbrisati na sljedeće načine:

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

Ovo također dodaje objekt u red čekanja za brisanje.

Kako funkcionira spremanje?

ObjectRepository kada se nadzirani objekti mijenjaju (bilo dodavanjem ili brisanjem ili promjenom svojstava), pokreće događaj ModelPromijenjenpretplaćeni na IShrana. Implementacije IShrana kada se događaj dogodi ModelPromijenjen promjene se stavljaju u 3 reda - za dodavanje, za ažuriranje i za brisanje.

Također implementacije IShrana nakon inicijalizacije, oni stvaraju mjerač vremena koji uzrokuje spremanje promjena svakih 5 sekundi. 

Osim toga, postoji API za prisiljavanje poziva za spremanje: ObjectRepository.Save().

Prije svakog spremanja, prvo se iz redova čekanja uklanjaju besmislene operacije (primjerice, dupli događaji - kada je objekt dva puta promijenjen ili brzo dodani/uklonjeni objekti), a tek onda samo spremanje. 

U svim slučajevima sprema se cijeli trenutni objekt, tako da je moguće da su objekti spremljeni drugačijim redoslijedom od onog kojim su promijenjeni, uključujući novije verzije objekata od onih u trenutku kada su dodani u red čekanja.

Što je još tamo?

  • Sve biblioteke temelje se na .NET Standardu 2.0. Može se koristiti u bilo kojem modernom .NET projektu.
  • API je siguran za niti. Interne zbirke provode se na temelju ConcurrentDictionary, rukovatelji događajima ili imaju brave ili ih ne trebaju. 
    Jedina stvar koju vrijedi zapamtiti je nazvati ObjektRepository.Save();
  • Proizvoljni indeksi (zahtijevaju jedinstvenost):

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

Tko ga koristi?

Osobno sam počeo koristiti ovaj pristup u svim hobi projektima jer je zgodan i ne zahtijeva velike troškove za pisanje sloja pristupa podacima ili postavljanje teške infrastrukture. Osobno, pohranjivanje podataka u litedb ili datoteku obično mi je dovoljno. 

Ali u prošlosti, kada je sada već nepostojeći startup EscapeTeams (Mislio sam, evo ga, novac - ali ne, opet iskustvo) - koristi se za pohranu podataka u Azure Table Storage.

Planovi za budućnost

Želio bih popraviti jedan od glavnih nedostataka ovog pristupa - horizontalno skaliranje. Da biste to učinili, trebate ili distribuirati transakcije (sic!), ili donijeti čvrstu odluku da se isti podaci iz različitih instanci ne smiju mijenjati ili pustiti da se mijenjaju po principu “tko je zadnji ima pravo”.

S tehničkog gledišta, sljedeću shemu vidim kao moguću:

  • Pohranite EventLog i Snapshot umjesto objektnog modela
  • Pronađite druge instance (dodajte krajnje točke svih instanci u postavke? otkrivanje udp-a? glavni/podređeni?)
  • Replicirajte među instancama EventLog-a putem bilo kojeg konsenzusnog algoritma, kao što je RAFT.

Postoji i još jedan problem koji me zabrinjava - kaskadno brisanje, odnosno otkrivanje slučajeva brisanja objekata koji imaju poveznice s drugim objektima. 

Izvorni kod

Ako ste pročitali sve do ovdje, preostaje samo pročitati kod; nalazi se na GitHubu:
https://github.com/DiverOfDark/ObjectRepository

Izvor: www.habr.com

Dodajte komentar