ObjectRepository - .NET-muistin arkistomalli kotiprojekteihisi

Miksi tallentaa kaikki tiedot muistiin?

WWW-sivusto- tai taustatietojen tallentamiseksi järkevimpien ihmisten ensimmäinen toive on valita SQL-tietokanta. 

Mutta joskus tulee mieleen ajatus, että tietomalli ei sovellu SQL:lle: esimerkiksi hakua tai sosiaalista graafia rakennettaessa täytyy etsiä monimutkaisia ​​suhteita objektien välillä. 

Pahin tilanne on, kun työskentelet ryhmässä ja kollega ei osaa rakentaa nopeita kyselyjä. Kuinka paljon aikaa käytit N+1-tehtävän ratkaisemiseen ja lisähakemistojen rakentamiseen, jotta pääsivun SELECT valmistuisi kohtuullisessa ajassa?

Toinen suosittu lähestymistapa on NoSQL. Useita vuosia sitten tästä aiheesta oli paljon hypeä - kaikkiin sopiviin tilaisuuksiin he ottivat käyttöön MongoDB:n ja olivat tyytyväisiä vastauksiin json-dokumenttien muodossa. (Muuten, kuinka monta kainalosauvaa piti laittaa paikoilleen asiakirjojen pyöreiden linkkien takia?).

Suosittelen kokeilemaan toista vaihtoehtoista menetelmää - miksi et yrittäisi tallentaa kaikkia tietoja sovelluksen muistiin, tallentamalla ne ajoittain satunnaiseen tallennustilaan (tiedosto, etätietokanta)? 

Muistista on tullut halpaa, ja kaikki mahdolliset tiedot useimmille pienille ja keskikokoisille projekteille mahtuvat 1 Gt muistiin. (Esimerkiksi suosikkini kotiprojektini on talousseuranta, joka pitää päivittäisiä tilastoja ja kulujeni, saldojeni ja tapahtumieni historiaa puolentoista vuoden ajan, kuluttaa vain 45 Mt muistia.)

Plussat:

  • Tietojen käyttö helpottuu - sinun ei tarvitse huolehtia kyselyistä, laiskasta latauksesta, ORM-ominaisuuksista, vaan työskentelet tavallisten C#-objektien kanssa;
  • Eri säikeistä pääsyyn ei liity ongelmia;
  • Erittäin nopea - ei verkkopyyntöjä, ei koodin käännöstä kyselykielelle, ei tarvetta (de)serialisoida objekteja;
  • On hyväksyttävää tallentaa tietoja missä tahansa muodossa - olipa se sitten XML-muodossa levyllä, SQL Serverissä tai Azure Table Storagessa.

Miinukset:

  • Vaakasuuntainen skaalaus menetetään, ja sen seurauksena nollakatkosaikaa ei voida ottaa käyttöön;
  • Jos sovellus kaatuu, saatat menettää tietoja osittain. (Mutta sovelluksemme ei koskaan kaatuu, eikö niin?)

Miten se toimii?

Algoritmi on seuraava:

  • Alussa muodostetaan yhteys tietovarastoon ja data ladataan;
  • Kohdemalli, ensisijaiset indeksit ja relaatioindeksit (1:1, 1:Monet) rakennetaan;
  • Tilaus luodaan kohteen ominaisuuksien muutoksille (INotifyPropertyChanged) ja elementtien lisäämiselle tai poistamiselle kokoelmaan (INotifyCollectionChanged);
  • Kun tilaus laukeaa, muutettu objekti lisätään jonoon tietomuistiin kirjoittamista varten;
  • Muutokset tallennustilaan tallennetaan ajoittain (ajastimella) taustasäikeeseen;
  • Kun suljet sovelluksen, muutokset tallentuvat myös tallennustilaan.

Esimerkki koodista

Tarvittavien riippuvuuksien lisääminen

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

Kuvaamme tietomallin, joka tallennetaan tallennustilaan

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

Sitten objektimalli:

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

Ja lopuksi itse arkistoluokka tietojen käyttöä varten:

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

Luo ObjectRepository-esiintymä:

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

Jos projekti käyttää HangFirea

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

Uuden objektin lisääminen:

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

Tämän kutsun avulla esine ParentModel lisätään sekä paikalliseen välimuistiin että jonoon tietokantaan kirjoittamista varten. Siksi tämä operaatio vaatii O(1), ja tätä objektia voidaan käsitellä välittömästi.

Jos haluat esimerkiksi löytää tämän objektin arkistosta ja varmistaa, että palautettu objekti on sama esiintymä:

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

