ObjectRepository - .NET mudellu di repository in memoria per i vostri prughjetti di casa

Perchè almacenà tutti i dati in memoria?

Per almacenà u situ web o dati backend, u primu desideriu di a maiò parte di e persone sana serà di sceglie una basa di dati SQL. 

Ma qualchì volta u pensamentu vene in mente chì u mudellu di dati ùn hè micca adattatu per SQL: per esempiu, quandu custruisce una ricerca o gràficu suciale, avete bisognu di ricercà relazioni cumplessi trà l'uggetti. 

A peor situazione hè quandu travagliate in una squadra è un cullegu ùn sapi micca cumu fà dumande veloci. Quantu tempu avete passatu à risolve i prublemi N + 1 è à custruisce indici supplementari in modu chì u SELECT in a pagina principale finisce in un tempu raghjone?

Un altru approcciu populari hè NoSQL. Parechji anni fà ci era assai hype intornu à questu tema - per ogni occasione cunvene anu implementatu MongoDB è eranu felici cù e risposte in forma di documenti json. (A propositu, quante crutches avete da inserisce per via di i ligami circulari in i ducumenti ?).

Suggeriu di pruvà un altru mètudu alternativu - perchè ùn pruvate micca di almacenà tutte e dati in a memoria di l'applicazione, salvendu periodicamente in un almacenamentu aleatoriu (file, basa di dati remota)? 

A memoria hè diventata economica, è qualsiasi dati pussibuli per a maiò parte di i prughjetti chjuchi è mediani si mette in 1 GB di memoria. (Per esempiu, u mo prughjettu di casa preferitu hè tracker finanziariu, chì mantene e statistiche di ogni ghjornu è a storia di e mo spese, saldi è transazzione per un annu è mezu, cunsuma solu 45 MB di memoria.)

Pros:

  • L'accessu à e dati diventa più faciule - ùn avete micca bisognu di preoccupassi di e dumande, di carica lazy, di funzioni ORM, di travaglià cù l'uggetti C# ordinali;
  • Ùn ci sò micca prublemi assuciati cù l'accessu da diversi fili;
  • Moltu veloce - senza richieste di rete, senza traduzzione di codice in una lingua di quistione, senza bisognu di (de) serializazione di l'uggetti;
  • Hè accettatu per almacenà dati in ogni forma - sia in XML in discu, o in SQL Server, o in Azure Table Storage.

Cons:

  • A scala horizontale hè persa, è in u risultatu, l'implementazione zero downtime ùn pò esse fatta;
  • Sè l 'applicazzioni crashes, vi pò parzialmente perde dati. (Ma a nostra applicazione ùn si ferma mai, nò?)

Cumu viaghja?

L'algoritmu hè u seguente:

  • À u principiu, una cunnessione hè stabilita cù l'almacenamiento di dati, è i dati sò caricati;
  • Un mudellu d'ughjettu, indici primari è indici relazionali (1: 1, 1: Many) sò custruitu;
  • Un abbonamentu hè creatu per i cambiamenti in e proprietà di l'ughjettu (INotifyPropertyChanged) è per aghjunghje o sguassà elementi à a cullezzione (INotifyCollectionChanged);
  • Quandu l'abbunamentu hè attivatu, l'ughjettu cambiatu hè aghjuntu à a fila per scrive à l'almacenamiento di dati;
  • I cambiamenti à l'almacenamiento sò salvati periodicamente (in un timer) in un filu di fondo;
  • Quandu esce da l'applicazione, i cambiamenti sò ancu salvati in u almacenamiento.

Esempiu di codice

Aghjunghjendu i dependenzi necessarii

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

Descrivimu u mudellu di dati chì serà guardatu in u almacenamiento

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

Allora u mudellu di l'ughjettu:

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

È infine, a classa di repository stessu per accede à i dati:

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

Crea una istanza di ObjectRepository:

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

Se u prugettu aduprà HangFire

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

Inserisce un novu ogettu:

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

Cù sta chjama, l'ughjettu ParentModel hè aghjuntu à a cache locale è à a fila per scrive à a basa di dati. Dunque, sta operazione piglia O (1), è questu ughjettu pò esse travagliatu immediatamente.

Per esempiu, per truvà questu ughjettu in u repositoriu è verificate chì l'ughjettu restituitu hè a stessa istanza:

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

