ObjectRepository – vzorec repozitorija .NET v pomnilniku za vaše domače projekte

Zakaj shranjevati vse podatke v pomnilnik?

Za shranjevanje podatkov spletnega mesta ali zalednih podatkov bo prva želja večine zdravih ljudi izbrati bazo podatkov SQL. 

Toda včasih mi pride na misel, da podatkovni model ni primeren za SQL: na primer, ko gradite iskalni ali socialni graf, morate iskati zapletene odnose med objekti. 

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

Drug priljubljen pristop je NoSQL. Pred nekaj leti je bilo okoli te teme veliko hrupa - za vsako primerno priložnost so namestili MongoDB in bili zadovoljni z odgovori v obliki dokumentov json (mimogrede, koliko bergel si moral vstaviti zaradi krožnih povezav v dokumentih?).

Predlagam, da poskusite z drugo, alternativno metodo - zakaj ne poskusite shraniti vseh podatkov v pomnilnik aplikacije in jih občasno shraniti v naključni pomnilnik (datoteka, oddaljena zbirka podatkov)? 

Pomnilnik je postal poceni in vsi možni podatki za večino majhnih in srednje velikih projektov se bodo prilegali v 1 GB pomnilnika. (Na primer, moj najljubši domači projekt je finančni sledilnik, ki leto in pol hrani dnevno statistiko in zgodovino mojih stroškov, stanja in transakcij, porabi le 45 MB pomnilnika.)

Profesionalci:

  • Dostop do podatkov postane lažji - ni vam treba skrbeti za poizvedbe, leno nalaganje, funkcije ORM, delate z običajnimi objekti C#;
  • Ni težav, povezanih z dostopom iz različnih niti;
  • Zelo hitro – brez omrežnih zahtev, brez prevoda kode v jezik poizvedb, brez potrebe po (de)serializaciji objektov;
  • Podatke je sprejemljivo shranjevati v kakršni koli obliki – bodisi v XML na disku, bodisi v SQL Server ali v Azure Table Storage.

Cons:

  • Horizontalno skaliranje je izgubljeno in posledično ni mogoče izvesti uvedbe brez izpadov;
  • Če se aplikacija zruši, lahko delno izgubite podatke. (Toda naša aplikacija se nikoli ne zruši, kajne?)

Kako deluje?

Algoritem je naslednji:

  • Na začetku se vzpostavi povezava s shrambo podatkov in naložijo se podatki;
  • Zgrajeni so objektni model, primarni indeksi in relacijski indeksi (1:1, 1:Mnogo);
  • Ustvari se naročnina za spremembe lastnosti objekta (INotifyPropertyChanged) in za dodajanje ali odstranjevanje elementov v zbirko (INotifyCollectionChanged);
  • Ko se naročnina sproži, se spremenjeni objekt doda v čakalno vrsto za zapisovanje v podatkovno shrambo;
  • Spremembe pomnilnika se občasno (na časovniku) shranjujejo v niti v ozadju;
  • Ko zapustite aplikacijo, se spremembe shranijo tudi v shrambo.

Vzorčna koda

Dodajanje potrebnih 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

Opisujemo 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;
    
    // Пример связи 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;
}

In končno, sam razred repozitorija za dostop do podatkov:

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

Ustvarite 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 bo projekt uporabljal HangFire

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

Vstavljanje novega predmeta:

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

S tem klicem predmet ParentModel se doda v lokalni predpomnilnik in čakalno vrsto za pisanje v bazo podatkov. Zato ta operacija traja O(1) in s tem objektom je mogoče takoj delati.

Če želite na primer poiskati ta objekt v repozitoriju in preveriti, ali je vrnjeni objekt isti primerek:

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

Kaj se dogaja? Set () vrne TableDictionary, ki vsebuje ConcurrentDictionary in zagotavlja dodatno funkcionalnost primarnih in sekundarnih indeksov. To vam omogoča, da imate metode za iskanje po ID-ju (ali drugih poljubnih uporabniških indeksih) brez popolnega ponavljanja po vseh objektih.