Mitä tapahtuu? Aseta () palaa Taulukkosanakirja, joka sisältää ConcurrentDictionary ja tarjoaa lisätoimintoja ensisijaiselle ja toissijaiselle indeksille. Tämän avulla voit etsiä menetelmiä Id:n (tai muiden mielivaltaisten käyttäjäindeksien) perusteella ilman, että iteroitat täysin kaikkia objekteja.

Kun lisäät esineitä kohteeseen Object Repository tilaus lisätään niiden ominaisuuksien muuttamiseksi, joten kaikki ominaisuuksien muutokset johtavat myös siihen, että tämä objekti lisätään kirjoitusjonoon. 
Ominaisuuksien päivittäminen ulkopuolelta näyttää samalta kuin työskentely POCO-objektin kanssa:

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

Voit poistaa kohteen seuraavilla tavoilla:

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

Tämä myös lisää objektin poistojonoon.

Miten säästäminen toimii?

Object Repository kun valvotut objektit muuttuvat (joko lisäämällä tai poistamalla tai muuttamalla ominaisuuksia), herättää tapahtuman Malli Vaihdettutilannut ITallennustila. Toteutukset ITallennustila kun tapahtuma tapahtuu Malli Vaihdettu muutokset asetetaan 3 jonoon - lisäämistä, päivitystä ja poistamista varten.

Myös toteutukset ITallennustila alustuksen yhteydessä ne luovat ajastimen, joka tallentaa muutokset 5 sekunnin välein. 

Lisäksi on API, joka pakottaa tallennuskutsun: ObjectRepository.Save().

Ennen jokaista tallennusta jonoista poistetaan ensin merkityksettömät toiminnot (esim. päällekkäiset tapahtumat - kun objektia muutettiin kahdesti tai objektit lisättiin/poistettiin nopeasti) ja vasta sitten itse tallennus. 

Joka tapauksessa koko nykyinen objekti tallennetaan, joten on mahdollista, että objektit tallennetaan eri järjestyksessä kuin ne on muutettu, mukaan lukien objektien uudemmat versiot kuin silloin, kun ne lisättiin jonoon.

Mitä muuta siellä on?

  • Kaikki kirjastot perustuvat .NET Standard 2.0:aan. Voidaan käyttää missä tahansa nykyaikaisessa .NET-projektissa.
  • API on säikeen turvallinen. Sisäiset kokoelmat toteutetaan perustuen ConcurrentDictionary, tapahtumakäsittelijöillä on lukot tai ne eivät tarvitse niitä. 
    Ainoa asia, joka kannattaa muistaa, on soittaa ObjectRepository.Save();
  • Mielivaltaiset indeksit (edellyttää yksilöllisyyttä):

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

Kuka sitä käyttää?

Henkilökohtaisesti aloin käyttämään tätä lähestymistapaa kaikissa harrastusprojekteissa, koska se on kätevä eikä vaadi suuria kustannuksia tiedonsiirtokerroksen kirjoittamiseen tai raskaan infrastruktuurin käyttöönotossa. Itselleni riittää yleensä tietojen tallentaminen litedb:hen tai tiedostoon. 

Mutta ennen, kun nyt lakkautettu startup EscapeTeams (Luulin, että tässä se on, rahaa - mutta ei, kokemus taas) - käytetään tietojen tallentamiseen Azure Table Storagessa.

Tulevaisuuden suunnitelmat

Haluaisin korjata yhden tämän lähestymistavan tärkeimmistä haitoista - vaakasuuntaisen skaalauksen. Tätä varten tarvitset joko hajautettuja tapahtumia (sic!) tai tee vahvan tahtoinen päätös, että samat tiedot eri instansseista eivät saa muuttua, tai anna niiden muuttua periaatteen "kuka on viimeinen on oikeassa" mukaan.

Teknisestä näkökulmasta katson seuraavan järjestelmän mahdolliseksi:

  • Tallenna EventLog ja Snapshot objektimallin sijaan
  • Etsi muita ilmentymiä (lisää kaikkien ilmentymien päätepisteet asetuksiin? udp Discovery? Master/slave?)
  • Replikoi EventLog-ilmentymien välillä millä tahansa konsensusalgoritmilla, kuten RAFT.

On myös toinen ongelma, joka huolestuttaa minua - kaskadipoisto tai sellaisten objektien poistotapausten havaitseminen, joilla on linkkejä muista objekteista. 

Lähdekoodi

Jos olet lukenut tähän asti, sinun tarvitsee vain lukea koodi; se löytyy GitHubista:
https://github.com/DiverOfDark/ObjectRepository

Lähde: will.com

Lisää kommentti