ObjectRepository - نمط مستودع الذاكرة الداخلي لمشاريعك المنزلية

لماذا تخزين كافة البيانات في الذاكرة؟

لتخزين بيانات موقع الويب أو الواجهة الخلفية، ستكون الرغبة الأولى لمعظم الأشخاص العقلاء هي اختيار قاعدة بيانات SQL. 

ولكن في بعض الأحيان يتبادر إلى الذهن فكرة أن نموذج البيانات غير مناسب لـ SQL: على سبيل المثال، عند إنشاء بحث أو رسم بياني اجتماعي، تحتاج إلى البحث عن علاقات معقدة بين الكائنات. 

أسوأ موقف هو عندما تعمل ضمن فريق ولا يعرف زميلك كيفية إنشاء استعلامات سريعة. ما هو مقدار الوقت الذي أمضيته في حل مشكلات N+1 وإنشاء فهارس إضافية حتى يكتمل التحديد الموجود على الصفحة الرئيسية في فترة زمنية معقولة؟

هناك طريقة شائعة أخرى وهي NoSQL. منذ عدة سنوات كان هناك الكثير من الضجيج حول هذا الموضوع - وفي أي مناسبة مناسبة قاموا بنشر MongoDB وكانوا سعداء بالإجابات في شكل مستندات json (بالمناسبة، كم عدد العكازات التي كان عليك إدخالها بسبب الروابط الدائرية الموجودة في المستندات؟).

أقترح تجربة طريقة بديلة أخرى - لماذا لا تحاول تخزين كافة البيانات في ذاكرة التطبيق، وحفظها بشكل دوري في وحدة تخزين عشوائية (ملف، قاعدة بيانات بعيدة)؟ 

لقد أصبحت الذاكرة رخيصة الثمن، وأي بيانات ممكنة لمعظم المشاريع الصغيرة والمتوسطة الحجم سوف تتناسب مع 1 غيغابايت من الذاكرة. (على سبيل المثال، مشروعي المنزلي المفضل هو تعقب مالي، الذي يحتفظ بالإحصائيات اليومية وتاريخ نفقاتي وأرصدتي ومعاملاتي لمدة عام ونصف، ويستهلك 45 ميجابايت فقط من الذاكرة.)

الايجابيات:

  • أصبح الوصول إلى البيانات أسهل - لا داعي للقلق بشأن الاستعلامات، والتحميل البطيء، وميزات ORM، فأنت تعمل مع كائنات C# العادية؛
  • لا توجد مشاكل مرتبطة بالوصول من سلاسل رسائل مختلفة؛
  • سريع جدًا - لا توجد طلبات شبكة، ولا ترجمة للتعليمات البرمجية إلى لغة استعلام، ولا حاجة إلى (إلغاء) تسلسل الكائنات؛
  • من المقبول تخزين البيانات بأي شكل من الأشكال - سواء كان ذلك في ملف XML على القرص، أو في SQL Server، أو في Azure Table Storage.

سلبيات:

  • يتم فقدان القياس الأفقي، ونتيجة لذلك، لا يمكن إجراء نشر التوقف الصفري؛
  • إذا تعطل التطبيق، فقد تفقد البيانات جزئيًا. (لكن تطبيقنا لا يتعطل أبدًا، أليس كذلك؟)

كيف يعمل؟

الخوارزمية هي على النحو التالي:

  • في البداية، يتم إنشاء اتصال مع مخزن البيانات، ويتم تحميل البيانات؛
  • يتم إنشاء نموذج الكائن والفهارس الأساسية والفهارس العلائقية (1:1، 1:Many)؛
  • يتم إنشاء اشتراك للتغييرات في خصائص الكائن (INotifyPropertyChanged) ولإضافة أو إزالة عناصر إلى المجموعة (INotifyCollectionChanged)؛
  • عند تشغيل الاشتراك، تتم إضافة الكائن الذي تم تغييره إلى قائمة الانتظار للكتابة إلى مخزن البيانات؛
  • يتم حفظ التغييرات التي يتم إجراؤها على وحدة التخزين بشكل دوري (مؤقتًا) في سلسلة رسائل في الخلفية؛
  • عند الخروج من التطبيق، يتم حفظ التغييرات أيضًا في وحدة التخزين.

مثال رمز

إضافة التبعيات اللازمة

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

نحن نصف نموذج البيانات الذي سيتم تخزينه في وحدة التخزين

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

ثم نموذج الكائن:

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

وأخيرًا، فئة المستودع نفسها للوصول إلى البيانات:

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:

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

إذا كان المشروع سيستخدم HangFire

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

إدراج كائن جديد:

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

مع هذه الدعوة، الكائن ParentModel تتم إضافته إلى كل من ذاكرة التخزين المؤقت المحلية وقائمة الانتظار للكتابة إلى قاعدة البيانات. لذلك، تستغرق هذه العملية O(1)، ويمكن العمل مع هذا الكائن على الفور.

