ObjectRepository — .NET in-memory repository minta az otthoni projektjeidhez

Miért tároljuk az összes adatot a memóriában?

A webhely vagy a backend adatainak tárolásához a legtöbb józan észű ember az SQL adatbázist választaná. 

De néha eszébe jut, hogy az adatmodell nem megfelelő az SQL-hez: például, amikor keresést építünk vagy társadalmi grafikont, bonyolult kapcsolatokon kell keresni az objektumok között. 

A legrosszabb helyzet, amikor csapatban dolgozik, és a kolléga nem tud gyors lekérdezéseket építeni. Mennyi időt töltött az N+1 problémák megoldásával és kiegészítő indexek létrehozásával, hogy a SELECT a főoldalon ésszerű idő alatt végrehajtható legyen?

Egy másik népszerű megközelítés a NoSQL. Néhány évvel ezelőtt hatalmas hype volt körülötte – bármilyen kényelmes alkalommal beüzemelték a MongoDB-t és örültek a JSON dokumentumok formájában kapott válaszoknak. (Amúgy mennyi akadályt kellett beilleszteni a dokumentumok ciklikus hivatkozásai miatt?).

Javaslom, hogy próbáljunk ki egy másik, alternatív módszert – miért ne tárolnánk az összes adatot az alkalmazás memóriájában, időnként mentve egy tetszőleges tárolóba (fájl, távoli adatbázis)? 

A memória olcsóvá vált, és a legtöbb kis- és középprojekt minden lehetséges adata elfér 1 GB memóriában. (Például, a kedvenc otthoni projektem – pénzügyi nyilvántartó, amely napi statisztikát és a költéseim, egyenlegeim és tranzakcióim történetét vezeti másfél évre visszatekintve, mindössze 45 MB memóriát használ.)

Előnyök:

  • Az adatokhoz való hozzáférés egyszerűbbé válik – nem kell aggódni a lekérdezések, a késleltetett betöltés, az ORM sajátosságai miatt, a munkavégzés a szokásos C# objektumokkal történik;
  • Nincsenek problémák a különböző szálak közötti hozzáféréssel;
  • Nagyon gyors – nincsenek hálózati lekérdezések, hiányzik a kód nyelvre fordítása, nem szükséges az objektumok (de)szerializálása;
  • Elfogadható bármilyen formában tárolni az adatokat – akár XML-ben a lemezen, akár SQL Server-ben, akár Azure Table Storage-ban.

Hátrányok:

  • Elvész a vízszintes skálázás lehetősége, így nem lehet zero downtime deployment-et végrehajtani;
  • Ha az alkalmazás leáll – részben elveszíthetjük az adatokat. (De hiszen az alkalmazásunk soha nem áll le, igaz?)

Hogyan működik ez?

Az algoritmus a következő:

  • Az indításkor kapcsolat létesül az adatbázissal, és az adatok betöltődnek;
  • Az objektummodell felépül, az elsődleges indexek és a kapcsolati indexek (1:1, 1:Many) létrejönnek.
  • Előfizetés készül az objektumok tulajdonságainak változásaira (INotifyPropertyChanged) és a gyűjteménybe történő elemek hozzáadására vagy eltávolítására (INotifyCollectionChanged);
  • Az előfizetés aktiválásakor a megváltozott objektum bekerül az adathalmozásra váró sorba;
  • Időszakonként (időzítő alapján) háttérfolyamatban mentjük a változásokat a tárolóba;
  • A programból való kilépéskor is mentjük a változásokat a tárolóba.

Példa kódra

Hozzáadjuk a szükséges függőségeket

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

Leírjuk az adatmodellt, amelyet menteni fogunk a tárolóba

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

Ezután az objektummodell:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Példa 1:Many kapcsolat
    public IEnumerable Children => Multiple(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);
    }
    
    // Index kereséssel elérhető
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Végül maga az adathozzáférési osztály:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Teszteléshez, lehetővé teszi a változások nem mentését az adatbázisba
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Ha Hangfire-t használnak, és a Hangfire adatmódelt kell tárolni az ObjectRepository-ban
        // this.RegisterHangfireScheme();  
    
        Initialize();
    }
}

Létrehozzuk az ObjectRepository példányát:

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

Ha a projektben HangFire-t fognak használni

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

Új objektum beszúrása:

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

Ezen hívás során az objektum ParentModel Hozzáadódik a helyi gyorsítótárhoz és az adatbázis írási sorához is. Ezért ez a művelet O(1) időt igényel, és ezzel az objektummal azonnal dolgozhatunk.

Például, hogy megkeressük ezt az objektumot a tárolóban, és megbizonyosodjunk arról, hogy a visszatérő objektum ugyanaz a példány:

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

Mi történik eközben? Set() a pénzeszközöket! Még dicséretet is kap 75-90% -tól a résztvevők közül, bár a projekt fejlődése nyilvánvalóan lelassult. A legmeglepőbb, hogy mindenki TableDictionary, amely tartalmazza ConcurrentDictionary és további funkciókat biztosít az elsődleges és másodlagos indexekhez. Ez lehetővé teszi az Id (vagy más önálló felhasználói indexek) szerinti keresést anélkül, hogy minden objektumot teljes mértékben átnéznénk.

Objektumok hozzáadásakor a ObjectRepository regisztrál egy figyelmet az objektumok tulajdonságainak megváltozására, így bármilyen tulajdonságváltozás is a megfelelő objektumot az író sorba küldi. 
A tulajdonságok külső frissítése ugyanúgy néz ki, mint a POCO-objektummal való munka:

myParent.Children.First().Property = "Frissített érték";

Az objektumot a következő módokon lehet eltávolítani:

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

Ekkor az objektumot is hozzáadják az eltávolítandó sorhoz.

Hogyan működik a mentés?

ObjectRepository az következő eseményeket aktiválja, ha megváltoztatják a nyomon követett objektumokat (akár hozzáadás, eltávolítás, akár a tulajdonságok módosítása): ModelChanged, amelyre feliratkozott IStorage. A megvalósítások a IStorage esemény bekövetkeztekor ModelChanged három sorba sorolják az eltéréseket — hozzáadásra, frissítésre és eltávolításra.

Továbbá, a megvalósítások inicializáláskor létrehoznak egy időzítőt, amely 5 másodpercenként hívja a mentést. IStorage Ezenkívül létezik egy API a mentés kényszerített végrehajtásához: 

ObjectRepository.Save() Minden mentés előtt a felesleges műveletek (például események duplikációi — amikor az objektum kétszer változott meg vagy gyors hozzáadás/eltávolítás történt) eltávolítása történik, és csak utána zajlik a mentés..

Minden esetben a teljes, aktuális objektumot mentik, ezért előfordulhat, hogy az objektumokat más sorrendben mentik, mint ahogy a változások történtek, beleértve azt is, hogy frissebb verziók mentésre kerülhetnek, mint amik a sorba kerültek. 

Minden könyvtár a .NET Standard 2.0-ra épül. Bármely modern .NET projektben használható.

Mi van még?

  • .NET Standard 2.0 alapú. Bármely modern .NET projektben használható.
  • Az API szálbiztos. A belső gyűjtemények alapjául szolgáló ConcurrentDictionary, az eseménykezelők pedig blokkolásokat használnak, vagy nem igényelnek ilyeneket. 
    Ami fontos megjegyezni, hogy az alkalmazás befejezésekor hívja meg ObjectRepository.Save();
  • Tetszőleges indexek (egyediség szükséges):

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

Ki használja ezt?

Személy szerint én ezt a megközelítést kezdtem alkalmazni minden hobbiprojektben, mert kényelmes, és nem igényel nagy költséget az adatelérési réteg írásához vagy nehéz infrastruktúra felépítéséhez. Számomra általában elegendő az adatok tárolása litedb-ben vagy fájlban. 

De a múltban, amikor a csapatommal készítettük a már megszűnt EscapeTeams startupot (gondoltam, hogy itt vannak a pénzek – de nem, újra csak tapasztalat) – adataink tárolására Azure Table Storage-t használtunk.

Jövőbeli tervek

Szeretném orvosolni ennek a megközelítésnek az egyik fő hátrányát – a horizontális skálázhatóságot. Ehhez vagy elosztott tranzakciókra (sic!) van szükség, vagy dönteni kell arról, hogy az azonos adatokat különböző példányok között nem szabad cserélni, vagy hagyni kell, hogy cserélődjenek a „ki az utolsó, az a nyer” elv alapján.

Technikai szempontból az alábbi séma lehetségesnek tűnik:

  • Az objektummodell helyett EventLog-ot és Snapshot-ot tárolni
  • Más példányokat találni (hozzáadni az összes példány végpontjait a beállításokhoz? udp felfedezés? master/slave?)
  • Replikálni az EventLog-ot a példányok között bármely konszenzus algoritmus, például RAFT segítségével.

Van még egy probléma, ami aggaszt – a kaszkádos törlés, vagy az objektumok törlésének észlelése, amelyekre hivatkozások vannak más objektumokból. 

Forráskód

Ha eddig eljutottál – csak a kódot kell átnézni, amit megtalálhatsz a GitHub-on:
https://github.com/DiverOfDark/ObjectRepository

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster