ObjectRepository - .NET in-memory repository patroan foar jo thúsprojekten

Wêrom bewarje alle gegevens yn it ûnthâld?

Om webside- of backendgegevens op te slaan, sil de earste winsk fan 'e measte sûne minsken wêze om in SQL-database te kiezen. 

Mar soms komt de gedachte yn 't sin dat it gegevensmodel net geskikt is foar SQL: bygelyks by it bouwen fan in sykopdracht of sosjale grafyk moatte jo sykje nei komplekse relaasjes tusken objekten. 

De minste situaasje is as jo yn in team wurkje en in kollega net wit hoe't jo rappe fragen kinne bouwe. Hoefolle tiid hawwe jo bestege oan it oplossen fan N + 1-problemen en it bouwen fan ekstra yndeksen sadat de SELECT op 'e haadside yn in ridlike tiid soe foltôgje?

In oare populêre oanpak is NoSQL. Ferskate jierren lyn wie d'r in protte hype om dit ûnderwerp - foar elke handige gelegenheid hawwe se MongoDB ynset en wiene bliid mei de antwurden yn 'e foarm fan json-dokuminten (troch de manier, hoefolle krukken moasten jo ynfoegje fanwegen de sirkulêre keppelings yn 'e dokuminten?).

Ik stel foar om in oare, alternative metoade te besykjen - wêrom net besykje alle gegevens yn applikaasjeûnthâld op te slaan, periodyk te bewarjen yn willekeurige opslach (bestân, databank op ôfstân)? 

Unthâld is goedkeap wurden, en alle mooglike gegevens foar de measte lytse en middelgrutte projekten sille passe yn 1 GB ûnthâld. (Bygelyks, myn favorite thúsprojekt is finansjele tracker, dy't deistige statistiken en de skiednis fan myn útjeften, saldo's en transaksjes in jier en in heal byhâldt, ferbrûkt mar 45 MB ûnthâld.)

Pros:

  • Tagong ta gegevens wurdt makliker - jo hoege net te soargen oer queries, loai laden, ORM funksjes, jo wurkje mei gewoane C # objekten;
  • Der binne gjin problemen ferbûn mei tagong fan ferskate triedden;
  • Hiel fluch - gjin netwurkoanfragen, gjin oersetting fan koade yn in query-taal, gjin ferlet fan (de)serialisaasje fan objekten;
  • It is akseptabel om gegevens yn elke foarm op te slaan - itsij yn XML op skiif, as yn SQL Server, of yn Azure Table Storage.

Cons:

  • Horizontale skaalfergrutting is ferlern, en as gefolch kin nul downtime ynset net dien wurde;
  • As de applikaasje crasht, kinne jo gegevens foar in part ferlieze. (Mar ús applikaasje crasht noait, toch?)

Hoe wurket it?

It algoritme is as folgjend:

  • By it begjin wurdt in ferbining makke mei de gegevens opslach, en gegevens wurde laden;
  • In objektmodel, primêre yndeksen en relasjonele yndeksen (1: 1, 1: In protte) wurde boud;
  • In abonnemint wurdt makke foar feroarings yn objekteigenskippen (INotifyPropertyChanged) en foar it tafoegjen of fuortsmite fan eleminten oan 'e kolleksje (INotifyCollectionChanged);
  • As it abonnemint wurdt aktivearre, wurdt it feroare foarwerp tafoege oan 'e wachtrige foar it skriuwen nei de gegevensopslach;
  • Feroarings oan de opslach wurde periodyk bewarre (op in timer) yn in eftergrûntried;
  • As jo ​​de applikaasje ôfslute, wurde feroarings ek opslein yn 'e opslach.

Koade foarbyld

It tafoegjen fan de nedige ôfhinklikens

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

Wy beskriuwe it gegevensmodel dat sil wurde opslein yn 'e opslach

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

Dan it objektmodel:

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

En as lêste, de repositoryklasse sels foar tagong ta gegevens:

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

Meitsje in ObjectRepository-eksimplaar:

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

As it projekt HangFire sil brûke

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

In nij objekt ynfoegje:

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

Mei dizze oprop, it objekt ParentModel wurdt tafoege oan sawol de lokale cache as de wachtrige foar skriuwen nei de databank. Dêrom, dizze operaasje nimt O (1), en dit objekt kin wurde wurke mei fuortendaliks.

Bygelyks om dit objekt yn 'e repository te finen en te kontrolearjen dat it weromjûne objekt itselde eksimplaar is:

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

Wat bart dêr? Set () jout werom TableDictionary, dy't befettet ConcurrentDictionary en jout ekstra funksjonaliteit fan primêre en sekundêre yndeksen. Hjirmei kinne jo metoaden hawwe foar sykjen op Id (of oare willekeurige brûkersyndeksen) sûnder folslein iterearjen oer alle objekten.

By it tafoegjen fan objekten oan ObjectRepository in abonnemint wurdt tafoege om harren eigenskippen te feroarjen, sadat elke feroaring yn eigenskippen ek resultearret yn dit objekt wurdt tafoege oan de skriuwwachtrige. 
It bywurkjen fan eigenskippen fan bûten liket itselde as wurkje mei in POCO-objekt:

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

Jo kinne in objekt op 'e folgjende manieren wiskje:

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

Dit foeget it objekt ek ta oan de wiskwachtrige.

Hoe wurket besparjen?

ObjectRepository wannear't kontrolearre objekten feroarje (it tafoegjen of wiskje, of eigenskippen feroarjen), bringt in evenemint op Model feroareynskreaun op Istorage. Implementaasjes Istorage as in evenemint bart Model feroare wizigingen wurde pleatst yn 3 wachtrijen - foar tafoegjen, bywurkjen en wiskjen.

Ek ymplemintaasjes Istorage by inisjalisaasje, se meitsje in timer dy't feroarsaket feroarings wurde bewarre elke 5 sekonden. 

Derneist is d'r in API om in opslach op te slaan: ObjectRepository.Save().

Foar elke opslach wurde betsjuttingsleaze operaasjes earst út 'e wachtrijen fuortsmiten (bygelyks dûbele eveneminten - as in objekt twa kear feroare is of gau objekten tafoege / fuortsmiten is), en allinich dan it bewarjen sels. 

Yn alle gefallen wurdt it hiele aktuele objekt bewarre, sadat it mooglik is dat objekten yn in oare folchoarder bewarre wurde as se binne feroare, ynklusyf nijere ferzjes fan objekten as op it stuit dat se oan 'e wachtrige tafoege binne.

Wat is der noch mear?

  • Alle bibleteken binne basearre op .NET Standert 2.0. Kin brûkt wurde yn alle moderne .NET-projekten.
  • De API is thread feilich. Ynterne kolleksjes wurde útfierd basearre op ConcurrentDictionary, barrenshannelers hawwe slûzen of hawwe se net nedich. 
    It iennichste ding om te ûnthâlden is om te skiljen ObjectRepository.Save();
  • Willekeurige yndeksen (fereaskje unykens):

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

Wa brûkt it?

Persoanlik begon ik dizze oanpak te brûken yn alle hobbyprojekten, om't it handich is en gjin grutte útjeften nedich is foar it skriuwen fan in gegevenstagongslaach of it ynsetten fan swiere ynfrastruktuer. Persoanlik is it opslaan fan gegevens yn litedb as in bestân foar my meastentiids genôch. 

Mar yn it ferline, doe't de no-ôfbrutsen opstart EscapeTeams (Ik tocht hjir is it, jild - mar nee, wer ûnderfining) - brûkt om gegevens op te slaan yn Azure Table Storage.

Plannen foar de takomst

Ik wol ien fan 'e wichtichste neidielen fan dizze oanpak reparearje - horizontale skaalfergrutting. Om dit te dwaan, hawwe jo of ferdielde transaksjes nedich (sic!), Of meitsje in sterke wilsbeslút dat deselde gegevens fan ferskate eksimplaren net moatte feroarje, of lit se feroarje neffens it prinsipe "wa't lêste is rjocht."

Ut in technysk eachpunt sjoch ik it folgjende skema as mooglik:

  • Bewarje EventLog en Snapshot ynstee fan objektmodel
  • Fyn oare eksimplaren (foegje einpunten fan alle eksimplaren ta oan de ynstellings? udp-ûntdekking? master/slave?)
  • Replikearje tusken EventLog-eksimplaren fia elk konsensusalgoritme, lykas RAFT.

D'r is ek in oar probleem dat my soargen makket - kaskadeferwidering, of deteksje fan gefallen fan wiskjen fan objekten dy't keppelings hawwe fan oare objekten. 

Boarne

As jo ​​​​hierhinne lêzen hawwe, dan bliuwt alles oer de koade te lêzen; it kin fûn wurde op GitHub:
https://github.com/DiverOfDark/ObjectRepository

Boarne: www.habr.com

Add a comment