Pse të ruajmë të gjithë të dhënat në kujtesë?
Për ruajtjen e të dhënave të faqes ose backend-it, shumica e njerëzve të shëndoshë do të zgjedhin një bazë të dhënash SQL.
Por ndonjëherë na vjen në mendje se modeli i të dhënave nuk është i përshtatshëm për SQL: për shembull, kur ndihmojmë në ndërtimin e kërkimeve ose të një grafi social, kërkohet kërkimi përmes lidhjeve komplekse midis objekteve.
Situata më e keqe është kur punoni në një ekip dhe kolegu nuk di të ndërtojë kërkesa të shpejta. Sa kohë keni shpenzuar për të zgjidhur problemet N+1 dhe për ndërtimin e indekseve të tjera, që SELECT në faqen kryesore të punojë brenda një kohe të arsyeshme?
Një qasje tjetër e njohur është NoSQL. Disa vjet më parë kishte shumë hype rreth këtij subjekti — për çdo rast të përshtatshëm, ne vendosnim MongoDB dhe gëzonim përgjigjet në formë dokumentesh JSON. (megjithatë, sa "këmbë" duhej të vendosnim për shkak të lidhjeve ciklike në dokumente?).
Unë propozoj të provojmë një qasje tjetër — pse të mos përpiqemi të ruajmë të gjithë të dhënat në kujtesën e aplikacionit, duke ruajtur rastësisht në një depo të jashtme (file, bazë të dhënash të largët)?
Kujtesa është bërë e lirë dhe pothuajse të gjitha të dhënat e shumicës së projekteve të vogla dhe të mesme do të futen në 1 GB kujtesë. (Për shembull, projekti im i preferuar në shtëpi — , i cili ndjek statistikën dhe historinë e shpenzimeve, bilanceve dhe transaksioneve për një periudhë një vit e gjysmë konsumon vetëm 45 MB kujtesë.)
Avantazhet:
- Qasja në të dhëna bëhet më e lehtë — nuk nevojitet të mendoni për kërkesat, ngarkimin e ngadalshëm, veçoritë e ORM, puna bëhet me objekte C# të zakonshme;
- Nuk ka probleme të lidhura me qasjen nga pika të ndryshme;
- Shumë shpejt — nuk ka kërkesa rrjeti, nuk ka përkthim të kodit në gjuhën e kërkesave, nuk nevojitet (de)serializimi i objekteve;
- Është e pranueshme të ruhen të dhënat në çdo formë — qoftë në XML në disk, qoftë në SQL Server, qoftë në Azure Table Storage.
Disavantazhet:
- Humbet skalimi horizontal, dhe si pasojë nuk mund të bëni një vendosje pa kohë shkëputjeje;
- Nëse aplikacioni bie — mund të humbni pjesërisht të dhënat. (Por aplikacioni ynë kurrë nuk bie, e vërteta?)
Si funksionon?
Algoritmi është si më poshtë:
- Në fillim krijohet një lidhje me depozitat e të dhënave, dhe ndodh ngarkimi i të dhënave;
- Ndërtohet një model objekti, indekset fillestare dhe indekset e marrëdhënieve (1:1, 1:Më shumë);
- Krijohet një abonim për ndryshimet në pronat e objekteve (INotifyPropertyChanged) dhe për shtimin ose heqjen e elementeve në koleksion (INotifyCollectionChanged);
- Kur ndodh një skenar — objekti që është ndryshuar, shtohet në radhën për të shkruar në depozitën e të dhënave;
- Herë pas here (përmes një timer-i) në një thread të prapavijës ruhen ndryshimet në depozitë;
- Kur dilni nga aplikacioni gjithashtu ruhen ndryshimet në depozitë.
Shembulli i kodit
Shtoni varësitë e nevojshme
// Основная библиотека
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.HangfirePërshkruani modelin e të dhënave që do të ruhet në depozitë
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; }
}Pastaj modelin e objektit:
public class ParentModel : ModelBase
{
public ParentModel(ParentEntity entity)
{
Entity = entity;
}
public ParentModel()
{
Entity = new ParentEntity(Guid.NewGuid());
}
public Guid? NullableId => null;
// Shembull lidhjeje 1:Më shumë
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);
}
// Qasje me kërkimin përmes indeksit
public ParentModel Parent => Single(ParentId);
protected override BaseEntity Entity => _childEntity;
}Dhe së fundi klasa-repozitë për qasjen në të dhëna:
public class MyObjectRepository : ObjectRepositoryBase
{
public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
{
IsReadOnly = true; // Për testet, lejon të mos ruhen ndryshime në bazë
AddType((ParentEntity x) => new ParentModel(x));
AddType((ChildEntity x) => new ChildModel(x));
// Nëse përdoret Hangfire dhe është e nevojshme të ruhen model të dhënash për Hangfire në ObjectRepository
// this.RegisterHangfireScheme();
Initialize();
}
}Krijojmë një instancë të ObjectRepository:
var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();Nëse në projekt do të përdoret HangFire
public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}Shtimi i një objekti të ri:
var newParent = new ParentModel()
repository.Add(newParent);Me këtë thirrje, objekti ParentModel shto shto shqip shkon në cache lokal dhe në radhë për të shkruar në bazë të dhënash. Prandaj, kjo operacion zgjat O(1), dhe ky objekt mund të përdoret menjëherë.
Për shembull, për të gjetur këtë objekt në depo dhe për t'u siguruar që objekti i kthyer është e njëjta referencë:
var parents = repository.Set();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));Çfarë ndodh në të njëjtën kohë? Set() kthen TableDictionary, që përmban brenda vetes ConcurrentDictionary dhe ofron funksionalitete shtesë për indekset e parë dhe të dytë. Kjo lejon metoda për kërkimin sipas Id (ose indekseve të tjera të përdoruesve) pa e kontrolluar të gjitha objektet.
Gjatë shtimit të objekteve në ObjectRepository shtohen anëtarësime mbi ndryshimin e pronave të tyre, kështu që çdo ndryshim i pronave ka të bëjë edhe me shtimin e këtij objekti në radhë për shkruan.
Përditësimi i pronave nga jashtë duket ashtu si puna me një objekt POCO:
myParent.Children.First().Property = "Vlera e përditësuar";Objekti mund të hiqet në këto mënyra:
repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove(x => !x.Children.Any());Gjithashtu, objekti shtohet në radhën për fshirje.
Si funksionon ruajtja?
ObjectRepository ndërsa objekti të jetë nën vëzhgim (si shtim ose fshirje, ashtu edhe ndryshim pronash) nxit një ngjarje ModelChanged, për të cilën është anëtarësuar IStorage. Implementimet IStorage në ngjarjen e riparimit ModelChanged grumbullojnë ndryshimet në 3 radhë - për shtim, për përditësim, dhe për fshirje.
Për më tepër, implementimet IStorage në inicializim krijojnë një timer, i cili çdo 5 sekonda thërret ruajtjen e ndryshimeve.
Përveç kësaj, ekziston një API për thirrjen e forcës së ruajtjes: ObjectRepository.Save().
Para çdo ruajtjeje, fillimisht ndodh fshirja e operacioneve të panevojshme nga radhët (p.sh. duplikat të ngjarjeve - kur objekti është modifikuar dy herë ose shtimi/fshirja e shpejtë e objekteve), dhe vetëm pastaj ndodh ruajtja.
Në të gjitha rastet, objekti aktual ruhet plotësisht, prandaj është e mundur të ndodhi një situatë kur objektet ruhen në një rend tjetër sesa ndryshuan, duke përfshirë mundësinë që versionet më të reja të objekteve ruhet më vonë se sa momenti i shtimit në radhë.
Çfarë ka tjetër?
- Të gjitha bibliotekat janë të bazuara në .NET Standard 2.0. Mund të përdoren në çdo projekt modern .NET.
- API është i sigurët për rrjedhën. Koleksionet e brendshme janë të implementuara mbi bazën e ConcurrentDictionary, menaxherët e ngjarjeve kanë ose bllokime, ose nuk nevojiten.
E vetmja gjë për t'u kujtuar - gjatë mbylljes së aplikacionit të thirret ObjectRepository.Save(); - Indekset e rastësishme (kërkojnë unikësi):
repository.Set().AddIndex(x => x.Value);
repository.Set().Find(x => x.Value, "myValue");Kush e përdor këtë?
Personalisht, unë fillova ta përdor këtë qasje në të gjitha projektet e mia të hobit, sepse është e leverdishme dhe nuk kërkon shumë investim në shkrimin e një sloj të qasjes ndaj të dhënave ose krijimin e një infrastrukture të rëndë. Për mua, zakonisht është e mjaftueshme ruajtja e të dhënave në litedb ose në një skedar.
Por në të kaluarën, kur me ekipin krijuam startup-in e tashmë të vdekur EscapeTeams (mendoja se këtu janë paratë - por jo, përsëri përvojë) - e përdorëm për ruajtjen e të dhënave Azure Table Storage.
Planet për të ardhmen
Dua të rregulloj një nga disavantazhet kryesore të këtij qasje - shkallëzimi horizontal. Për këtë nevojiten ose transaksione të shpërndara (sic!), ose të marrim një vendim të fuqishëm se të dhënat e njëjta nga instanca të ndryshme nuk duhet të ndryshojnë, ose le të ndryshojnë sipas parimit "kush është i fundit - ato janë të drejtat."
Nga pikëpamja teknike, unë shoh një skemë të mundshme:
- Ruaj në vend të modelit objektor EventLog dhe Snapshot
- Të gjejë instancat e tjera (shtoni në cilësimet përfundimtare të të gjitha instancave? zbulimi UDP? master/slave?)
- Të riplikojë midis instancave EventLog përmes ndonjë algoritmi konsensusi, për shembull RAFT.
Ka një problem tjetër që më shqetëson - është fshirja kaskadive, ose zbulimi i rasteve të fshirjes së objekteve, të cilat kanë referenca nga objekte të tjera.
Kod i burimit
Nëse e keni lexuar këtë deri këtu - atëherë mbetet vetëm të lexoni kodin, mund ta gjeni në GitHub:
Burimi: habr.com
