ObjectRepository - .NET In-Memory Repository Muster fir Är Heemprojeten

Firwat späicheren all d'Donnéeën an Erënnerung?

Fir Websäit oder Backenddaten ze späicheren, ass den éischte Wonsch vun de meeschte vernünfteg Leit eng SQL Datebank ze wielen. 

Awer heiansdo kënnt de Gedanken datt den Datemodell net fir SQL gëeegent ass: zum Beispill, wann Dir eng Sich oder sozial Grafik baut, musst Dir no komplexe Bezéiungen tëscht Objeten sichen. 

Déi schlëmmste Situatioun ass wann Dir an engem Team schafft an e Kolleg net weess wéi séier Ufroe bauen. Wéi vill Zäit hutt Dir verbruecht fir N+1 Probleemer ze léisen an zousätzlech Indexen ze bauen fir datt de SELECT op der Haaptsäit an enger vernünftlecher Zäit fäerdeg ass?

Eng aner populär Approche ass NoSQL. Virun e puer Joer gouf et vill Hype ronderëm dëst Thema - fir all praktesch Geleeënheet hunn se MongoDB ofgesat a ware frou mat den Äntwerten a Form vun json Dokumenter (iwwregens, wéivill Krutchen hues du missen asetzen wéinst de kreesfërmegen Linken an den Dokumenter?).

Ech proposéieren eng aner, alternativ Method ze probéieren - firwat probéiert net all d'Donnéeën an der Applikatiounspäicherung ze späicheren, periodesch op zoufälleg Späichere späicheren (Datei, Remote-Datebank)? 

D'Erënnerung ass bëlleg ginn, an all méiglech Daten fir déi meescht kleng a mëttelgrouss Projete passen an 1 GB Erënnerung. (Zum Beispill, mäi Liiblingsheemprojet ass finanziell Tracker, déi alldeeglech Statistiken an d'Geschicht vu menge Ausgaben, Salden an Transaktioune fir e Joer an en halleft hält, verbraucht nëmmen 45 MB Erënnerung.)

Pros:

  • Zougang zu Daten gëtt méi einfach - Dir musst Iech keng Suergen iwwer Ufroen, Lazy Luede, ORM Features, Dir schafft mat gewéinleche C # Objeten;
  • Et gi keng Problemer mat Zougang aus verschiddene thread assoziéiert;
  • Ganz séier - keng Reseau Ufroen, keng Iwwersetzung vum Code an eng Ufro Sprooch, kee Besoin fir (De) Serialiséierung vun Objeten;
  • Et ass akzeptabel Daten an iergendenger Form ze späicheren - sief et an XML op Disk, oder am SQL Server, oder an Azure Table Storage.

Muecht:

  • Horizontal Skaléieren ass verluer, an als Resultat kann null Ausbroch Deployment net gemaach ginn;
  • Wann d'Applikatioun crasht, kënnt Dir deelweis Daten verléieren. (Awer eis Applikatioun crasht ni, richteg?)

Wéi heescht et schaffen?

Den Algorithmus ass wéi follegt:

  • Am Ufank gëtt eng Verbindung mat der Datelagerung etabléiert, an d'Date ginn gelueden;
  • En Objektmodell, primär Indizes a relational Indizes (1: 1, 1: Vill) gi gebaut;
  • En Abonnement gëtt erstallt fir Ännerungen an Objekteigenschaften (INotifyPropertyChanged) a fir Elementer an d'Kollektioun ze addéieren oder ze läschen (INotifyCollectionChanged);
  • Wann d'Abonnement ausgeléist gëtt, gëtt de geännerten Objet an d'Schlaang bäigefüügt fir an d'Datelagerung ze schreiwen;
  • Ännerunge fir d'Späichere ginn periodesch (op engem Timer) an engem Background thread gespäichert;
  • Wann Dir d'Applikatioun verléisst, ginn d'Ännerungen och an der Späichere gespäichert.

Code Beispill

Dobäizemaachen déi néideg Ofhängegkeeten

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

Mir beschreiwen den Datemodell deen an der Späichere gespäichert gëtt

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

Dann den Objektmodell:

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

A schliisslech ass d'Repository Klass selwer fir Zougang zu Daten:

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

Erstellt eng ObjectRepository Instanz:

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

Wann de Projet HangFire benotzt

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

En neien Objet setzen:

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

Mat dësem Opruff, den Objet Elterenmodell gëtt souwuel am lokalen Cache wéi och an der Schlaang bäigefüügt fir an d'Datebank ze schreiwen. Dofir hëlt dës Operatioun O (1), an dësem Objet kann direkt geschafft ginn.

Zum Beispill, fir dësen Objet am Repository ze fannen an z'iwwerpréiwen datt de zréckginn Objet déiselwecht Instanz ass:

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

