ObjectRepository - uy loyihalaringiz uchun .NET xotiradagi ombor namunasi

Nima uchun barcha ma'lumotlarni xotirada saqlash kerak?

Veb-sayt yoki backend ma'lumotlarini saqlash uchun ko'pchilik aqli raso odamlarning birinchi istagi SQL ma'lumotlar bazasini tanlash bo'ladi. 

Ammo ba'zida ma'lumotlar modeli SQL uchun mos emas degan fikr xayolga keladi: masalan, qidiruv yoki ijtimoiy grafikni qurishda siz ob'ektlar orasidagi murakkab munosabatlarni qidirishingiz kerak. 

Eng yomon vaziyat, siz jamoada ishlaganingizda va hamkasbingiz tezkor so'rovlarni qanday yaratishni bilmaydi. Asosiy sahifadagi SELECT mos vaqt ichida tugallanishi uchun N+1 masalalarni yechish va qoʻshimcha indekslar yaratish uchun qancha vaqt sarfladingiz?

Yana bir mashhur yondashuv - NoSQL. Bir necha yil oldin ushbu mavzu atrofida juda ko'p shov-shuv bo'lgan - har qanday qulay vaziyatda ular MongoDB-ni joylashtirdilar va json hujjatlari ko'rinishidagi javoblardan mamnun edilar. (Aytgancha, hujjatlardagi dumaloq havolalar tufayli nechta tayoq qo'yish kerak edi?).

Men boshqa, muqobil usulni sinab ko'rishni taklif qilaman - nima uchun barcha ma'lumotlarni dastur xotirasida saqlashga, vaqti-vaqti bilan tasodifiy saqlashga (fayl, masofaviy ma'lumotlar bazasi) saqlashga harakat qilmaysiz? 

Xotira arzonlashdi va ko'pgina kichik va o'rta loyihalar uchun mumkin bo'lgan har qanday ma'lumotlar 1 Gb xotiraga mos keladi. (Masalan, mening sevimli uy loyiham moliyaviy kuzatuvchi, bir yarim yil davomida har kungi statistika va mening xarajatlarim, balanslarim va tranzaksiyalarim tarixini saqlaydigan atigi 45 MB xotirani sarflaydi.)

Taroziga soling:

  • Ma'lumotlarga kirish osonlashadi - so'rovlar, dangasa yuklash, ORM xususiyatlari haqida tashvishlanishingiz shart emas, siz oddiy C# ob'ektlari bilan ishlaysiz;
  • Turli mavzulardan kirish bilan bog'liq muammolar yo'q;
  • Juda tez - tarmoq so'rovlari yo'q, kodni so'rovlar tiliga tarjima qilishning hojati yo'q, ob'ektlarni ketma-ketlashtirish (de) zarurati yo'q;
  • Ma'lumotni istalgan shaklda saqlash mumkin - xoh u diskdagi XML-da, xoh SQL Serverda, xoh Azure Table Storage-da.

Kamchiliklari:

  • Gorizontal masshtab yo'qoladi va natijada nol ishlamay qo'yishni amalga oshirib bo'lmaydi;
  • Agar dastur ishlamay qolsa, ma'lumotlarni qisman yo'qotishingiz mumkin. (Ammo bizning ilovamiz hech qachon ishdan chiqmaydi, to'g'rimi?)

U qanday ishlaydi?

Algoritm quyidagicha:

  • Boshida ma'lumotlarni saqlash bilan aloqa o'rnatiladi va ma'lumotlar yuklanadi;
  • Ob'ekt modeli, birlamchi indekslar va relyatsion indekslar (1:1, 1:Ko'p) qurilgan;
  • Ob'ekt xususiyatlaridagi o'zgarishlar (INotifyPropertyChanged) va to'plamga elementlarni qo'shish yoki o'chirish (INotifyCollectionChanged) uchun obuna yaratiladi;
  • Obuna ishga tushirilganda, o'zgartirilgan ob'ekt ma'lumotlarni saqlashga yozish uchun navbatga qo'shiladi;
  • Saqlashdagi o'zgarishlar vaqti-vaqti bilan (taymerda) fon oqimida saqlanadi;
  • Ilovadan chiqqaningizda, o'zgarishlar xotiraga ham saqlanadi.

Kod misoli

Kerakli bog'liqliklarni qo'shish

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

Biz xotirada saqlanadigan ma'lumotlar modelini tasvirlaymiz

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

Keyin ob'ekt modeli:

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

Va nihoyat, ma'lumotlarga kirish uchun ombor sinfining o'zi:

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

ObjectRepository misolini yarating:

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

Agar loyiha HangFire-dan foydalansa

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

Yangi ob'ektni kiritish:

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

Ushbu chaqiruv bilan ob'ekt Ota-ona modeli mahalliy keshga ham, ma'lumotlar bazasiga yozish uchun navbatga ham qo'shiladi. Shuning uchun bu operatsiya O(1) ni oladi va bu obyekt bilan darhol ishlash mumkin.

Masalan, ushbu ob'ektni omborda topish va qaytarilgan ob'ekt bir xil misol ekanligini tekshirish uchun:

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