على سبيل المثال، للعثور على هذا الكائن في المستودع والتحقق من أن الكائن الذي تم إرجاعه هو نفس المثيل:

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

ما يحدث؟ تعيين () عائدات قاموس الجدول، الذي يحتوي على القاموس المتزامن ويوفر وظائف إضافية للفهارس الأولية والثانوية. يتيح لك هذا الحصول على طرق للبحث عن طريق المعرف (أو فهارس المستخدم العشوائية الأخرى) دون التكرار الكامل على جميع الكائنات.

عند إضافة كائنات إلى مستودع الكائن تتم إضافة اشتراك لتغيير خصائصها، لذا فإن أي تغيير في الخصائص يؤدي أيضًا إلى إضافة هذا الكائن إلى قائمة انتظار الكتابة. 
يبدو تحديث الخصائص من الخارج مثل العمل مع كائن POCO:

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

يمكنك حذف كائن بالطرق التالية:

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

يؤدي هذا أيضًا إلى إضافة الكائن إلى قائمة انتظار الحذف.

كيف يعمل الادخار؟

مستودع الكائن عندما تتغير الكائنات المراقبة (إما بإضافة أو حذف أو تغيير الخصائص)، يُطلق حدثًا تم تغيير النموذجالاشتراك في تخزين. التنفيذ تخزين عند وقوع حدث ما تم تغيير النموذج يتم وضع التغييرات في 3 قوائم انتظار - للإضافة والتحديث والحذف.

وكذلك التنفيذات تخزين عند التهيئة، يقومون بإنشاء مؤقت يؤدي إلى حفظ التغييرات كل 5 ثوانٍ. 

بالإضافة إلى ذلك، هناك واجهة برمجة التطبيقات (API) لفرض استدعاء الحفظ: ObjectRepository.Save().

قبل كل عملية حفظ، تتم أولاً إزالة العمليات التي لا معنى لها من قوائم الانتظار (على سبيل المثال، الأحداث المكررة - عند تغيير كائن مرتين أو إضافة/إزالة كائنات بسرعة)، وعندها فقط يتم الحفظ نفسه. 

في جميع الحالات، يتم حفظ الكائن الحالي بالكامل، لذلك من الممكن أن يتم حفظ الكائنات بترتيب مختلف عن الترتيب الذي تم تغييره فيه، بما في ذلك الإصدارات الأحدث من الكائنات عما كانت عليه في وقت إضافتها إلى قائمة الانتظار.

ماذا يوجد هناك أيضآ؟

  • تعتمد كافة المكتبات على .NET Standard 2.0. يمكن استخدامها في أي مشروع .NET حديث.
  • واجهة برمجة التطبيقات (API) آمنة للخيط. يتم تنفيذ التحصيلات الداخلية على أساس القاموس المتزامن، إما أن معالجات الأحداث لديها أقفال أو لا تحتاج إليها. 
    الشيء الوحيد الذي يستحق التذكر هو الاتصال ObjectRepository.Save();
  • الفهارس التعسفية (تتطلب التفرد):

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

من يستخدمه؟

أنا شخصياً بدأت باستخدام هذا الأسلوب في جميع مشاريع الهوايات لأنه مريح ولا يتطلب نفقات كبيرة لكتابة طبقة الوصول إلى البيانات أو نشر البنية التحتية الثقيلة. شخصيًا، عادةً ما يكون تخزين البيانات في litdb أو ملف كافيًا بالنسبة لي. 

ولكن في الماضي، عندما بدأت شركة EscapeTeams الناشئة (اعتقدت أن هذا هو المال - ولكن لا، الخبرة مرة أخرى) - يستخدم لتخزين البيانات في Azure Table Storage.

خطط للمستقبل

أرغب في إصلاح أحد العيوب الرئيسية لهذا النهج - القياس الأفقي. للقيام بذلك، تحتاج إما إلى معاملات موزعة (هكذا!)، أو اتخاذ قرار قوي الإرادة بعدم تغيير نفس البيانات من حالات مختلفة، أو السماح لها بالتغيير وفقًا لمبدأ "من هو الأخير على حق".

من وجهة نظر فنية، أرى أن المخطط التالي ممكن:

  • قم بتخزين EventLog وSnapshot بدلاً من نموذج الكائن
  • ابحث عن مثيلات أخرى (أضف نقاط النهاية لجميع المثيلات إلى الإعدادات؟ اكتشاف UDP؟ رئيسي/تابع؟)
  • النسخ المتماثل بين مثيلات EventLog عبر أي خوارزمية توافقية، مثل RAFT.

هناك أيضًا مشكلة أخرى تقلقني - الحذف المتتالي، أو الكشف عن حالات حذف الكائنات التي لها روابط من كائنات أخرى. 

شفرة المصدر

إذا كنت قد قرأت حتى هنا، فكل ما تبقى هو قراءة الكود، ويمكن العثور عليه على GitHub:
https://github.com/DiverOfDark/ObjectRepository

المصدر: www.habr.com

إضافة تعليق