ObjectRepository — .NET atmiņas repozitorija modelis jūsu mājas projektiem

Kāpēc saglabāt visus datus atmiņā?

Lai saglabātu vietnes vai aizmugursistēmas datus, saprātīgāko cilvēku pirmā vēlme būs izvēlēties SQL datu bāzi. 

Taču dažkārt nāk prātā doma, ka datu modelis nav piemērots SQL: piemēram, veidojot meklēšanas vai sociālo grafiku, ir jāmeklē sarežģītas attiecības starp objektiem. 

Sliktākā situācija ir tad, kad strādājat komandā un kolēģis nezina, kā izveidot ātrus vaicājumus. Cik daudz laika pavadījāt, risinot N+1 uzdevumus un veidojot papildu indeksus, lai galvenās lapas SELECT pabeigtu saprātīgā laikā?

Vēl viena populāra pieeja ir NoSQL. Pirms vairākiem gadiem par šo tēmu bija liela ažiotāža — jebkuram ērtam gadījumam viņi izvietoja MongoDB un priecājās par atbildēm json dokumentu veidā. (starp citu, cik kruķu bija jāievieto, jo dokumentos bija apļveida saites?).

Iesaku izmēģināt citu, alternatīvu metodi – kāpēc gan nepamēģināt visus datus saglabāt lietojumprogrammas atmiņā, periodiski saglabājot tos nejaušā krātuvē (failā, attālajā datu bāzē)? 

Atmiņa ir kļuvusi lēta, un visi iespējamie dati lielākajai daļai mazo un vidējo projektu ietilps 1 GB atmiņā. (Piemēram, mans mīļākais mājas projekts ir finanšu izsekotājs, kas pusotru gadu glabā ikdienas statistiku un manu izdevumu, atlikumu un darījumu vēsturi, patērē tikai 45 MB atmiņas.)

Plusi:

  • Piekļuve datiem kļūst vienkāršāka - jums nav jāuztraucas par vaicājumiem, slinku ielādi, ORM funkcijām, jūs strādājat ar parastiem C# objektiem;
  • Nav problēmu, kas saistīta ar piekļuvi no dažādiem pavedieniem;
  • Ļoti ātri - nav tīkla pieprasījumu, nav koda tulkošanas vaicājumu valodā, nav nepieciešama objektu (de)serializācija;
  • Ir pieņemams glabāt datus jebkurā formā — vai tas būtu XML formātā diskā, vai SQL serverī, vai Azure Table Storage.

Mīnusi:

  • Tiek zaudēta horizontālā mērogošana, un rezultātā nevar veikt nulles dīkstāves izvietošanu;
  • Ja lietojumprogramma avarē, varat daļēji zaudēt datus. (Bet mūsu lietojumprogramma nekad avarē, vai ne?)

Kā tas strādā?

Algoritms ir šāds:

  • Sākumā tiek izveidots savienojums ar datu krātuvi un dati tiek ielādēti;
  • Tiek izveidots objekta modelis, primārie indeksi un relāciju indeksi (1:1, 1:Daudzi);
  • Tiek izveidots abonements objekta rekvizītu izmaiņām (INotifyPropertyChanged) un elementu pievienošanai vai noņemšanai kolekcijai (INotifyCollectionChanged);
  • Kad tiek aktivizēts abonements, mainītais objekts tiek pievienots rindai rakstīšanai datu krātuvē;
  • Izmaiņas krātuvē tiek periodiski saglabātas (uz taimera) fona pavedienā;
  • Izejot no programmas, izmaiņas tiek saglabātas arī atmiņā.

Koda piemērs

Nepieciešamo atkarību pievienošana

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

Mēs aprakstām datu modeli, kas tiks saglabāts krātuvē

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 objekta modelis:

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

Un visbeidzot pati repozitorija klase, lai piekļūtu datiem:

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

Izveidojiet ObjectRepository gadījumu:

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

Ja projekts izmantos HangFire

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

Jauna objekta ievietošana:

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

Ar šo zvanu objekts Vecāku modelis tiek pievienots gan lokālajai kešatmiņai, gan rindai rakstīšanai datu bāzē. Tāpēc šai darbībai ir nepieciešams O(1), un ar šo objektu var strādāt nekavējoties.

Piemēram, lai atrastu šo objektu repozitorijā un pārbaudītu, vai atgrieztais objekts ir tas pats gadījums:

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

Kas notiek? Iestatīt () atgriežas Tabulas vārdnīca, kas satur ConcurrentDictionary un nodrošina primāro un sekundāro indeksu papildu funkcionalitāti. Tas ļauj izmantot metodes meklēšanai pēc ID (vai citiem patvaļīgiem lietotāja indeksiem), pilnībā neatkārtojot visus objektus.

Pievienojot objektus ObjectRepository tiek pievienots abonements, lai mainītu to rekvizītus, tāpēc jebkuras izmaiņas rekvizītos arī noved pie šī objekta pievienošanas rakstīšanas rindai. 
Rekvizītu atjaunināšana no ārpuses izskatās tāpat kā darbs ar POCO objektu:

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

Objektu var izdzēst šādos veidos:

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

Tas arī pievieno objektu dzēšanas rindai.

Kā darbojas taupīšana?

ObjectRepository kad uzraudzītie objekti mainās (vai nu pievieno, vai dzēš, vai maina rekvizītus), izraisa notikumu Modelis Mainītsabonējis Ikrātuve. Īstenojumi Ikrātuve kad notiek notikums Modelis Mainīts izmaiņas tiek ievietotas 3 rindās - pievienošanai, atjaunināšanai un dzēšanai.

Arī realizācijas Ikrātuve pēc inicializācijas tie izveido taimeri, kas liek saglabāt izmaiņas ik pēc 5 sekundēm. 

Turklāt ir API, lai piespiestu saglabāt zvanu: ObjectRepository.Save().

Pirms katras saglabāšanas no rindām vispirms tiek izņemtas bezjēdzīgas darbības (piemēram, notikumu dublikāti - kad objekts tika mainīts divas reizes vai ātri pievienoti/noņemti objekti), un tikai pēc tam pati saglabāšana. 

Visos gadījumos tiek saglabāts viss pašreizējais objekts, tāpēc ir iespējams, ka objekti tiek saglabāti citā secībā, nekā tie tika mainīti, ieskaitot jaunākas objektu versijas nekā to pievienošanas rindai brīdī.

Kas vēl tur ir?

  • Visas bibliotēkas ir balstītas uz .NET Standard 2.0. Var izmantot jebkurā modernā .NET projektā.
  • API ir droša pavedienam. Iekšējās kolekcijas tiek īstenotas, pamatojoties uz ConcurrentDictionary, notikumu apstrādātājiem vai nu ir slēdzenes, vai arī tās nav vajadzīgas. 
    Vienīgais, ko vērts atcerēties, ir piezvanīt ObjectRepository.Save();
  • Patvaļīgi indeksi (nepieciešama unikalitāte):

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

Kas to izmanto?

Personīgi es sāku izmantot šo pieeju visos hobiju projektos, jo tas ir ērti un neprasa lielus izdevumus datu piekļuves slāņa rakstīšanai vai smagas infrastruktūras izvēršanai. Personīgi man parasti pietiek ar datu glabāšanu litedb vai failā. 

Bet agrāk, kad vairs neeksistējošais starta uzņēmums EscapeTeams (Man likās, lūk, nauda – bet nē, atkal pieredze) — izmanto datu glabāšanai Azure Table Storage.

Plāni nākotnei

Es vēlos novērst vienu no šīs pieejas galvenajiem trūkumiem - horizontālo mērogošanu. Lai to izdarītu, jums ir nepieciešami vai nu izplatīti darījumi (sic!), vai arī jāpieņem lēmums, ka nedrīkst mainīt vienus un tos pašus datus no dažādām instancēm, vai arī ļaujiet tiem mainīties saskaņā ar principu “kam pēdējam ir taisnība”.

No tehniskā viedokļa es redzu šādu shēmu kā iespējamu:

  • Objekta modeļa vietā saglabājiet notikumu žurnālu un momentuzņēmumu
  • Atrodiet citus gadījumus (pievienot iestatījumiem visu gadījumu galapunktus? udp discovery? master/slave?)
  • Replicējiet starp EventLog gadījumiem, izmantojot jebkuru konsensa algoritmu, piemēram, RAFT.

Mani satrauc arī cita problēma – kaskādes dzēšana jeb tādu objektu dzēšanas gadījumu noteikšana, kuriem ir saites no citiem objektiem. 

Pirmkods

Ja esat izlasījis visu ceļu līdz šejienei, tad atliek tikai izlasīt kodu; to var atrast vietnē GitHub:
https://github.com/DiverOfDark/ObjectRepository

Avots: www.habr.com

Pievieno komentāru