Wat geschitt dann? Setzt () geet zréck TableDictionary, déi enthält Concurrent Dictionary a bitt zousätzlech Funktionalitéit vu primären a sekundären Indexen. Dëst erlaabt Iech Methoden ze hunn fir no Id ze sichen (oder aner arbiträr Benotzerindexe) ouni komplett iwwer all Objekter ze iteréieren.

Wann Dir Objete bäidréit ObjectRepository en Abonnement gëtt bäigefüügt fir hir Eegeschaften z'änneren, sou datt all Ännerung vun Eegeschaften och dozou féiert datt dësen Objet an d'Schreifschlaang bäigefüügt gëtt. 
D'Aktualiséierung vun Eegeschafte vu baussen gesäit d'selwecht aus wéi mat engem POCO Objet ze schaffen:

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

Dir kënnt en Objet op de folgende Weeër läschen:

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

Dëst füügt och den Objet un d'Läschschlaang.

Wéi funktionéiert Spueren?

ObjectRepository wann iwwerwaacht Objete änneren (entweder derbäigesat oder läschen, oder Ännerung vun Eegeschafte), erhéicht en Event Modell geännertabonnéiert op ISlagerung. Ëmsetzungen ISlagerung wann en Event geschitt Modell geännert Ännerungen ginn an 3 Schlaangen gesat - fir dobäizemaachen, fir ze aktualiséieren a fir ze läschen.

Och Implementatiounen ISlagerung op initialization, si schafen eng Timer datt Ännerungen all 5 Sekonnen gerett ginn. 

Zousätzlech gëtt et eng API fir e Späicherruff ze zwéngen: ObjectRepository.Save().

Virun all späicheren sinn sënnlos Operatiounen fir d'éischt aus de Schlaangen geläscht (zum Beispill duplizéiert Eventer - wann en Objet zweemol geännert gouf oder séier Objete bäigefüügt / geläscht gouf), an nëmmen dann d'Späichere selwer. 

An alle Fäll gëtt de ganzen aktuellen Objet gespäichert, sou datt et méiglech ass datt d'Objete an enger anerer Uerdnung gespäichert ginn wéi se geännert goufen, och méi nei Versioune vun Objeten wéi an der Zäit wou se an d'Schlaang bäigefüügt goufen.

Wat gëtt et nach?

  • All Bibliothéike baséieren op .NET Standard 2.0. Kann an all modernen .NET Projet benotzt ginn.
  • D'API ass thread sécher. Intern Kollektiounen sinn ëmgesat baséiert op Concurrent Dictionary, Event Handler hunn entweder Spären oder brauchen se net. 
    Dat eenzegt wat et wäert erënneren ass ze ruffen ObjectRepository.Save();
  • Arbiträr Indizes (erfuerderen Eenzegaartegkeet):

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

Wien benotzt et?

Perséinlech hunn ech ugefaang dës Approche an all Hobbyprojeten ze benotzen, well et praktesch ass a keng grouss Ausgaben erfuerdert fir eng Datenzougangschicht ze schreiwen oder schwéier Infrastruktur z'installéieren. Perséinlech ass d'Späichere vun Daten an litedb oder enger Datei normalerweis genuch fir mech. 

Awer an der Vergaangenheet, wann den elo defunkten Startup EscapeTeams (Ech geduecht hei ass et, Suen - awer nee, Erfahrung erëm) - benotzt fir Daten an Azure Table Storage ze späicheren.

Pläng fir d'Zukunft

Ech wéilt ee vun den Haapt Nodeeler vun dëser Approche fixéieren - horizontale Skala. Fir dëst ze maachen, braucht Dir entweder verdeelt Transaktiounen (sic!), Oder maacht eng staark Wëllen Entscheedung datt déiselwecht Donnéeën aus verschiddenen Instanzen net sollten änneren, oder loosst se nom Prinzip änneren "Wien ass lescht ass richteg."

Vun enger technescher Siicht gesinn ech de folgende Schema wéi méiglech:

  • Store EventLog a Snapshot amplaz vum Objektmodell
  • Fannt aner Instanzen (Füügt Endpunkte vun all Instanzen un d'Astellungen? udp Entdeckung? Master / Sklave?)
  • Replizéiert tëscht EventLog Instanzen iwwer all Konsens Algorithmus, sou wéi RAFT.

Et gëtt och en anere Problem, dee mech Suergen mécht - Kaskadeläsche, oder Detektioun vu Fäll vu Läsch vun Objeten, déi Linke vun aneren Objeten hunn. 

Quelltext

Wann Dir de ganze Wee bis hei gelies hutt, da bleift alles de Code ze liesen; et kann op GitHub fonnt ginn:
https://github.com/DiverOfDark/ObjectRepository

Source: will.com

Setzt e Commentaire