ObjectRepository — .NET in-memory repository pattern za vaše kućne projekte

Zašto pohranjivati sve podatke u memoriji?

Za pohranu podataka web stranice ili backend-a, većina razumnih ljudi prvo će odabrati SQL bazu podataka. 

No, ponekad na pamet dođe misao da model podataka nije prikladan za SQL: na primjer, prilikom izrade pretraživanja ili društvenog grafa potreban je pretraživač za složenim vezama između objekata. 

Najgore je stanje kada radite u timu, a kolega ne zna izraditi brze upite. Koliko ste vremena potrošili na rješavanje problema N+1 i na izradu dodatnih indeksa, kako bi SELECT na glavnoj stranici izvodio u razumnom vremenu?

Drugi popularan pristup je NoSQL. Prije nekoliko godina bio je veliki hype oko ove teme — za svaku povoljnu priliku uveli bismo MongoDB i radovali se odgovorima u obliku JSON dokumenata. (Usput, koliko ste štaka morali staviti zbog cikličnih referenci u dokumentima?).

Predlažem da probamo još jedan alternativni način — zašto ne bismo pokušali pohranjivati sve podatke u memoriji aplikacije, povremeno ih pohranjujući u proizvoljno spremište (datoteka, udaljena baza podataka)? 

Memorija je postala jeftina, a svi mogući podaci većine malih i srednjih projekata stanu u 1 GB memorije. (Na primjer, moj omiljeni kućni projekt — financijski praćenje, koji vodi dnevnu statistiku i povijest mojih troškova, stanja i transakcija tijekom pola godine, troši samo 45 MB memorije.)

Prednosti:

  • Pristup podacima postaje jednostavniji — ne treba se brinuti o upitima, lenjog učitavanju, osobitostima ORM-a, rad se odvija s običnim C# objektima;
  • Nema problema povezanih s pristupom iz različitih niti;
  • Vrlo je brzo — nema mrežnih upita, izostaje prevođenje koda u jezik upita, nije potrebno (de)serijaliziranje objekata;
  • Dopušteno je pohranjivati podatke u bilo kojem obliku — bilo u XML-u na disku, bilo u SQL Server-u, bilo u Azure Table Storage.

Mane:

  • Gubi se horizontalno skaliranje, a kao posljedica ne može se ostvariti zero downtime deployment;
  • Ako aplikacija padne — može se djelomično izgubiti podaci. (Ali, naše aplikacije nikada ne padaju, zar ne?)

Kako to radi?

Algoritam je sljedeći:

  • Na početku se uspostavlja veza s spremištem podataka i dolazi do učitavanja podataka;
  • Gradi se objektni model, primarni indeksi i indeksi odnosa (1:1, 1:više);
  • Pretplaćujemo se na promjene svojstava objekata (INotifyPropertyChanged) te na dodavanje ili uklanjanje elemenata iz kolekcije (INotifyCollectionChanged);
  • Prilikom aktiviranja pretplate — promijenjeni objekt dodaje se u red za ispis u spremniku podataka;
  • Periodično (uz pomoć tajmera) u pozadinskom procesu spremaju se promjene u spremnik;
  • Također, prilikom izlaska iz aplikacije spremaju se promjene u spremnik.

Primer koda

Dodajemo potrebne zavisnosti

// Основная библиотека
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 model podataka koji će se spremati u spremnik

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;
    
    // Primjer veze 1:Mnogo
    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);
    }
    
    // Pristup s pretragom po indeksu
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

I konačno, sama klasa-repozitorij za pristup podacima:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Za testove, omogućuje ne spremanje promjena u bazu
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Ako se koristi Hangfire i potrebno je spremiti model podataka za Hangfire u ObjectRepository
        // this.RegisterHangfireScheme();  
    
        Initialize();
    }
}

Stvaramo 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 se u projektu 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);

Prilikom ovog poziva objekt ParentModel dodaje se i u lokalni keš, i u red za zapis u bazu. Stoga ova operacija traje O(1), a s ovim objektom se može odmah raditi.

Na primjer, da pronađete ovaj objekt u repozitoriju i uvjerite se da je vraćeni objekt isti primjerak:

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

