ObjectRepository - .NET memoriako biltegiaren eredua zure etxeko proiektuetarako

Zergatik gorde datu guztiak memorian?

Webgunearen edo backend datuak gordetzeko, pertsona sano gehienen lehen nahia SQL datu-base bat aukeratzea izango da. 

Baina batzuetan, datu-eredua SQLrako egokia ez dela pentsatzea etortzen zait burura: adibidez, bilaketa edo grafiko sozial bat eraikitzean, objektuen arteko erlazio konplexuak bilatu behar dituzu. 

Egoerarik okerrena taldean lan egiten duzunean eta lankide batek ez dakiela kontsulta azkarrak eraikitzen. Zenbat denbora eman zenuen N+1 problemak konpontzen eta indize gehigarriak eraikitzen, orrialde nagusiko SELECTa zentzuzko denbora-tarte batean osatzeko?

Beste ikuspegi ezagun bat NoSQL da. Duela hainbat urte gai honen inguruan zirrara handia zegoen - edozein momentutan MongoDB zabaldu zuten eta pozik zeuden erantzunekin json dokumentuen moduan. (bide batez, zenbat makulu sartu behar izan dituzu dokumentuetako esteka zirkularengatik?).

Beste metodo alternatibo bat probatzea proposatzen dut - zergatik ez saiatu datu guztiak aplikazioaren memorian gordetzen, aldian-aldian ausazko biltegiratze batean (fitxategia, urruneko datu-basea) gordetzen? 

Memoria merke bihurtu da, eta proiektu txiki eta ertain gehienetarako datu posible guztiak 1 GBko memorian sartuko dira. (Adibidez, nire etxeko proiekturik gogokoena da finantza-jarraitzailea, nire gastuen, saldoen eta transakzioen eguneroko estatistikak eta historia gordetzen dituena urte eta erdiz, 45 MB-ko memoria baino ez du kontsumitzen.)

Pros:

  • Datuetarako sarbidea errazagoa da - ez duzu kontsultak, karga alferrak, ORM funtzioak kezkatu beharrik, C# objektu arruntekin lan egiten duzu;
  • Ez dago hari ezberdinetatik sarbidearekin lotutako arazorik;
  • Oso azkarra - ez da sare-eskaerarik, ez dago kodea kontsulta-hizkuntza batera itzuli, ez da objektuen (des)serializatu behar;
  • Onargarria da datuak edozein formatan gordetzea, izan XML diskoan, edo SQL Server-en, edo Azure Table Storage-n.

Cons:

  • Eskalatze horizontala galtzen da, eta, ondorioz, ezin da zero geldialdi-denbora inplementatu;
  • Aplikazioa huts egiten bada, baliteke datuak partzialki galtzea. (Baina gure aplikazioa ez da inoiz huts egiten, ezta?)

Nola funtzionatzen du?

Algoritmoa hau da:

  • Hasieran, konexio bat ezartzen da datu biltegiarekin, eta datuak kargatzen dira;
  • Objektu-eredu bat, indize primarioak eta indize erlazionalak (1:1, 1:Asko) eraikitzen dira;
  • Objektuen propietateetan (INotifyPropertyChanged) aldaketetarako eta bildumari elementuak gehitzeko edo kentzeko (INotifyCollectionChanged) harpidetza bat sortzen da;
  • Harpidetza abiarazten denean, aldatutako objektua ilarara gehitzen da datuen biltegian idazteko;
  • Biltegiratzeko aldaketak aldian-aldian gordetzen dira (tenporizadore batean) atzeko hari batean;
  • Aplikaziotik irteten zarenean, aldaketak biltegian ere gordetzen dira.

Kode Adibidea

Beharrezko menpekotasunak gehitzea

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

Biltegian gordeko den datu-eredua deskribatzen dugu

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

Ondoren, objektuaren eredua:

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

Eta azkenik, biltegiaren klasea bera datuak sartzeko:

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

Sortu ObjectRepository instantzia bat:

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

Proiektuak HangFire erabiliko badu

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

Objektu berri bat txertatuz:

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

Dei honekin, objektua Guraso eredua cache lokalean eta datu-basean idazteko ilaran gehitzen da. Beraz, eragiketa honek O(1) hartzen du, eta objektu honekin berehala lan egin daiteke.

Adibidez, objektu hau biltegian aurkitzeko eta itzulitako objektua instantzia bera dela egiaztatzeko:

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