Nimalar bo'lyapti? Oʻrnatish () qaytadi Jadval lug'ati, o'z ichiga oladi ConcurrentDictionary va asosiy va ikkilamchi indekslarning qo'shimcha funksionalligini ta'minlaydi. Bu sizga barcha ob'ektlarni to'liq takrorlamasdan Id (yoki boshqa ixtiyoriy foydalanuvchi indekslari) bo'yicha qidirish usullariga ega bo'lish imkonini beradi.

Ob'ektlarni qo'shganda ObjectRepository ularning xususiyatlarini o'zgartirish uchun obuna qo'shiladi, shuning uchun xususiyatlarning har qanday o'zgarishi ham ushbu ob'ektni yozish navbatiga qo'shilishiga olib keladi. 
Xususiyatlarni tashqi tomondan yangilash POCO ob'ekti bilan ishlash bilan bir xil ko'rinadi:

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

Ob'ektni quyidagi usullar bilan o'chirishingiz mumkin:

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

Bu, shuningdek, ob'ektni o'chirish navbatiga qo'shadi.

Saqlash qanday ishlaydi?

ObjectRepository kuzatilayotgan ob'ektlar o'zgarganda (qo'shish yoki o'chirish yoki xususiyatlarni o'zgartirish), hodisani keltirib chiqaradi Model o'zgartirildiobuna bo'lgan ISsaqlash. Amalga oshirish ISsaqlash voqea sodir bo'lganda Model o'zgartirildi o'zgartirishlar 3 ta navbatga qo'yiladi - qo'shish, yangilash va o'chirish uchun.

Shuningdek, amalga oshirishlar ISsaqlash ishga tushirilganda, ular har 5 soniyada o'zgarishlarni saqlashga olib keladigan taymer yaratadilar. 

Bundan tashqari, qo'ng'iroqni saqlashga majburlash uchun API mavjud: ObjectRepository.Save().

Har bir saqlashdan oldin birinchi navbatda ma'nosiz operatsiyalar navbatlardan o'chiriladi (masalan, takroriy hodisalar - ob'ekt ikki marta o'zgartirilganda yoki ob'ektlar tezda qo'shilgan/o'chirilganda) va shundan keyingina saqlashning o'zi. 

Barcha holatlarda, butun joriy ob'ekt saqlanadi, shuning uchun ob'ektlar o'zgartirilganidan boshqacha tartibda, jumladan, navbatga qo'shilgan vaqtga nisbatan yangiroq versiyalarda saqlangan bo'lishi mumkin.

Yana nima bor?

  • Barcha kutubxonalar .NET Standard 2.0 ga asoslangan. Har qanday zamonaviy .NET loyihasida foydalanish mumkin.
  • API ip xavfsizdir. Ichki kollektsiyalar asosida amalga oshiriladi ConcurrentDictionary, hodisa ishlov beruvchilarida qulflar mavjud yoki ularga kerak emas. 
    Esda tutish kerak bo'lgan yagona narsa - qo'ng'iroq qilish ObjectRepository.Save();
  • Ixtiyoriy indekslar (o'ziga xoslikni talab qiladi):

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

Kim foydalanadi?

Shaxsan men ushbu yondashuvni barcha sevimli mashg'ulotlarida qo'llashni boshladim, chunki bu qulay va ma'lumotlarga kirish qatlamini yozish yoki og'ir infratuzilmani joylashtirish uchun katta xarajatlarni talab qilmaydi. Shaxsan men uchun odatda litedb yoki faylda ma'lumotlarni saqlash etarli. 

Ammo o'tmishda, endi tugatilgan EscapeTeams startapi (Men bu erda pul deb o'yladim - lekin yo'q, yana tajriba) - Azure Table Storage-da ma'lumotlarni saqlash uchun ishlatiladi.

Kelajak uchun rejalar

Men ushbu yondashuvning asosiy kamchiliklaridan birini - gorizontal o'lchovni tuzatmoqchiman. Buni amalga oshirish uchun sizga taqsimlangan tranzaktsiyalar kerak (sic!) Yoki turli instansiyalardagi bir xil ma'lumotlar o'zgarmasligi to'g'risida ixtiyoriy qaror qabul qiling yoki ularni "kim oxirgi bo'lsa, to'g'ri" tamoyiliga muvofiq o'zgartirishga ruxsat bering.

Texnik nuqtai nazardan, men iloji boricha quyidagi sxemani ko'raman:

  • Ob'ekt modeli o'rniga EventLog va Snapshotni saqlang
  • Boshqa misollarni toping (barcha misollarning soʻnggi nuqtalarini sozlamalarga qoʻshing? udp Discovery? master/slave?)
  • RAFT kabi har qanday konsensus algoritmi orqali EventLog misollari orasida takrorlang.

Yana bir muammo meni xavotirga solmoqda - kaskadli o'chirish yoki boshqa ob'ektlar bilan bog'langan ob'ektlarni o'chirish holatlarini aniqlash. 

Manba kodi

Agar siz bu yergacha o'qigan bo'lsangiz, kodni o'qish qoladi, uni GitHub da topish mumkin:
https://github.com/DiverOfDark/ObjectRepository

Manba: www.habr.com

a Izoh qo'shish