ObjectRepository – .NET memórián belüli adattár minta otthoni projektjeihez

Miért tárolunk minden adatot a memóriában?

A webhely- vagy háttéradatok tárolásához a legtöbb józan embernek az lesz az első vágya, hogy válasszon egy SQL-adatbázist. 

De néha eszünkbe jut az a gondolat, hogy az adatmodell nem alkalmas SQL-re: például keresési vagy közösségi gráf építésénél bonyolult kapcsolatokat kell keresni az objektumok között. 

A legrosszabb helyzet az, amikor csapatban dolgozol, és egy kolléga nem tudja, hogyan kell gyors lekérdezéseket felépíteni. Mennyi időt töltött N+1 probléma megoldásával és további indexek felépítésével, hogy a főoldalon lévő SELECT ésszerű időn belül elkészüljön?

Egy másik népszerű megközelítés a NoSQL. Néhány évvel ezelőtt nagy volt a hírverés a téma körül – minden kényelmes alkalomra telepítették a MongoDB-t, és örültek a json dokumentumok formájában kapott válaszoknak. (egyébként hány mankót kellett behelyezni a dokumentumokban található körkörös hivatkozások miatt?).

Azt javaslom, próbáljon ki egy másik, alternatív módszert - miért ne próbálja meg az összes adatot az alkalmazás memóriájában tárolni, és időnként véletlenszerű tárhelyre (fájl, távoli adatbázis) menti? 

A memória olcsóvá vált, és a legtöbb kis- és közepes méretű projekthez minden lehetséges adat elfér 1 GB memóriában. (Például a kedvenc otthoni projektem az pénzügyi nyomkövető, amely másfél éven keresztül napi statisztikát és előzményt vezet a kiadásaimról, egyenlegeimről és tranzakcióimról, mindössze 45 MB memóriát fogyaszt.)

Előnyök:

  • Az adatokhoz való hozzáférés könnyebbé válik – nem kell aggódnia a lekérdezések, a lusta betöltés, az ORM-szolgáltatások miatt, hanem hétköznapi C# objektumokkal dolgozhat;
  • Nincsenek problémák a különböző szálakról való hozzáféréssel kapcsolatban;
  • Nagyon gyors - nincs hálózati kérés, nincs kód lekérdező nyelvre fordítása, nincs szükség az objektumok (de)szerializálására;
  • Az adatok bármilyen formában történő tárolása elfogadható – legyen az XML-ben a lemezen, az SQL Serverben vagy az Azure Table Storage-ban.

Hátrányok:

  • A vízszintes méretezés elveszik, és ennek eredményeként a nulla állásidő nélküli telepítés nem hajtható végre;
  • Ha az alkalmazás összeomlik, akkor részleges adatvesztés következhet be. (De az alkalmazásunk sosem omlik össze, igaz?)

Hogyan működik?

Az algoritmus a következő:

  • Indításkor kapcsolat jön létre az adattárolóval, és az adatok betöltődnek;
  • Objektummodellt, elsődleges indexeket és relációs indexeket (1:1, 1:Sok) építenek;
  • Előfizetés jön létre az objektum tulajdonságainak módosítására (INotifyPropertyChanged), valamint elemek hozzáadására vagy eltávolítására a gyűjteményhez (INotifyCollectionChanged);
  • Az előfizetés aktiválásakor a megváltozott objektum hozzáadódik az adattárba írási sorhoz;
  • A tárhely módosításait rendszeres időközönként (időzítőn) egy háttérszálban menti a rendszer;
  • Amikor kilép az alkalmazásból, a változtatások a tárolóra is mentésre kerülnek.

Kódpélda

A szükséges függőségek hozzáadása

// Основная библиотека
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 a tárolóban tárolandó adatmodellt

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;
    
    // Пример связи 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;
}

És végül maga az adattárosztály az adatok eléréséhez:

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

Hozzon létre egy ObjectRepository példányt:

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

Ha a projekt HangFire-t fog 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);

Ezzel a hívással az objektum ParentModel hozzáadódik a helyi gyorsítótárhoz és az adatbázisba írási sorhoz is. Ezért ehhez a művelethez O(1) szükséges, és ezzel az objektummal azonnal dolgozni lehet.

Ha például meg szeretné keresni ezt az objektumot a lerakatban, és ellenőrizni szeretné, hogy a visszaadott objektum ugyanaz a példány-e:

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

