ObjectRepository - .NET en-memora deponejo ŝablono por viaj hejmaj projektoj

Kial konservi ĉiujn datumojn en memoro?

Por stoki retejojn aŭ malantaŭajn datumojn, la unua deziro de plej prudentaj homoj estos elekti SQL-datumbazon. 

Sed foje venas al la menso la penso, ke la datummodelo ne taŭgas por SQL: ekzemple, dum konstruado de serĉo aŭ socia grafikaĵo, vi devas serĉi kompleksajn rilatojn inter objektoj. 

La plej malbona situacio estas kiam vi laboras en teamo kaj kolego ne scias kiel konstrui rapidajn demandojn. Kiom da tempo vi elspezis solvante N+1-problemojn kaj konstrui pliajn indeksojn por ke la SELECT sur la ĉefpaĝo kompletigus en akceptebla tempo?

Alia populara aliro estas NoSQL. Antaŭ pluraj jaroj estis multe da ekzaltiĝo ĉirkaŭ ĉi tiu temo - por iu ajn oportuna okazo ili deplojis MongoDB kaj estis feliĉaj kun la respondoj en la formo de json-dokumentoj. (cetere, kiom da lambastonoj vi devis enmeti pro la cirklaj ligiloj en la dokumentoj?).

Mi sugestas provi alian, alternativan metodon - kial ne provi konservi ĉiujn datumojn en aplika memoro, periode konservante ĝin al hazarda stokado (dosiero, fora datumbazo)? 

Memoro fariĝis malmultekosta, kaj ĉiuj eblaj datumoj por la plej multaj malgrandaj kaj mezgrandaj projektoj konvenos en 1 GB da memoro. (Ekzemple, mia plej ŝatata hejma projekto estas financa spuristo, kiu konservas ĉiutagajn statistikojn kaj historion de miaj elspezoj, ekvilibroj kaj transakcioj dum jaro kaj duono, konsumas nur 45 MB da memoro.)

Pros:

  • Aliro al datumoj plifaciliĝas - vi ne bezonas zorgi pri demandoj, maldiligenta ŝarĝo, ORM-funkcioj, vi laboras kun ordinaraj C#-objektoj;
  • Ne estas problemoj asociitaj kun aliro de malsamaj fadenoj;
  • Tre rapide - neniuj retaj petoj, neniu tradukado de kodo en demandolingvon, neniu bezono de (de)seriigo de objektoj;
  • Estas akcepteble konservi datumojn en ajna formo - ĉu en XML sur disko, aŭ en SQL-Servilo, aŭ en Azure Table Storage.

Kons:

  • Horizontala skalo estas perdita, kaj kiel rezulto, nula malfunkciodeplojo ne povas esti farita;
  • Se la aplikaĵo frakasas, vi eble parte perdos datumojn. (Sed nia aplikaĵo neniam kraŝas, ĉu ne?)

Kiel ĝi funkcias?

La algoritmo estas kiel sekvas:

  • Komence, konekto estas establita kun la datumstokado, kaj datumoj estas ŝarĝitaj;
  • Objektmodelo, primaraj indeksoj kaj interrilataj indeksoj (1:1, 1:Multaj) estas konstruitaj;
  • Abono estas kreita por ŝanĝoj en objektopropraĵoj (INotifyPropertyChanged) kaj por aldoni aŭ forigi elementojn al la kolekto (INotifyCollectionChanged);
  • Kiam la abono estas ekigita, la ŝanĝita objekto estas aldonita al la atendovico por skribi al la datumstokado;
  • Ŝanĝoj al la stokado periode estas konservitaj (sur tempigilo) en fonfadeno;
  • Kiam vi forlasas la aplikaĵon, ŝanĝoj ankaŭ estas konservitaj en la stokado.

Ekzemplo de kodo

Aldonante la necesajn dependecojn

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

Ni priskribas la datummodelon, kiu estos stokita en la stokado

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

Tiam la objekta modelo:

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

Kaj finfine, la deponejo mem por aliri datumojn:

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

Kreu ekzemplon de ObjectRepository:

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

Se la projekto uzos HangFire

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

Enmetante novan objekton:

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

Kun ĉi tiu voko, la objekto Gepatromodelo estas aldonita kaj al la loka kaŝmemoro kaj la vosto por skribi al la datumbazo. Tial, ĉi tiu operacio prenas O(1), kaj ĉi tiu objekto povas esti laborita kun tuj.

Ekzemple, por trovi ĉi tiun objekton en la deponejo kaj kontroli, ke la redonita objekto estas la sama okazo:

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

Kio okazas? Agordu () revenas Tabelvortaro, kiu enhavas Samtempa Vortaro kaj disponigas kroman funkciecon de primaraj kaj sekundaraj indeksoj. Ĉi tio ebligas al vi havi metodojn por serĉi per Id (aŭ aliaj arbitraj uzantindeksoj) sen tute ripetadi super ĉiuj objektoj.

Aldonante objektojn al ObjektoDeponejo abono estas aldonita por ŝanĝi iliajn trajtojn, do ĉiu ŝanĝo en propraĵoj ankaŭ rezultigas, ke ĉi tiu objekto estas aldonita al la skribvico. 
Ĝisdatigi ecojn de ekstere aspektas same kiel labori kun POCO-objekto:

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

Vi povas forigi objekton en la jenaj manieroj:

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

Ĉi tio ankaŭ aldonas la objekton al la vicovico.

Kiel funkcias ŝparado?

ObjektoDeponejo kiam monitoritaj objektoj ŝanĝiĝas (aŭ aldonante aŭ forigante, aŭ ŝanĝante trajtojn), levas eventon Modelo Ŝanĝitaabonis ISstokado. Efektivigoj ISstokado kiam okazas evento Modelo Ŝanĝita ŝanĝoj estas metitaj en 3 atendovicojn - por aldoni, por ĝisdatigi kaj por forigi.

Ankaŭ efektivigoj ISstokado post inicialigo, ili kreas tempigilon, kiu igas ŝanĝojn konservi ĉiujn 5 sekundojn. 

Krome, ekzistas API por devigi konservi vokon: ObjektoDeponejo.Konservi ().

Antaŭ ĉiu konservado, sensignifaj operacioj unue estas forigitaj el la atendovicoj (ekzemple, duplikataj eventoj - kiam objekto estis ŝanĝita dufoje aŭ rapide aldonita/forigita objektoj), kaj nur tiam la konservado mem. 

En ĉiuj kazoj, la tuta aktuala objekto estas konservita, do eblas ke objektoj estas konservitaj en malsama ordo ol ili estis ŝanĝitaj, inkluzive de pli novaj versioj de objektoj ol tiutempe ili estis aldonitaj al la atendovico.

Kio alia estas tie?

  • Ĉiuj bibliotekoj baziĝas sur .NET Standard 2.0. Uzeblas en iu ajn moderna projekto .NET.
  • La API estas fadensekura. Internaj kolektoj estas efektivigitaj surbaze de Samtempa Vortaro, eventaj prizorgantoj aŭ havas serurojn aŭ ne bezonas ilin. 
    La nura afero memorinda estas voki ObjektoDeponejo.Konservi ();
  • Arbitraj indeksoj (postulas unikecon):

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

Kiu uzas ĝin?

Persone, mi komencis uzi ĉi tiun aliron en ĉiuj ŝatokupprojektoj ĉar ĝi estas oportuna kaj ne postulas grandajn elspezojn por verki datuman alirtavolon aŭ deploji pezan infrastrukturon. Persone, konservi datumojn en litedb aŭ dosiero kutime sufiĉas por mi. 

Sed en la pasinteco, kiam la nun malfunkcia starto EscapeTeams (Mi pensis ĉi tie, mono - sed ne, sperto denove) - uzata por konservi datumojn en Azure Table Storage.

Planoj por la estonteco

Mi ŝatus ripari unu el la ĉefaj malavantaĝoj de ĉi tiu aliro - horizontala skalo. Por fari tion, vi bezonas aŭ distribuitajn transakciojn (sic!), aŭ fari fortevolan decidon, ke la samaj datumoj de malsamaj okazoj ne ŝanĝiĝu, aŭ lasi ilin ŝanĝi laŭ la principo "kiu estas la lasta pravas."

De teknika vidpunkto, mi vidas la jenan skemon kiel eble:

  • Stoku EventLog kaj Snapshot anstataŭ objekta modelo
  • Trovu aliajn okazojn (aldonu finpunktojn de ĉiuj okazoj al la agordoj? udp-malkovro? majstro/sklavo?)
  • Repliku inter EventLog-kazoj per iu ajn konsenta algoritmo, kiel RAFT.

Estas ankaŭ alia problemo, kiu maltrankviligas min - kaskada forigo, aŭ detekto de kazoj de forigo de objektoj, kiuj havas ligilojn de aliaj objektoj. 

Fontkodo

Se vi legis ĝis ĉi tie, tiam restas nur legi la kodon; ĝi troveblas en GitHub:
https://github.com/DiverOfDark/ObjectRepository

fonto: www.habr.com

Aldoni komenton