Chì succede ? Set () torna TableDictionary, chì cuntene Dictionary Concurrent è furnisce funziunalità supplementari di l'indici primari è secundarii. Questu permette di avè metudi per a ricerca per Id (o altri indici d'utilizatori arbitrarii) senza iterazione cumplettamente nantu à tutti l'uggetti.

Quandu aghjunghje l'uggetti à Repository d'oggetti un abbunamentu hè aghjuntu à cambià e so proprietà, cusì ogni cambiamentu di pruprietà hè ancu risultatu in questu oggettu chì hè aghjuntu à a fila di scrittura. 
L'aghjurnà e proprietà da l'esternu s'assumiglia à travaglià cù un oggettu POCO:

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

Pudete sguassà un oggettu in i seguenti modi:

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

Questu aghjunghje ancu l'ughjettu à a fila di eliminazione.

Cumu u travagliu di salvezza?

Repository d'oggetti quandu l'uggetti monitorati cambianu (aghjunghje o sguassate, o mudificate proprietà), suscite un avvenimentu Mudellu cambiatuabbonatu à ISmagazzinu. Implementazioni ISmagazzinu quandu un avvenimentu accade Mudellu cambiatu i cambiamenti sò posti in fila 3 - per aghjunghje, per aghjurnà è per sguassà.

Ancu implementazioni ISmagazzinu dopu à l'inizializazione, creanu un timer chì face chì i cambiamenti sò salvati ogni 5 seconde. 

Inoltre, ci hè una API per furzà una chjama di salvezza: ObjectRepository.Save ().

Prima di ogni salvezza, l'operazioni senza significatu sò prima sguassate da a fila (per esempiu, l'avvenimenti duplicati - quandu un ughjettu hè cambiatu duie volte o l'oggetti aghjustati / sguassati rapidamente), è solu dopu a salvezza stessu. 

In tutti i casi, tuttu l'ughjettu attuale hè salvatu, cusì hè pussibule chì l'uggetti sò salvati in un ordine diffirenti di quelli chì sò stati cambiati, cumprese e versioni più recenti di l'uggetti chì à u mumentu chì sò stati aghjuntu à a fila.

Chì ci hè altru ?

  • Tutte e librerie sò basate nantu à .NET Standard 2.0. Pò esse usatu in ogni prughjettu mudernu .NET.
  • L'API hè thread safe. I cullizzioni internu sò implementati basatu nantu Dictionary Concurrent, i gestori di l'avvenimenti o anu chjusi o ùn ne anu micca bisognu. 
    L'unicu chì vale a pena ricurdà hè di chjamà ObjectRepository.Save ();
  • Indici arbitrari (esigene unicità):

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

Quale si usa ?

In modu persunale, aghju cuminciatu à aduprà stu approcciu in tutti i prughjetti di hobby perchè hè cunvene è ùn hè micca bisognu di grandi spese per scrive una capa d'accessu di dati o implementà una infrastruttura pesante. In modu persunale, l'almacenamiento di dati in litedb o un schedariu hè di solitu abbastanza per mè. 

Ma in u passatu, quandu l'iniziu oramai difunta EscapeTeams (Pensu chì quì hè, soldi - ma micca, sperienza di novu) - utilizatu per almacenà dati in Azure Table Storage.

Piani di u futuru

Vogliu riparà unu di i principali disadvantages di stu approcciu - scala horizontale. Per fà questu, avete bisognu di transazzione distribuitu (sic!), O di piglià una decisione forte chì i stessi dati da diverse istanze ùn deve micca cambià, o lasciate cambià secondu u principiu "quale hè l'ultimu hè ghjustu".

Da un puntu di vista tecnicu, vecu u schema seguente pussibule:

  • Store EventLog è Snapshot invece di mudellu d'ughjettu
  • Truvate altre istanze (aghjunghje endpoints di tutte l'istanze à i paràmetri? udp discovery? master/slave?)
  • Replica trà e istanze di EventLog via qualsiasi algoritmu di cunsensu, cum'è RAFT.

Ci hè ancu un altru prublema chì mi preoccupa - eliminazione in cascata, o rilevazione di casi di eliminazione di l'uggetti chì anu ligami da altri oggetti. 

Codice fonte

Sè avete lettu finu à quì, allora tuttu ciò chì resta hè di leghje u codice; pò esse truvatu in GitHub:
https://github.com/DiverOfDark/ObjectRepository

Source: www.habr.com

Add a comment