KÄpÄc uzglabÄt visus datus atmiÅÄ?
Kad runa ir par mÄjaslapas vai aizmugurÄjÄs puses datu uzglabÄÅ”anu, vairums saprÄtÄ«gu cilvÄku pirmÄ izvÄle ir SQL datubÄze.Ā
Bet dažreiz prÄtÄ ienÄk doma, ka datu modelis nav piemÄrots SQL: piemÄram, veidojot meklÄÅ”anu vai sociÄlo grafiku, nepiecieÅ”ama sarežģītu attiecÄ«bu starp objektiem meklÄÅ”ana.Ā
VisgrÅ«tÄk ir strÄdÄjot komandÄ, kad kolÄÄ£is nezina, kÄ veidot Ätrus vaicÄjumus. Cik daudz laika esat pavadÄ«jis, risinot N+1 problÄmas un veidojot papildus indeksus, lai SELECT galvenajÄ lapÄ izpildÄ«tos saprÄtÄ«gÄ laikÄ?
Cits populÄrs pieejas veids ir NoSQL. Pirms dažiem gadiem ap Å”o tÄmu bija liela ažiotÄža ā jebkurÄ izdevÄ«gÄ gadÄ«jumÄ tika izvÄrsta MongoDB un priecÄjÄmies par atbildÄm json dokumentu veidÄ. (Starpcitu, cik daudz pagaidu risinÄjumu nÄcÄs ievietot dÄļ cikliskÄm atsaucÄm dokumentos?).
Es piedÄvÄju izmÄÄ£inÄt vÄl vienu alternatÄ«vu pieeju ā kÄpÄc gan neglabÄt visus datus lietojumprogrammas atmiÅÄ, regulÄri saglabÄjot tos nejauÅ”Ä krÄtuvÄ (fails, attÄlinÄta datubÄze)?Ā
AtmiÅa ir kļuvusi lÄta, un jebkuri iespÄjamie dati vairumam mazo un vidÄjo projektu iekļaujas 1 GB atmiÅÄ. (PiemÄram, mans iecienÄ«tÄkais mÄjas projekts ā , kas veic ikdienas statistiku un manu izdevumu, bilances un darÄ«jumu vÄsturi pusotru gadu, patÄrÄ tikai 45 MB atmiÅas.)
PriekŔrocības:
- Piekļuve datiem kļūst vienkÄrÅ”Äka ā nav jÄuztraucas par vaicÄjumiem, slinkajÄm ielÄdÄm, ORM Ä«patnÄ«bÄm, darbs notiek ar parastajiem C# objektiem;
- Nav problÄmu, kas saistÄ«tas ar piekļuvi no dažÄdiem pavedieniem;
- Ä»oti Ätri ā nav tÄ«kla vaicÄjumu, nav koda translÄcijas vaicÄjumu valodÄ, nav nepiecieÅ”ama (de)serializÄcija objektu;
- Ir pieÅemami glabÄt datus jebkurÄ veidÄ ā gan XML diskÄ, gan SQL Server, gan Azure Table Storage.
Mīnusi:
- PazaudÄta horizontÄlÄ mÄrogoÅ”ana, lÄ«dz ar to nav iespÄjams veikt zero downtime deployment;
- Ja lietojumprogramma avarÄ ā iespÄjams daļÄji zaudÄt datus. (Bet mÅ«su lietojumprogramma taÄu nekad neavarÄ, vai ne?)
KÄ tas darbojas?
Algoritms ir Å”Äds:
- SÄkumÄ tiek izveidota savienojums ar datu krÄtuvi, un notiek datu ielÄde;
- Tiek veidota objektu modeļu struktÅ«ra, primÄrie indeksi un attiecÄ«bu indeksi (1:1, 1:Daudz);
- Tiek izveidota abonÄÅ”ana uz objektu Ä«paŔību izmaiÅÄm (INotifyPropertyChanged) un uz elementu pievienoÅ”anu vai dzÄÅ”anu kolekcijÄ (INotifyCollectionChanged);
- Kad abonÄÅ”ana tiek aktivizÄta, mainÄ«tais objekts tiek pievienots datu veikala ierakstu rindai;
- Periodiski (pÄc taimeri) fona pavedienÄ tiek saglabÄtas izmaiÅas datu veikalÄ;
- Izliekot no programmas, arÄ« tiek saglabÄtas izmaiÅas datu veikalÄ.
Koda piemÄrs
Pievienojam nepiecieÅ”amÄs atkarÄ«bas
// ŠŃŠ½Š¾Š²Š½Š°Ń Š±ŠøŠ±Š»ŠøŠ¾ŃŠµŠŗŠ°
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.HangfireAprakstÄm datu modeli, kas tiks saglabÄts datu veikalÄ
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; }
}Tad ir objektu modelis:
public class ParentModel : ModelBase
{
public ParentModel(ParentEntity entity)
{
Entity = entity;
}
public ParentModel()
{
Entity = new ParentEntity(Guid.NewGuid());
}
public Guid? NullableId => null;
// 1:Daudz attiecÄ«bu piemÄrs
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;
}
// PieejamÄ«ba ar indeksu meklÄÅ”anu
public ParentModel Parent => Single(ParentId);
protected override BaseEntity Entity => _childEntity;
}Un visfinally pats datu piekļuves klase - repozitorijs:
public class MyObjectRepository : ObjectRepositoryBase
{
public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
{
IsReadOnly = true; // Testiem, ļauj nesaglabÄt izmaiÅas datu bÄzÄ
AddType((ParentEntity x) => new ParentModel(x));
AddType((ChildEntity x) => new ChildModel(x));
// Ja tiek lietots Hangfire un nepiecieÅ”ams saglabÄt datu modeli Hangfire datu repozitorijÄ
// this.RegisterHangfireScheme();
Initialize();
}
}Izveidojam ObjectRepository instance:
var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();Ja projektÄ tiks izmantots HangFire
public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}Jauna objekta pievienoŔana:
var newParent = new ParentModel()
repository.Add(newParent);Å ajÄ izsaukumÄ objekts ParentModel tiek pievienots gan lokÄlajam keÅ”atmiÅai, gan ieraksta rindai datubÄzÄ. TÄpÄc Ŕī operÄcija aizÅem O(1), un ar Å”o objektu var uzreiz strÄdÄt.
PiemÄram, lai atrastu Å”o objektu repozitorijÄ un pÄrliecinÄtos, ka atgrieztais objekts ir tas pats eksemplÄrs:
var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));Kas notiek Å”ajÄ gadÄ«jumÄ? Set<ParentModel>() atgriež TableDictionary<ParentModel>, kas satur iekÅ”Ä ConcurrentDictionary<ParentModel, ParentModel> un piedÄvÄ papildu funkcionalitÄti primÄrajiem un sekundÄrajiem indeksi. Tas ļauj izmantot metodes meklÄÅ”anai pÄc Id (vai citiem lietotÄja indeksiem) bez visu objektu pilnÄ«gas pÄrskatīŔanas.
Pievienojot objektus ObjectRepository tiek pievienota paraksts uz to Ä«paŔību izmaiÅÄm, tÄpÄc jebkura Ä«paŔību izmaiÅa arÄ« rezultÄ objekta pievienoÅ”anÄ ieraksta rindai.Ā
PÄrveidojot Ä«paŔības no Ärpuses, tas izskatÄs tÄpat kÄ strÄdÄjot ar POCO objektu:
myParent.Children.First().Property = "AtjaunotÄ vÄrtÄ«ba";Objektu var izdzÄst Å”Ädos veidos:
repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());Å ajÄ gadÄ«jumÄ notiek arÄ« objekta pievienoÅ”ana izdzÄÅ”anas rindai.
KÄ darbojas saglabÄÅ”ana?
ObjectRepository ja izsekojamo objektu izmaiÅas (kÄ pievienoÅ”ana vai izdzÄÅ”ana, tÄpat kÄ Ä«paŔību izmaiÅas) izsauc notikumu ModelChanged, uz kuru ir parakstÄ«jies IStorage. RealizÄcijas IStorage notikuma raÅ”anÄs gadÄ«jumÄ ModelChanged uzkrÄj izmaiÅas 3 rindÄs ā pievienoÅ”anai, atjauninÄÅ”anai un izdzÄÅ”anai.
TÄpat realizÄcijas IStorage savas inicializÄcijas laikÄ izveido taimeri, kas ik pÄc 5 sekundÄm izsauc izmaiÅu saglabÄÅ”anu.Ā
TurklÄt pastÄv API piespiedu saglabÄÅ”anas izsaukÅ”anai: ObjectRepository.Save().
Pirms katras saglabÄÅ”anas vispirms notiek bezjÄdzÄ«gu operÄciju izÅemÅ”ana no rindÄm (piemÄram, notikumu dublikÄti ā kad objekts tika mainÄ«ts divreiz vai Ätra pievienoÅ”ana/izdzÄÅ”ana), un tikai pÄc tam pati saglabÄÅ”ana.Ā
Visos gadÄ«jumos tiek saglabÄts aktuÄlais objekts pilnÄ«bÄ, tÄpÄc var rasties situÄcija, kad objekti tiek saglabÄti citÄ secÄ«bÄ, nekÄ mainÄ«jÄs, tajÄ skaitÄ var tikt saglabÄtas jaunÄkas objektu versijas, nekÄ brÄ«dÄ«, kad tÄs tika pievienotas rindai.
Kas vÄl ir?
- Visas bibliotÄkas balstÄs uz .NET Standard 2.0. To var izmantot jebkurÄ mÅ«sdienu .NET projektÄ.
- API ir plÅ«stoÅ”s. IekÅ”ÄjÄs kolekcijas ir balstÄ«tas uz ConcurrentDictionary, un notikumu apstrÄdÄtÄjiem ir vai nu bloÄ·ÄÅ”ana, vai arÄ« tas nav nepiecieÅ”ams.Ā
VienÄ«gais, kas jÄatceras ā pirms lietojuma beigÅ”anas ir jÄvÄrÅ”as pie ObjectRepository.Save(); - Patvaļīgie indeksi (prasa unikÄlumu):
repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");Kas to lieto?
PersonÄ«gi es sÄku izmantot Å”o pieeju visos hobija projektos, jo tas ir Ärti un neprasa lielus izdevumus datu piekļuves slÄÅa rakstīŔanai vai smagas infrastruktÅ«ras izveidoÅ”anai. Man parasti pietiek ar datu glabÄÅ”anu litedb vai failÄ.Ā
Bet pagÄtnÄ, kad ar komandu veidojÄm tagad jau neeksistÄjoÅ”o startapu EscapeTeams (domÄju, nu lÅ«k, nauda ā bet nÄ, atkal pieredze) ā izmantojÄm Azure Table Storage datu glabÄÅ”anai.
NÄkotnes plÄni
Gribu novÄrst vienu no galvenajiem Ŕī pieejas trÅ«kumiem ā horizontÄlo mÄrogoÅ”anu. Tam nepiecieÅ”amas vai nu izplatÄ«tas transakcijas (sic!), vai jÄpieÅem grÅ«ts lÄmums, ka vienÄdi dati no dažÄdiem instancÄm nedrÄ«kst mainÄ«ties, vai rezultÄtÄ pieļaut mainīŔanu pÄc principa 'kurÅ” pÄdÄjais, tas taisnÄ«ba'.
No tehniskÄ viedokļa redzu iespÄjamu sekojoÅ”o shÄmu:
- GlabÄt nevis objektu modeli, bet gan EventLog un Snapshot
- Atrast citas instances (iekļaut visÄs instance iestatÄ«jumos gala punktus? udp atklÄÅ”ana? master/slave?)
- Repliku veikt starp instancÄm ar jebkuru no konsensa algoritmiem, piemÄram, RAFT.
TÄpat pastÄv vÄl viena problÄma, kas mani uztrauc ā tas ir kaskÄdes dzÄÅ”ana, vai objektu, uz kuriem ir atsauces no citiem objektiem, dzÄÅ”anas gadÄ«jumu noteikÅ”ana.Ā
AtslÄgas kods
Ja jÅ«s esat izlasÄ«jis lÄ«dz Å”im ā tad tÄlÄk paliek tikai lasÄ«t kodu, to var atrast GitHub:
Avots: habr.com