Pri dodajanju predmetov v ObjectRepository dodana je naročnina za spreminjanje njihovih lastnosti, zato vsaka sprememba lastnosti prav tako povzroči, da se ta objekt doda v čakalno vrsto za pisanje. 
Posodabljanje lastnosti od zunaj je videti enako kot delo s predmetom POCO:

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

Predmet lahko izbrišete na naslednje načine:

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

To prav tako doda predmet v čakalno vrsto za brisanje.

Kako deluje varčevanje?

ObjectRepository ko se nadzorovani objekti spremenijo (bodisi dodajanje ali brisanje ali spreminjanje lastnosti), sproži dogodek ModelChangednaročen na IShramba. Izvedbe IShramba ko pride do dogodka ModelChanged spremembe se postavijo v 3 čakalne vrste - za dodajanje, za posodabljanje in za brisanje.

Tudi izvedbe IShramba po inicializaciji ustvarijo časovnik, ki povzroči shranjevanje sprememb vsakih 5 sekund. 

Poleg tega obstaja API za vsilitev klica za shranjevanje: ObjectRepository.Save().

Pred vsakim shranjevanjem se iz čakalnih vrst najprej odstranijo nesmiselne operacije (na primer podvojeni dogodki - ko je bil objekt dvakrat spremenjen ali hitro dodani/odstranjeni objekti), šele nato samo shranjevanje. 

V vseh primerih je shranjen celoten trenutni objekt, zato je možno, da so objekti shranjeni v drugačnem vrstnem redu, kot so bili spremenjeni, vključno z novejšimi različicami objektov kot v času, ko so bili dodani v čakalno vrsto.

Kaj je še tam?

  • Vse knjižnice temeljijo na .NET Standard 2.0. Lahko se uporablja v katerem koli sodobnem projektu .NET.
  • API je varen za niti. Interne zbirke so izvedene na podlagi ConcurrentDictionary, imajo obdelovalci dogodkov ključavnice ali pa jih ne potrebujejo. 
    Edina stvar, ki si jo je vredno zapomniti, je klic ObjectRepository.Save();
  • Poljubni indeksi (zahtevajo edinstvenost):

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

Kdo ga uporablja?

Osebno sem ta pristop začel uporabljati pri vseh hobi projektih, ker je priročen in ne zahteva velikih stroškov za pisanje sloja za dostop do podatkov ali uvajanje težke infrastrukture. Osebno mi običajno zadostuje shranjevanje podatkov v litedb ali datoteko. 

Toda v preteklosti, ko je zdaj propadlo zagonsko podjetje EscapeTeams (Mislil sem, da je tukaj denar - a ne, spet izkušnja) - uporablja se za shranjevanje podatkov v Azure Table Storage.

Načrti za prihodnost

Rad bi popravil eno glavnih pomanjkljivosti tega pristopa - vodoravno skaliranje. Če želite to narediti, potrebujete ali porazdeljene transakcije (sic!) ali pa se dobrovoljno odločite, da se isti podatki iz različnih instanc ne smejo spreminjati ali pa pustite, da se spreminjajo po načelu »kdor je zadnji, ima prav«.

S tehničnega vidika vidim naslednjo shemo kot možno:

  • Shranite EventLog in Snapshot namesto objektnega modela
  • Poiščite druge primerke (dodajte končne točke vseh primerkov v nastavitve? odkrivanje udp? glavni/podrejeni?)
  • Podvajanje med primerki EventLog prek katerega koli algoritma soglasja, kot je RAFT.

Skrbi pa še ena težava - kaskadno brisanje oziroma zaznavanje primerov brisanja objektov, ki imajo povezave iz drugih objektov. 

Izvorna koda

Če ste prebrali vse do sem, vam preostane le še branje kode; najdete jo na GitHubu:
https://github.com/DiverOfDark/ObjectRepository

Vir: www.habr.com

Dodaj komentar