Mi történik? Készlet () visszatér Táblázatszótár, ami tartalmaz ConcurrentDictionary és további funkciókat biztosít az elsődleges és másodlagos indexekhez. Ez lehetővé teszi, hogy az Id (vagy más tetszőleges felhasználói indexek) alapján keressen metódusokat anélkül, hogy az összes objektumon végig kellene ismételni.

Amikor objektumokat ad hozzá ObjectRepository egy előfizetés hozzáadódik a tulajdonságaik megváltoztatásához, így a tulajdonságokban bekövetkezett bármilyen változás azt is eredményezi, hogy ez az objektum hozzáadódik az írási sorhoz. 
A tulajdonságok kívülről történő frissítése ugyanúgy néz ki, mint egy POCO objektummal végzett munka:

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

Egy objektumot a következő módokon törölhet:

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

Ezzel az objektumot is hozzáadja a törlési sorhoz.

Hogyan működik a mentés?

ObjectRepository amikor a megfigyelt objektumok megváltoznak (akár hozzáadnak, akár törölnek, vagy megváltoztatnak tulajdonságokat), eseményt vet fel Modell megváltozottelőfizetett IStorage. Megvalósítások IStorage amikor egy esemény bekövetkezik Modell megváltozott a változtatások 3 sorba kerülnek - hozzáadáshoz, frissítéshez és törléshez.

Megvalósításokat is IStorage inicializáláskor létrehoznak egy időzítőt, amely 5 másodpercenként menti a változtatásokat. 

Ezenkívül van egy API a mentési hívás kényszerítésére: ObjectRepository.Save().

Minden mentés előtt először az értelmetlen műveletek törlődnek a sorokból (például duplikált események - ha egy objektumot kétszer módosítottak, vagy gyorsan hozzáadtak/eltávolítottak objektumokat), és csak ezt követően maga a mentés. 

Minden esetben a teljes aktuális objektum mentésre kerül, így előfordulhat, hogy az objektumok mentése más sorrendben történik, mint ahogyan megváltoztak, beleértve az objektumok újabb verzióit is, mint amikor felkerültek a sorba.

Mi más van még?

  • Minden könyvtár a .NET 2.0 szabványon alapul. Bármely modern .NET projektben használható.
  • Az API szálbiztos. A belső gyűjtemények az alapján valósulnak meg ConcurrentDictionary, az eseménykezelőknek vagy van zára, vagy nincs rájuk szükségük. 
    Az egyetlen dolog, amit érdemes megjegyezni, az az, hogy fel kell hívni 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?

Személy szerint azért kezdtem el ezt a megközelítést alkalmazni minden hobbiprojektben, mert kényelmes és nem igényel nagy kiadásokat az adatelérési réteg megírása vagy a nehéz infrastruktúra kiépítése. Személy szerint nekem általában elég a litedb-ben vagy fájlban tárolni az adatokat. 

De a múltban, amikor a már megszűnt induló EscapeTeams (Azt hittem, itt a pénz – de nem, ismét tapasztalat) – adatok tárolására szolgál az Azure Table Storage szolgáltatásban.

Tervek a jövőre

Szeretném kijavítani ennek a megközelítésnek az egyik fő hátrányát - a vízszintes skálázást. Ehhez vagy elosztott tranzakciókra van szükség (sic!), vagy határozott akaratú döntést kell hoznia, hogy a különböző példányokból származó ugyanazok az adatok ne változzanak, vagy hagyják, hogy a „kinek van igaza” elv szerint változzanak.

Technikai szempontból a következő sémát látom lehetségesnek:

  • Tárolja az eseménynaplót és a pillanatképet az objektummodell helyett
  • Más példányok keresése (minden példány végpontjának hozzáadása a beállításokhoz? udp-felderítés? mester/szolga?)
  • Replikáljon az EventLog-példányok között bármilyen konszenzusos algoritmussal, például RAFT-tal.

Van egy másik probléma is, amely aggaszt – a lépcsőzetes törlés, vagy olyan objektumok törlésének észlelése, amelyek más objektumokra hivatkoznak. 

Forráskód

Ha egészen idáig elolvastad, akkor már csak a kódot kell elolvasnod; az megtalálható a GitHubon:
https://github.com/DiverOfDark/ObjectRepository

Forrás: will.com

Hozzászólás