Što se pritom događa? Set() vraća TableDictionary, koji sadrži ConcurrentDictionary i pruža dodatne funkcionalnosti primarnih i sekundarnih indeksa. To omogućava metode za pretraživanje prema Id-u (ili drugim proizvoljnim korisničkim indeksima) bez potpunog pretraživanja svih objekata.

Prilikom dodavanja objekata u ObjectRepository dodaje se pretplata na promjenu njihovih svojstava, pa bilo koja promjena svojstava također dovodi do dodavanja ovog objekta u red za zapis. 
Ažuriranje svojstava s vanjske strane izgleda isto kao i rad s POCO objektom:

myParent.Children.First().Property = "Ažurirana vrijednost";

Objekt se može ukloniti na sljedeće načine:

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

Pritom se također događa dodavanje objekta u red za uklanjanje.

Kako radi spremanje?

ObjectRepository prilikom promjene praćenih objekata (kao dodavanje ili uklanjanje, tako i promjena svojstava) pokreće događaj ModelChanged, na koji se pretplaćuje IStorage. Implementacije IStorage prilikom nastanka događaja ModelChanged nakupljaju promjene u 3 reda – za dodavanje, za ažuriranje i za uklanjanje.

Također implementacije IStorage prilikom inicijalizacije kreiraju mjerač vremena koji svake 5 sekundi pokreće spremanje promjena. 

Osim toga, postoji API za prisilno pozivanje spremanja: ObjectRepository.Save().

Prije svakog spremanja prvo se uklanjaju iz redova besmislene operacije (na primjer duplici događaja – kada se objekt mijenjao dvaput ili brzo dodavanje/uklanjanje objekata), a tek potom samo spremanje. 

U svim slučajevima sprema se aktualni objekt u cijelosti, pa je moguća situacija da se objekti spremaju u drugom redoslijedu nego što su se mijenjali, pa čak mogu biti spremljene novije verzije objekata nego u trenutku dodavanja u red.

Što je još?

  • Sve biblioteke temelje se na .NET Standard 2.0. Mogu se koristiti u bilo kojem modernom .NET projektu.
  • API je thread-safe. Interni kolekcije su implementirane na bazi ConcurrentDictionary, a handleri događaja imaju ili blokade ili ih ne trebaju. 
    Jedina stvar o kojoj treba voditi računa - prilikom zatvaranja aplikacije pozvati ObjectRepository.Save();
  • Prilagodljivi indeksi (zahtijevaju jedinstvenost):

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

Tko to koristi?

Osobno, ja sam počeo koristiti ovaj pristup u svim svojim hobi projektima, jer je praktičan, i ne zahtijeva velike troškove pisanja sloja za pristup podacima ili postavljanja teške infrastrukture. Osobno mi obično sufficira pohrana podataka u litedb ili u datoteci. 

No, u prošlosti, kada smo s tim timom radili na sada već nepostojećem startapu EscapeTeams (mislio sam da su tu novci - ali ne, opet iskustvo) - koristili smo Azure Table Storage za pohranu podataka.

Planovi za budućnost

Želim popraviti jedan od glavnih nedostataka ovog pristupa - horizontalno skaliranje. Za to su potrebne ili distribuirane transakcije (sic!), ili donijeti odluku da isti podaci iz različitih instanci ne bi trebali mijenjati, ili neka se mijenjaju prema načelu 'tko posljednji - taj je u pravu'.

S tehničkog stajališta, vidim mogućom sljedeću shemu:

  • Pohraniti umjesto objektne modele EventLog i Snapshot
  • Pronaći druge instance (dodati u postavke krajnje točke svih instanci? udp otkrivanje? master/slave?)
  • Replikacija između instanci EventLog-a putem nekog od algoritama konsenzusa, na primjer RAFT.

Postoji i još jedan problem koji me brine - kaskadno brisanje, ili otkrivanje slučajeva brisanja objekata na koje postoje reference iz drugih objekata. 

Izvorni kod

Ako ste pročitali do ovdje - preostaje samo čitati kod, koji možete pronaći na GitHubu:
https://github.com/DiverOfDark/ObjectRepository

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster