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

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


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


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


أسوأ موقف هو عندما تعمل في فريق ، ولا يستطيع أحد الزملاء بناء استفسارات سريعة. كم من الوقت قضيته في حل مشاكل N + 1 وبناء فهارس إضافية بحيث تم تحديد SELECT في الصفحة الرئيسية في وقت معقول؟


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


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


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


الايجابيات:


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

سلبيات:


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

كيف يعمل؟


الخوارزمية هي كما يلي:


  • في البداية ، يتم إنشاء اتصال بمستودع البيانات ، ويتم تنزيل البيانات ؛
  • نموذج كائن ، فهارس أساسية ، ومؤشرات علاقة (1: 1 ، 1: كثير) مبنية ؛
  • يتم إنشاء اشتراك لتغيير خصائص الكائنات (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());  }    //   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)); 

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


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


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

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


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

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


كيف يعمل الحفظ؟


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


أيضًا ، تقوم تطبيقات IStorage أثناء التهيئة بإنشاء مؤقت يؤدي كل 5 ثوانٍ إلى حفظ التغييرات.


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


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


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


ماذا هناك؟


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

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

من يستخدمه؟


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


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


الخطط المستقبلية


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


من الناحية الفنية ، أرى أن المخطط التالي ممكن:


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

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


شفرة المصدر


إذا قرأت ما يصل إلى هنا - فتبقى الكود الوحيد المطلوب قراءته ، يمكن أن يكون كذلك
وجدت على جيثب .

Source: https://habr.com/ru/post/ar452232/


All Articles