ObjectRepository - .NET in-memory repository mønster til dine hjemmeprojekter

Hvorfor gemme alle data i hukommelsen?

For at gemme hjemmeside- eller backend-data vil det første ønske fra de fleste fornuftige mennesker være at vælge en SQL-database. 

Men nogle gange kommer tanken i tankerne om, at datamodellen ikke er egnet til SQL: for eksempel, når du bygger en søgning eller social graf, skal du søge efter komplekse relationer mellem objekter. 

Den værste situation er, når du arbejder i et team, og en kollega ikke ved, hvordan man opbygger hurtige forespørgsler. Hvor meget tid brugte du på at løse N+1-problemer og opbygge yderligere indekser, så SELECT på hovedsiden ville fuldføres inden for rimelig tid?

En anden populær tilgang er NoSQL. For adskillige år siden var der en masse hype omkring dette emne - til enhver passende lejlighed implementerede de MongoDB og var glade for svarene i form af json-dokumenter (hvor mange krykker skulle du i øvrigt indsætte på grund af de cirkulære links i dokumenterne?).

Jeg foreslår, at du prøver en anden, alternativ metode - hvorfor ikke prøve at gemme alle data i applikationshukommelsen og periodisk gemme dem til tilfældig lagring (fil, fjerndatabase)? 

Hukommelsen er blevet billig, og enhver mulig data til de fleste små og mellemstore projekter vil passe ind i 1 GB hukommelse. (For eksempel er mit yndlings hjemmeprojekt finansiel tracker, som gemmer daglig statistik og historik over mine udgifter, saldi og transaktioner i halvandet år, bruger kun 45 MB hukommelse.)

Teknikere:

  • Adgang til data bliver lettere - du behøver ikke bekymre dig om forespørgsler, doven indlæsning, ORM-funktioner, du arbejder med almindelige C#-objekter;
  • Der er ingen problemer forbundet med adgang fra forskellige tråde;
  • Meget hurtigt - ingen netværksanmodninger, ingen oversættelse af kode til et forespørgselssprog, intet behov for (af)serialisering af objekter;
  • Det er acceptabelt at gemme data i enhver form - det være sig i XML på disk, eller i SQL Server eller i Azure Table Storage.

Ulemper:

  • Horisontal skalering går tabt, og som følge heraf kan nul nedetidsimplementering ikke udføres;
  • Hvis programmet går ned, kan du delvist miste data. (Men vores applikation går aldrig ned, vel?)

Hvordan fungerer det?

Algoritmen er følgende:

  • Ved starten etableres en forbindelse med datalageret, og data indlæses;
  • En objektmodel, primære indekser og relationelle indekser (1:1, 1:Mange) bygges;
  • Der oprettes et abonnement for ændringer i objektegenskaber (INotifyPropertyChanged) og for tilføjelse eller fjernelse af elementer til samlingen (INotifyCollectionChanged);
  • Når abonnementet udløses, tilføjes det ændrede objekt til køen for skrivning til datalageret;
  • Ændringer i lageret gemmes med jævne mellemrum (på en timer) i en baggrundstråd;
  • Når du afslutter programmet, gemmes ændringer også på lageret.

Kode eksempel

Tilføjelse af de nødvendige afhængigheder

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

Vi beskriver den datamodel, der vil blive lagret i lageret

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

Så objektmodellen:

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

Og endelig, selve depotklassen til at få adgang til data:

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

Opret en ObjectRepository-instans:

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

Hvis projektet vil bruge HangFire

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

Indsættelse af et nyt objekt:

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

Med dette opkald vil objektet Forældremodel føjes til både den lokale cache og køen for at skrive til databasen. Derfor tager denne operation O(1), og der kan arbejdes med dette objekt med det samme.

For at finde dette objekt i lageret og kontrollere, at det returnerede objekt er den samme forekomst:

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

Hvad sker der? Sæt () vender tilbage Tabelordbog, som indeholder ConcurrentDictionary og giver yderligere funktionalitet af primære og sekundære indekser. Dette giver dig mulighed for at have metoder til at søge efter Id (eller andre vilkårlige brugerindekser) uden at gentage alle objekter fuldstændigt.

Når du tilføjer objekter til ObjectRepository et abonnement tilføjes for at ændre deres egenskaber, så enhver ændring i egenskaber resulterer også i, at dette objekt tilføjes til skrivekøen. 
Opdatering af egenskaber udefra ser det samme ud som at arbejde med et POCO-objekt:

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

Du kan slette et objekt på følgende måder:

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

Dette føjer også objektet til slettekøen.

Hvordan fungerer besparelsen?

ObjectRepository når overvågede objekter ændres (enten tilføjer eller sletter eller ændrer egenskaber), rejser en hændelse ModelÆndretabonnerer på Opbevaring. Implementeringer Opbevaring når en begivenhed indtræffer ModelÆndret ændringer sættes i 3 køer - til tilføjelse, opdatering og sletning.

Også implementeringer Opbevaring ved initialisering opretter de en timer, der får ændringer til at blive gemt hvert 5. sekund. 

Derudover er der en API til at tvinge et gemt opkald: ObjectRepository.Save().

Før hver lagring fjernes først meningsløse handlinger fra køerne (f.eks. duplikerede hændelser - når et objekt blev ændret to gange eller hurtigt tilføjede/fjernede objekter), og først derefter selve lagringen. 

I alle tilfælde gemmes hele det aktuelle objekt, så det er muligt, at objekter gemmes i en anden rækkefølge, end de blev ændret, inklusive nyere versioner af objekter end på det tidspunkt, de blev tilføjet til køen.

Hvad er der ellers?

  • Alle biblioteker er baseret på .NET Standard 2.0. Kan bruges i ethvert moderne .NET-projekt.
  • API'et er trådsikkert. Interne indsamlinger implementeres ud fra ConcurrentDictionary, hændelseshandlere har enten låse eller har ikke brug for dem. 
    Det eneste der er værd at huske er at ringe ObjectRepository.Save();
  • Vilkårlige indekser (kræver unikhed):

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

Hvem bruger det?

Personligt begyndte jeg at bruge denne tilgang i alle hobbyprojekter, fordi den er praktisk og ikke kræver store udgifter til at skrive et dataadgangslag eller installere tung infrastruktur. Personligt er det normalt nok for mig at gemme data i litedb eller en fil. 

Men tidligere, da det nu hedengangne ​​startup EscapeTeams (Jeg troede her er det, penge - men nej, oplevelse igen) - bruges til at gemme data i Azure Table Storage.

Planer for fremtiden

Jeg vil gerne rette en af ​​de største ulemper ved denne tilgang - horisontal skalering. For at gøre dette har du brug for enten distribuerede transaktioner (sic!), eller træffe en viljestærk beslutning om, at de samme data fra forskellige instanser ikke skal ændres, eller lade dem ændre sig i henhold til princippet "hvem der er sidst har ret."

Fra et teknisk synspunkt ser jeg følgende ordning mulig:

  • Gem EventLog og Snapshot i stedet for objektmodel
  • Find andre forekomster (tilføj slutpunkter for alle forekomster til indstillingerne? udp-opdagelse? master/slave?)
  • Repliker mellem EventLog-instanser via enhver konsensusalgoritme, såsom RAFT.

Der er også et andet problem, der bekymrer mig - kaskade sletning, eller påvisning af tilfælde af sletning af objekter, der har links fra andre objekter. 

Kildekode

Hvis du har læst hele vejen hertil, så er der kun tilbage at læse koden; den kan findes på GitHub:
https://github.com/DiverOfDark/ObjectRepository

Kilde: www.habr.com

Tilføj en kommentar