ObjectRepository - .NET obrazac skladišta u memoriji za vaše kućne projekte

Zašto pohranjivati ​​sve podatke u memoriju?

Za pohranu podataka web stranice ili backend-a, prva želja većine razumnih ljudi bit će da izaberu SQL bazu podataka. 

Ali ponekad mi padne na pamet da model podataka nije prikladan za SQL: na primjer, kada gradite pretragu ili društveni graf, morate tražiti složene odnose između objekata. 

Najgora situacija je kada radite u timu, a kolega ne zna kako napraviti brze upite. Koliko ste vremena potrošili na rješavanje N+1 problema i pravljenje dodatnih indeksa kako bi SELECT na glavnoj stranici bio završen u razumnom vremenu?

Još jedan popularan pristup je NoSQL. Prije nekoliko godina bilo je mnogo pompe oko ove teme - za svaku prigodnu priliku postavili su MongoDB i bili su zadovoljni odgovorima u obliku json dokumenata (usput, koliko ste štaka morali da ubacite zbog kružnih veza u dokumentima?).

Predlažem da isprobate drugu, alternativnu metodu - zašto ne pokušate pohraniti sve podatke u memoriju aplikacije, povremeno ih spremajući u nasumično skladište (datoteka, udaljena baza podataka)? 

Memorija je postala jeftina, a svi mogući podaci za većinu malih i srednjih projekata će stati u 1 GB memorije. (Na primjer, moj omiljeni kućni projekat je finansijski tracker, koji vodi dnevnu statistiku i istoriju mojih troškova, stanja i transakcija godinu i po dana, troši samo 45 MB memorije.)

Pros:

  • Pristup podacima postaje lakši - ne morate da brinete o upitima, lijenom učitavanju, ORM funkcijama, radite sa običnim C# objektima;
  • Nema problema povezanih sa pristupom iz različitih niti;
  • Vrlo brzo - nema mrežnih zahtjeva, nema prijevoda koda u jezik upita, nema potrebe za (de)serijalizacijom objekata;
  • Prihvatljivo je pohranjivati ​​podatke u bilo kojem obliku - bilo u XML-u na disku, ili u SQL Serveru, ili u Azure Storageu tablica.

Cons:

  • Horizontalno skaliranje je izgubljeno, i kao rezultat toga, ne može se izvršiti implementacija bez zastoja;
  • Ako se aplikacija sruši, možete djelimično izgubiti podatke. (Ali naša aplikacija se nikada ne ruši, zar ne?)

Как это работает?

Algoritam je sljedeći:

  • Na početku se uspostavlja veza sa skladištem podataka i podaci se učitavaju;
  • Izgrađen je objektni model, primarni indeksi i relacijski indeksi (1:1, 1:Mnogo);
  • Kreira se pretplata za promjene svojstava objekta (INotifyPropertyChanged) i za dodavanje ili uklanjanje elemenata kolekciji (INotifyCollectionChanged);
  • Kada se pretplata aktivira, promijenjeni objekt se dodaje u red za upisivanje u skladište podataka;
  • Promjene u memoriji se povremeno spremaju (na tajmer) u pozadinskoj niti;
  • Kada izađete iz aplikacije, promjene se također spremaju u memoriju.

Primjer koda

Dodavanje potrebnih zavisnosti

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

Opisujemo model podataka koji će biti pohranjen u memoriji

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

Zatim objektni model:

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

I konačno, sama klasa spremišta za pristup podacima:

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

Kreirajte instancu ObjectRepository:

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

Ako će projekt koristiti HangFire

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

Umetanje novog objekta:

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

Sa ovim pozivom, objekt ParentModel se dodaje i lokalnoj predmemoriji i redu za upis u bazu podataka. Stoga, ova operacija traje O(1) i sa ovim objektom se može odmah raditi.

Na primjer, da pronađete ovaj objekt u spremištu i provjerite da li je vraćeni objekt ista instanca:

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

