ObjectRepository — .NET in-memory repository obrazac za vaše lične projekte

Zakaj hraniti vse podatke v pomnilniku?

Za shranjevanje podatkov spletnega mesta ali ozadja večina razumnih ljudi najprej izbere SQL bazo podatkov. 

Včasih pa pride na misel, da model podatkov morda ni primeren za SQL: na primer, pri gradnji iskalnika ali družbenega grafikon lahko potrebujete iskanje preko zapletenih povezav med objekti. 

Najslabše je, ko delate v ekipi in kolega ne zna graditi hitrih poizvedb. Koliko časa ste porabili za reševanje težav N+1 in za gradnjo dodatnih indeksov, da je SELECT na glavni strani deloval v razumnem času?

Drug priljubljen pristop je NoSQL. Pred nekaj leti je bilo ob tej temi veliko vznemirjenja — za vsak ugoden primer smo odprli MongoDB in bili veseli odgovorov v obliki json-dokumentov. (mimogrede, koliko bolečih rešitev je bilo potrebnih zaradi obročnih povezav v dokumentih?).

Predlagam, da poskusite še en, alternativen način — zakaj ne bi hranili vseh podatkov v pomnilniku aplikacije, občasno pa jih shranili v poljuben skladišče (datoteka, oddaljena baza podatkov)? 

Pomnilnik je postal poceni, večina mogočih podatkov manjših in srednje velikih projektov se prilega v 1 GB pomnilnika. (Na primer, moj najljubši domači projekt — finančni sledilnik, ki vodi dnevno statistiko in zgodovino mojih stroškov, stanj in transakcij v zadnjem letu in pol, porabi le 45 MB pomnilnika.)

Prednosti:

  • Dostop do podatkov postane enostavnejši — ni treba skrbeti za poizvedbe, leno nalaganje, posebnosti ORM, delo poteka z običajnimi C# objekti;
  • Ni težav, povezanih z dostopom iz različnih niti;
  • Zelo hitro — ni omrežnih poizvedb, odsotnost prevajanja kode v jezik poizvedb, ni potrebno (de)seriliziranje objektov;
  • Dovoljena je shranitev podatkov v katerikoli obliki — bodisi v XML na disku, bodisi v SQL Serverju, bodisi v Azure Table Storage.

Mane:

  • Zgubljeno je horizontalno skaliranje, in kot posledica tega ni mogoče izvesti zero downtime implementacije;
  • Če aplikacija pade — lahko delno izgubimo podatke. (Ampak naša aplikacija nikoli ne pade, kajne?)

Kako deluje?

Algoritem je naslednji:

  • Ob zagonu se vzpostavi povezava s podatkovnim skladiščem in naložijo se podatki;
  • Gradi se objektni model, primarni indeksi in indeksi odnosov (1:1, 1:Many);
  • Ustvarja se naročnina na spremembe lastnosti objektov (INotifyPropertyChanged) in na dodajanje ali odstranjevanje elementov v zbirki (INotifyCollectionChanged);
  • Ob aktivaciji naročnine se spremenjeni objekt doda v vrsto za zapis v podatkovno shrambo;
  • Občasno (po časovniku) se v ozadju shranijo spremembe v shrambi;
  • Ob zapustitvi aplikacije se tudi shranijo spremembe v shrambi.

Primer kode

Dodamo potrebne odvisnosti

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

Opišemo podatkovni model, ki bo shranjen v shrambi

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

Nato objektni model:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Primer povezave 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);
    }
    
    // Dostop s iskanjem po indeksu
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

In končno razred repozitorija za dostop do podatkov:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Za teste, omogoča, da ne shranjujemo sprememb v bazo
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Če se uporablja Hangfire in je potrebno hraniti model podatkov za Hangfire v ObjectRepository
        // this.RegisterHangfireScheme();
    
        Initialize();
    }
}

Ustvarimo primerek ObjectRepository:

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

Če se v projektu uporablja HangFire

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

Vstavitev novega objekta:

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

Ob tem klicu je objekt ParentModel se dodaja tako v lokalno predpomnilnik kot v vrsto za zapis v bazo. Zato ta operacija traja O(1), in s tem objektom lahko takoj delamo.

Na primer, da najdete ta objekt v repozitoriju in se prepričate, da je vrnjen objekt istega primera:

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