Zer gertatzen ari da? Ezarri () itzultzen TaulaDictionary, daukana AldiberekoHiztegia eta lehen eta bigarren indizeen funtzionalitate gehigarriak eskaintzen ditu. Honi esker, Id (edo beste erabiltzaile-indize arbitrario batzuen) bilaketa-metodoak izan ditzakezu objektu guztien gainean guztiz errepikatu gabe.

Objektuak gehitzean ObjectRepository haien propietateak aldatzeko harpidetza bat gehitzen da, beraz, propietateen edozein aldaketa ere objektu hau idazketa-ilarara gehitzea dakar. 
Kanpotik propietateak eguneratzeak POCO objektu batekin lan egitearen itxura bera du:

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

Modu hauetan objektu bat ezaba dezakezu:

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

Honek objektua ezabatze-ilarara ere gehitzen du.

Nola funtzionatzen du aurrezteak?

ObjectRepository kontrolatutako objektuak aldatzen direnean (gehitu edo ezabatuz, edo propietateak aldatuz), gertaera bat sortzen du Eredua aldatu daharpidetuta ISbiltegiratzea. Ezarpenak ISbiltegiratzea gertaera bat gertatzen denean Eredua aldatu da aldaketak 3 ilaratan jartzen dira - gehitzeko, eguneratzeko eta ezabatzeko.

Inplementazioak ere ISbiltegiratzea hasieratzerakoan, aldaketak 5 segundoro gordetzea eragiten duen tenporizadorea sortzen dute. 

Horrez gain, API bat dago gordetzeko deia behartzeko: ObjectRepository.Save().

Gorde bakoitzaren aurretik, zentzurik gabeko eragiketak lehenik ilaretatik kentzen dira (adibidez, bikoiztutako gertaerak - objektu bat bi aldiz aldatzen denean edo objektuak azkar gehitu/kentzen direnean), eta gero bakarrik gorde egiten da. 

Kasu guztietan, uneko objektu osoa gordetzen da, beraz, baliteke objektuak aldatu ziren beste ordena batean gordetzea, ilaran gehitu ziren unean baino objektuen bertsio berriagoak barne.

Zer gehiago dago?

  • Liburutegi guztiak .NET Standard 2.0-n oinarritzen dira. Edozein .NET proiektu modernotan erabil daiteke.
  • APIa hari segurua da. Barne-bilketak oinarrituta ezartzen dira AldiberekoHiztegia, gertaeren kudeatzaileek blokeoak dituzte edo ez dituzte behar. 
    Gogoratu beharreko gauza bakarra deitzea da ObjectRepository.Gorde();
  • Indize arbitrarioak (bakartasuna eskatzen dute):

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

Nork erabiltzen du?

Pertsonalki, ikuspegi hau zaletasun-proiektu guztietan erabiltzen hasi nintzen erosoa delako eta ez duelako gastu handirik behar datuetarako sarbide-geruza bat idazteko edo azpiegitura astunak zabaltzeko. Pertsonalki, litedb edo fitxategi batean datuak gordetzea nahikoa da normalean niretzat. 

Baina iraganean, orain desagertutako EscapeTeams startup-ak (Pentsatu nuen hemen dagoela, dirua, baina ez, berriro esperientzia) - Azure Table Storage-n datuak gordetzeko erabiltzen da.

Etorkizunerako planak

Ikuspegi honen desabantaila nagusietako bat konpondu nahiko nuke - eskalatze horizontala. Horretarako, transakzio banatuak behar dituzu (sic!), edo instantzia ezberdinetako datu berdinak ez aldatzeko erabakia hartu behar duzu, edo "azkena nork du arrazoia" printzipioaren arabera aldatzen utzi.

Ikuspuntu teknikotik honako eskema posible ikusten dut:

  • Gorde EventLog eta Snapshot objektu-ereduaren ordez
  • Bilatu beste instantzia batzuk (gehitu instantzia guztien amaiera-puntuak ezarpenetara? udp aurkikuntza? maisua/esklaboa?)
  • Erreplikatu EventLog instantzien artean edozein adostasun algoritmoaren bidez, hala nola RAFT.

Kezkatzen nauen beste arazo bat ere badago: kaskadako ezabatzea edo beste objektu batzuetatik estekak dituzten objektuak ezabatzeko kasuak detektatzea. 

Iturburu kodea

Honaino irakurri baduzu, kodea irakurtzea besterik ez da geratzen; GitHub-en aurki daiteke:
https://github.com/DiverOfDark/ObjectRepository

Iturria: www.habr.com

Gehitu iruzkin berria