Šta se dešava? Set () vraća TableDictionary, koji sadrži ConcurrentDictionary i pruža dodatnu funkcionalnost primarnih i sekundarnih indeksa. Ovo vam omogućava da imate metode za pretraživanje po ID-u (ili drugim proizvoljnim korisničkim indeksima) bez potpunog ponavljanja svih objekata.

Prilikom dodavanja objekata u ObjectRepository dodaje se pretplata za promjenu njihovih svojstava, tako da svaka promjena svojstava također rezultira dodavanjem ovog objekta u red za pisanje. 
Ažuriranje svojstava izvana izgleda isto kao rad s POCO objektom:

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

Objekt možete izbrisati na sljedeće načine:

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

Ovo također dodaje objekt u red za brisanje.

Kako funkcionira štednja?

ObjectRepository kada se nadgledani objekti mijenjaju (bilo dodavanje ili brisanje, ili promjena svojstava), pokreće događaj ModelChangedpretplaćen na Istorage. Implementacije Istorage kada se dogodi neki događaj ModelChanged promjene se stavljaju u 3 reda - za dodavanje, za ažuriranje i za brisanje.

Također implementacije Istorage nakon inicijalizacije, kreiraju tajmer koji uzrokuje da se promjene pohranjuju svakih 5 sekundi. 

Osim toga, postoji API za prisilno pozivanje na spremanje: ObjectRepository.Save().

Prije svakog spremanja iz redova se prvo uklanjaju besmislene operacije (na primjer, dupli događaji - kada je objekt dvaput promijenjen ili brzo dodani/uklonjeni objekti), a tek onda samo spremanje. 

U svim slučajevima se sprema cijeli trenutni objekt, tako da je moguće da se objekti spremaju drugačijim redoslijedom nego što su promijenjeni, uključujući novije verzije objekata nego u vrijeme kada su dodani u red čekanja.

Šta još ima?

  • Sve biblioteke su zasnovane na .NET Standardu 2.0. Može se koristiti u bilo kojem modernom .NET projektu.
  • API je siguran niti. Interne kolekcije se implementiraju na osnovu ConcurrentDictionary, obrađivači događaja ili imaju brave ili im nisu potrebni. 
    Jedina stvar koju vrijedi zapamtiti je poziv ObjectRepository.Save();
  • Proizvoljni indeksi (zahtevaju jedinstvenost):

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

Ko ga koristi?

Lično sam ovaj pristup počeo koristiti u svim hobi projektima jer je zgodan i ne zahtijeva velike troškove za pisanje sloja za pristup podacima ili postavljanje teške infrastrukture. Lično, pohranjivanje podataka u litedb ili fajl obično mi je dovoljno. 

Ali u prošlosti, kada je sada nepostojeći startup EscapeTeams (Mislio sam, evo ga, novac - ali ne, opet iskustvo) - koristi se za pohranjivanje podataka u Azure Storage tablica.

Planovi za budućnost

Želio bih popraviti jedan od glavnih nedostataka ovog pristupa - horizontalno skaliranje. Da biste to učinili, trebate ili distribuirati transakcije (sic!), ili donijeti čvrstu odluku da se isti podaci iz različitih instanci ne mijenjaju, ili ih pustiti da se mijenjaju po principu „ko je zadnji u pravu“.

Sa tehničke tačke gledišta, vidim sljedeću shemu kao moguću:

  • Pohranite EventLog i Snapshot umjesto objektnog modela
  • Pronađite druge instance (dodati krajnje tačke svih instanci u postavke? udp discovery? master/slave?)
  • Replicirajte između instanci EventLog putem bilo kojeg konsenzusnog algoritma, kao što je RAFT.

Postoji još jedan problem koji me brine - kaskadno brisanje, odnosno otkrivanje slučajeva brisanja objekata koji imaju veze sa drugim objektima. 

Izvor

Ako ste pročitali skroz do ovdje, preostaje vam samo da pročitate kod; može se naći na GitHubu:
https://github.com/DiverOfDark/ObjectRepository

izvor: www.habr.com

Dodajte komentar