Kaj se pri tem zgodi? Set() vračajo TableDictionary, ki vsebuje ConcurrentDictionary in zagotavlja dodatno funkcionalnost primarnih in sekundarnih indeksov. To omogoča imeti metode za iskanje po Id (ali drugih poljubnih uporabniških indeksih) brez popolnega prečesavanja vseh objektov.

Pri dodajanju objektov v ObjectRepository se doda naročnina za spremembo njihovih lastnosti, zato vsaka sprememba lastnosti povzroči tudi dodatek tega objekta v vrsto za zapis. 
Posodobitev lastnosti od zunaj izgleda enako kot delo s POCO objektom:

myParent.Children.First().Property = "Posodobljena vrednost";

Objekt lahko odstranite na naslednje načine:

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

Pri tem se tudi dodaja objekt v vrsto za odstranjevanje.

Kako deluje shranjevanje?

ObjectRepository pri spremembah spremljanih objektov (kot dodajanje ali odstranjevanje ter sprememba lastnosti) sproži dogodek ModelChanged, na katerega je naročen IStorage. Implementacije IStorage ob nastanka dogodka ModelChanged shranijo spremembe v 3 vrste — za dodajanje, posodobitev in odstranjevanje.

Implementacije IStorage ob inicializaciji ustvarijo časovnik, ki vsakih 5 sekund sproži shranjevanje sprememb. 

Obstaja tudi API za prisilno sprožitev shranjevanja: ObjectRepository.Save().

Pred vsakim shranjevanjem se najprej odstranijo iz vrst brezsmiselne operacije (na primer podvojitve dogodkov — ko se je objekt spremenil dvakrat ali hitro dodajanje/odstranjevanje objektov), šele nato samo shranjevanje. 

V vseh primerih se shrani aktualen objekt v celoti, zato je možna situacija, ko se objekti shranijo v drugačnem vrstnem redu, kot so se spreminjali, vključno z možnostjo shranjevanja novejših različic objektov, kot so bile ob dodajanju v vrsto.

Šta je još?

  • Vse knjižnice temeljijo na .NET Standard 2.0. Lahko se uporabljajo v katerem koli sodobnem .NET projektu.
  • API je niti varno. Notranje zbirke so izvedene na osnovi ConcurrentDictionary, obravnava dogodkov ima bodisi zaklepe bodisi jih ne potrebuje. 
    Eno, kar je treba zapomniti — pri zaključku aplikacije pokličite ObjectRepository.Save();
  • Poljubni indeksi (zahtevajo edinstvenost):

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

Kdo to uporablja?

Osebno sem začel uporabljati ta pristop pri vseh svojih hobi projektih, ker je priročno in ne zahteva veliko napora za pisanje sloja za dostop do podatkov ali uvajanje težke infrastrukture. Zame je običajno dovolj, da podatke hranim v litedb ali datoteki. 

Toda v preteklosti, ko smo s timom delali zdaj že umrli startup EscapeTeams (sem mislil, da so tukaj, denar — pa ni, spet izkušnje) — smo za shranjevanje podatkov uporabljali Azure Table Storage.

Načrti za prihodnost

Želim odpraviti eno od glavnih slabosti tega pristopa — horizontalno skaliranje. Za to so potrebne bodisi razdeljene transakcije (sic!), bodisi je treba sprejeti odločitev, da se enaki podatki iz različnih instanc ne smejo spreminjati, ali pa naj se spreminjajo po načelu 'kdo zadnji, tisti je pravi'.

Z vidika tehnike vidim naslednjo shemo:

  • Shranjujem namesto objektnega modela EventLog in Snapshot
  • Najti druge instance (dodati končne točke vseh instanc v nastavitve? udp odkrivanje? master/slave?)
  • Replikacija med instancami EventLog preko katerega koli od konsenznih algoritmov, na primer RAFT.

Obstaja tudi še en problem, ki me skrbi — to je kaskadno brisanje ali odkrivanje primerov brisanja objektov, na katere so sklici iz drugih objektov. 

Izvorna koda

Če ste do sem prebrali — ostane le še branje kode, ki jo lahko najdete na GitHubu:
https://github.com/DiverOfDark/ObjectRepository

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster