ObjectRepository — .NET mälureposiitoori mudel teie isiklike projektide jaoks

Miks hoida kogu teavet mälus?

Veebisaidi või taustasüsteemi andmete salvestamisel valivad enamik mõistlikke inimesi esmalt SQL andmebaasi. 

Kuid mõnikord tekib mõte, et andmemudel ei sobi SQL-iga: näiteks otsingu või sotsiaalsete graafikute koostamisel on vajalik keeruline objekti vaheliste seoste otsimine. 

Halvim on see, kui töötate meeskonnas ja kolleeg ei oska luua kiireid päringuid. Kui palju aega olete kulutanud N+1 probleemide lahendamisele ja täiendavate indeksite loomisele, et SELECT peaeesmärgil töötaks mõistliku ajaga?

Teine populaarne lähenemine on NoSQL. Mõni aasta tagasi oli selle teema ümber suur müra — iga sobiva juhtumi jaoks käivitasime MongoDB ja nautisime vastuseid JSON-dokumentidena. (Muide, kui palju kompromisse tuli dokumentides tsükliliste linkide tõttu teha?).

Pakun proovima veel ühte alternatiivset viisi — miks mitte hoida kõiki andmeid rakenduse mälus, salvestades neid perioodiliselt mis tahes salvestusse (fail, kaugandmebaas)? 

Mälu on saanud odavaks ning enamik väikeste ja keskmiste projektide andmeid mahub 1 GB mällu. (Näiteks, minu lemmik koduprojekt — rahanduse jälgija, mis hoiab igapäevast statistikat ja minu kulutuste, saldod ja tehingute ajalugu aasta ja kuu jooksul, vajab vaid 45 MB mälu.)

Plussid:

  • Andmetele pääsemine on lihtsam — ei pea muretsema päringute, laiskade laadimise, ORM-iga seotud eripärade pärast, töö toimub tavapäraste C# objektidega;
  • Ei ole probleeme erinevatest lõngadest juurdepääsuga;
  • Väga kiire — pole võrgu päringuid, puudub koodi tõlkimine päringute keelde, ei ole vaja (de)serialiseerida objekte;
  • Andmeid võib hoida mis tahes vormis — kas siis XML-is kettal, SQL Serveris või Azure Table Storage'is.

Miinused:

  • Kadunud horisontaalne skaaleerimine, mis tähendab, et ei saa teostada null-sisaldusega juurutamist;
  • Kui rakendus langeb, on andmed osaliselt kadunud. (Aga meie rakendus ei krahhi kunagi, eks?)

Kuidas see töötab?

Algrütm on järgmine:

  • Alustamisel luuakse ühendus andmehoidla ning laaditakse andmed;
  • Ehitatakse objekti mudel, esmased indeksid ja seose indeksid (1:1, 1:Many);
  • Loodakse tellimus objekti omaduste muutuste (INotifyPropertyChanged) ning elementide lisamise või eemaldamise kohta kogus (INotifyCollectionChanged);
  • Tellimuse aktiveerimisel — muudetud objekt lisatakse andmehoidlasse salvestamise järjekorda;
  • Perioodiliselt (ajastaja kaudu) taustaprotsessis salvestatakse muudatused andmehoidlasse;
  • Rakendusest väljumisel salvestatakse samuti muudatused andmehoidlasse.

Koodinäide

Lisame vajalikud sõltuvused

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

Kirjeldame andmemudelit, mis salvestatakse andmehoidlasse

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

Seejärel objekti mudel:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Näide seosest 1:Palju
    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
    }
    
    // Otsinguhindade abil
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Ja lõpuks, klass-repositoorium andmete juurde pääsemiseks:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Testimiseks, võimaldab mitte salvestada muudatusi andmebaasi
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Kui Hangfire'i kasutatakse ja on vajalik salvestada andmemudelit Hangfire'i objektirepositooriumis
        // this.RegisterHangfireScheme();  
    
        Initialize();
    }
}

Loome ObjectRepository eksemplari:

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

Kui projektis kasutatakse HangFire'i

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

Uue objekti lisamine:

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

Sel hetkel lisatakse objekt ParentModel nii kohalikku vahemälusse kui ka kirjutamisjärjekorda andmebaasis. Seetõttu võtab see operatsioon O(1) aega ja seda objekti saab kohe kasutada.

Näiteks, et leida see objekt repositooriumist ja veenduda, et tagastatud objekt on sama eksemplar:

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

Mis sel hetkel juhtub? Set() tagastab TableDictionary, mis sisaldab endas ConcurrentDictionary ja pakub täiendavaid funktsioone peamiste ja teiseste indeksite jaoks. See võimaldab meetodeid otsimiseks ID järgi (või muude juhuslike kasutaja indeksite järgi) ilma kõigi objektide täieliku läbivaatamiseta.

Objektide lisamisel ObjectRepository lisatakse tellimus nende omaduste muutmisele, seega toob iga omaduste muutumine kaasa ka selle objekti lisamise kirjutamisjärjekorda. 
Omandite väljastpoolt värskendamine toimub nagu ka POCO-objektidega töötamine:

myParent.Children.First().Property = "Värskendatud väärtus";

Objekti saab eemaldada järgmistel viisidel:

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

Samuti toimub objekti lisamine eemaldamise järjekorda.

Kuidas salvestamine töötab?

ObjectRepository kui jälgitud objekte muudetakse (näiteks lisamine, eemaldamine või omaduste muutmine) kutsub see esile sündmuse ModelChanged, millele on tellinud IStorage. Teostused IStorage sündmuse tekkimisel ModelChanged koguvad muudatused 3 järjekorda — lisamine, värskendamine ja eemaldamine.

Samuti teostused IStorage Algoritm käivitamisel luuakse taimer, mis kutsub iga 5 sekundi järel esile muudatuste salvestamise. 

Lisaks on olemas API sundsalvestamise jaoks: ObjectRepository.Save().

Iga salvestamise eel eemaldatakse järjekordadest mõttetud toimingud (näiteks sündmuste duplikaadid — kui objekt muutus kaks korda või objekti kiire lisamine/eemaldamine), ja alles siis toimub salvestamine. 

Igal juhul salvestatakse kogu kehtiv objekt, mistõttu võib juhtuda, et objekte salvestatakse erinevas järjekorras kui need muutusid, sealhulgas võivad salvestuda uuemad versioonid objektidest kui need järjekorda lisamisel.

Mis on veel?

  • Kõik raamatukogud põhinevad .NET Standard 2.0-l. Saate kasutada igas kaasaegses .NET projektis.
  • API on lõimivaba. Sisemist kollektsiooni on rakendatud ConcurrentDictionary, sündmuste töötlejate juures on kas lukud või ei ole neid vaja. 
    Ainus, mida tuleks meeles pidada — rakenduse lõpetamisel kutsuda esile ObjectRepository.Save();
  • Mugavad indeksid (nõuavad unikaalsust):

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

Kes seda kasutab?

Isiklikult hakkan ma seda lähenemist kõikides oma hobi projektides kasutama, kuna see on mugav ja ei nõua suuri kulutusi andmete juurdepääsutasandi kirjutamiseks või keeruka infrastruktuuri seadistamiseks. Minu jaoks piisab tavaliselt andmete hoidmisest litedb või failis. 

Aga varem, kui koos meeskonnaga tegime nüüdseks kadunud startuppi EscapeTeams (mõtlesin, et nüüd on raha — aga ei, jälle kogemus) — kasutasime andmete hoidmiseks Azure Table Storage.

Tulevikuplaanid

Tahaksin parandada ühe selle lähenemise põhiminuse — horisontaalne skaleerimine. Selleks on vaja kas jaotatud tehinguid (sic!), või teha otsus, et samad andmed erinevates instantsides ei tohiks muutuda, või lasta neil muutuda printsiibi järgi 'kes viimasena tuleb, see ainus peab'.

Tehnilisest vaatepunktist näen ma järgmise skeemi võimalikuna:

  • Hoidke objekti mudeli asemel EventLog ja Snapshot
  • Leida teised instantsid (kas lisada kõikide instantside lõpp-punktide seadistustesse? udp avastamine? master/slave?)
  • Replitseerige EventLog instantside vahel mis tahes konsensuse algoritmiga, näiteks RAFT.

On veel mures, mis mind häirivad — see on kaskaadsete kustutamiste või juhtumite tuvastamine, kus objektid, millele viidatakse, on kustutatud teistest objektidest. 

Allikasood

Kui olete siiani lugenud — jääb üle ainult koodi lugemine, mille leiate GitHubist:
https://github.com/DiverOfDark/ObjectRepository

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster