إبطال ذاكرة التخزين المؤقت في ريلز على مستوى الحقل: حل DSL
نشرت: 2022-03-11في تطوير الويب الحديث ، يعد التخزين المؤقت طريقة سريعة وفعالة لتسريع الأمور. عند القيام بذلك بشكل صحيح ، يمكن أن يؤدي التخزين المؤقت إلى تحسينات كبيرة على الأداء العام لتطبيقك. عندما يتم القيام به بشكل خاطئ ، سينتهي بالتأكيد بكارثة.
يعد إبطال ذاكرة التخزين المؤقت ، كما تعلم ، واحدة من أصعب ثلاث مشاكل في علوم الكمبيوتر - يتمثل المشكلان الآخران في تسمية الأشياء والأخطاء الفردية. أحد الطرق السهلة للخروج هو إبطال كل شيء ، يسارًا ويمينًا ، كلما تغير شيء ما. لكن هذا يقضي على الغرض من التخزين المؤقت. أنت تريد إبطال ذاكرة التخزين المؤقت فقط عند الضرورة القصوى.
إذا كنت ترغب في تحقيق أقصى استفادة من التخزين المؤقت ، فأنت بحاجة إلى أن تكون دقيقًا جدًا بشأن ما تبطله وتحفظ تطبيقك من إهدار الموارد الثمينة على العمل المتكرر.
في منشور المدونة هذا ، ستتعلم أسلوبًا للتحكم بشكل أفضل في كيفية عمل ذاكرات التخزين المؤقت لـ Rails: على وجه التحديد ، تنفيذ إبطال ذاكرة التخزين المؤقت على مستوى الحقل. تعتمد هذه التقنية على Rails ActiveRecord و ActiveSupport::Concern
بالإضافة إلى التلاعب بسلوك أسلوب touch
.
تستند مشاركة المدونة هذه إلى تجاربي الأخيرة في مشروع شهدنا فيه تحسنًا ملحوظًا في الأداء بعد تنفيذ إبطال ذاكرة التخزين المؤقت على مستوى الحقل. ساعد في تقليل إبطال ذاكرة التخزين المؤقت غير الضرورية والعرض المتكرر للقوالب.
القضبان والياقوت والأداء
روبي ليست أسرع لغة ، لكنها بشكل عام خيار مناسب فيما يتعلق بسرعة التطوير. علاوة على ذلك ، فإن إمكانيات البرمجة الوصفية وإمكانيات اللغة الخاصة بالمجال المضمنة (DSL) تمنح المطور مرونة هائلة.
هناك دراسات مثل دراسة جاكوب نيلسن التي توضح لنا أنه إذا استغرقت المهمة أكثر من 10 ثوانٍ ، فسوف نفقد تركيزنا. واستعادة تركيزنا يستغرق وقتًا. لذلك يمكن أن يكون هذا مكلفًا بشكل غير متوقع.
لسوء الحظ ، في Ruby on Rails ، من السهل جدًا تجاوز حد العشر ثوانٍ مع إنشاء القوالب. لن ترى هذا يحدث في أي تطبيق "مرحبًا بالعالم" أو مشروع حيوان أليف صغير الحجم ، ولكن في مشاريع العالم الحقيقي حيث يتم تحميل الكثير من الأشياء على صفحة واحدة ، صدقوني ، يمكن أن يبدأ إنشاء النماذج في السحب بسهولة.
وهذا بالضبط ما كان عليّ حله في مشروعي.
تحسينات بسيطة
ولكن كيف بالضبط تقوم بتسريع الأمور؟
الجواب: المعيار والتحسين.
في مشروعي ، هناك خطوتان فعالتان للغاية في التحسين وهما:
- حذف N + 1 من الاستفسارات
- تقديم تقنية تخزين مؤقت جيدة للقوالب
استعلامات N + 1
يعد إصلاح استعلامات N + 1 أمرًا سهلاً. ما يمكنك القيام به هو التحقق من ملفات السجل - كلما رأيت استعلامات SQL متعددة مثل تلك الواردة أدناه في سجلاتك ، قم بإزالتها عن طريق استبدالها بتحميل فوري:
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
هناك جوهرة لهذا تسمى الرصاصة للمساعدة في اكتشاف عدم الكفاءة هذا. يمكنك أيضًا استعراض كل حالة من حالات الاستخدام ، وفي غضون ذلك ، تحقق من السجلات من خلال فحصها وفقًا للنمط أعلاه. من خلال القضاء على جميع أوجه القصور في N + 1 ، يمكنك أن تكون واثقًا بدرجة كافية من أنك لن تفرط في تحميل قاعدة البيانات الخاصة بك وسوف ينخفض الوقت الذي تقضيه في ActiveRecord بشكل كبير.
بعد إجراء هذه التغييرات ، كان مشروعي يعمل بالفعل بسرعة أكبر. لكنني قررت الانتقال إلى المستوى التالي ومعرفة ما إذا كان بإمكاني خفض وقت التحميل هذا إلى أبعد من ذلك. كان لا يزال هناك قدر لا بأس به من العرض غير الضروري يحدث في القوالب ، وفي النهاية ، هذا هو المكان الذي ساعد فيه التخزين المؤقت للأجزاء.
جزء التخزين المؤقت
يساعد التخزين المؤقت للأجزاء بشكل عام في تقليل وقت إنشاء القالب بشكل ملحوظ. لكن سلوك ذاكرة التخزين المؤقت الافتراضية في ريلز لم يكن يقطعها لمشروعي.
الفكرة وراء التخزين المؤقت لأجزاء ريلز رائعة. يوفر آلية تخزين مؤقت بسيطة وفعالة للغاية.
كتب مؤلفو روبي أون ريلز مقالة جيدة جدًا في Signal v. Noise حول كيفية عمل التخزين المؤقت للشظايا.
لنفترض أن لديك القليل من واجهة المستخدم التي تعرض بعض حقول الكيان.
- عند تحميل الصفحة ، يحسب ريلز
cache_key
بناءً على فئة الكيان والحقلupdated_at
. - باستخدام هذا
cache_key
، فإنه يتحقق لمعرفة ما إذا كان هناك أي شيء في ذاكرة التخزين المؤقت المرتبطة بهذا المفتاح. - إذا لم يكن هناك أي شيء في ذاكرة التخزين المؤقت ، فسيتم تقديم رمز HTML لهذا الجزء للعرض (ويتم تخزين المحتوى الذي تم تقديمه حديثًا في ذاكرة التخزين المؤقت).
- إذا كان هناك أي محتوى موجود في ذاكرة التخزين المؤقت باستخدام هذا المفتاح ، فسيتم تقديم العرض بمحتويات ذاكرة التخزين المؤقت.
هذا يعني أن ذاكرة التخزين المؤقت لا تحتاج أبدًا إلى إبطالها بشكل صريح. كلما قمنا بتغيير الكيان وإعادة تحميل الصفحة ، يتم تقديم محتوى ذاكرة التخزين المؤقت الجديد للكيان.
توفر ريلز ، بشكل افتراضي ، أيضًا القدرة على إبطال ذاكرة التخزين المؤقت للكيانات الرئيسية في حالة تغيير الطفل:
belongs_to :parent_entity, touch: true
هذا ، عند تضمينه في النموذج ، سوف يلمس الوالد تلقائيًا عند لمس الطفل. يمكنك معرفة المزيد عن touch
هنا. من خلال هذا ، توفر لنا ريلز طريقة بسيطة وفعالة لإبطال ذاكرة التخزين المؤقت للكيانات الأم في نفس الوقت مع ذاكرة التخزين المؤقت للكيانات الفرعية.
التخزين المؤقت في القضبان
ومع ذلك ، يتم إنشاء التخزين المؤقت في ريلز لخدمة واجهات المستخدم حيث يحتوي جزء HTML الذي يمثل الكيان الأصلي على أجزاء HTML تمثل الكيانات الفرعية للجهة الرئيسية فقط. بمعنى آخر ، لا يمكن أن يحتوي جزء HTML الذي يمثل الكيانات الفرعية في هذا النموذج على حقول من الكيان الأصلي.
لكن هذا ليس ما يحدث في العالم الحقيقي. قد تحتاج إلى القيام بأشياء في تطبيق ريلز الخاص بك تنتهك هذا الشرط.
كيف يمكنك التعامل مع الموقف حيث تعرض واجهة المستخدم حقول الكيان الرئيسي داخل جزء HTML الذي يمثل الكيان الفرعي؟
إذا كان الطفل يحتوي على حقول من الكيان الأصلي ، فأنت في مشكلة مع سلوك إبطال ذاكرة التخزين المؤقت الافتراضي لريلز.

في كل مرة يتم فيها تعديل الحقول المقدمة من الكيان الأصلي ، ستحتاج إلى لمس جميع الكيانات الفرعية التي تنتمي إلى هذا الكيان الأصل. على سبيل المثال ، إذا تم تعديل Parent1
، فستحتاج إلى التأكد من إلغاء صلاحية ذاكرة التخزين المؤقت لطريقة العرض Child1
و Child2
.
من الواضح أن هذا يمكن أن يتسبب في اختناق كبير في الأداء. سيؤدي لمس كل كيان فرعي كلما تغير أحد الوالدين إلى الكثير من استعلامات قاعدة البيانات بدون سبب وجيه.
سيناريو آخر مشابه هو عندما تم تقديم الكيانات المرتبطة has_and_belongs_to
في القائمة ، وبدأ تعديل هذه الكيانات سلسلة من إبطال ذاكرة التخزين المؤقت من خلال سلسلة الاقتران.
class Event < ActiveRecord::Base has_many :participants has_many :users, through: :participants end class Participant < ActiveRecord::Base belongs_to :event belongs_to :user end class User < ActiveRecord::Base has_many :participants has_many :events, through :participants end
لذلك ، بالنسبة لواجهة المستخدم أعلاه ، سيكون من غير المنطقي لمس المشارك أو الحدث عندما يتغير موقع المستخدم. لكن يجب أن نتطرق إلى كل من الحدث والمشارك عندما يتغير اسم المستخدم ، أليس كذلك؟
لذا فإن الأساليب الموجودة في مقالة Signal v. Noise غير فعالة بالنسبة لبعض مثيلات UI / UX ، كما هو موضح أعلاه.
على الرغم من أن ريلز فعالة للغاية بالنسبة للأشياء البسيطة ، إلا أن المشروعات الحقيقية لها مضاعفاتها الخاصة.
حقل مستوى ريلز إبطال ذاكرة التخزين المؤقت
في مشاريعي ، كنت أستخدم Ruby DSL صغيرًا للتعامل مع مواقف مثل المذكورة أعلاه. يتيح لك تحديد الحقول التي ستؤدي إلى إبطال ذاكرة التخزين المؤقت من خلال الارتباطات بشكل إعلاني.
دعنا نلقي نظرة على بعض الأمثلة حيث تساعد حقًا:
مثال 1:
class Event < ActiveRecord::Base include Touchable ... has_many :tasks ... touch :tasks, in_case_of_modified_fields: [:name] ... end class Task < ActiveRecord::Base belongs_to :event end
يستفيد هذا المقتطف من قدرات البرمجة الوصفية وإمكانيات DSL الداخلية في Ruby.
لكي نكون أكثر تحديدًا ، لن يؤدي إلا تغيير الاسم في الحدث إلى إبطال ذاكرة التخزين المؤقت لأجزاء المهام ذات الصلة. لن يؤدي تغيير الحقول الأخرى للحدث - مثل الغرض أو الموقع - إلى إبطال ذاكرة التخزين المؤقت لجزء المهمة. أود أن أطلق على هذا التحكم الدقيق في إبطال ذاكرة التخزين المؤقت على مستوى الحقل .
المثال 2:
دعنا نلقي نظرة على مثال يوضح إبطال ذاكرة التخزين المؤقت من خلال سلسلة الارتباط has_many
.
يُظهر جزء واجهة المستخدم الموضح أدناه مهمة ومالكها:
بالنسبة لواجهة المستخدم هذه ، يجب إلغاء صلاحية جزء HTML الذي يمثل المهمة فقط عندما تتغير المهمة أو عندما يتغير اسم المالك. إذا تغيرت جميع الحقول الأخرى للمالك (مثل المنطقة الزمنية أو التفضيلات) ، فيجب ترك ذاكرة التخزين المؤقت للمهمة كما هي.
يتم تحقيق ذلك باستخدام DSL الموضح هنا:
class User < ActiveRecord::Base include Touchable touch :tasks, in_case_of_modified_fields: [:first_name, :last_name] ... end class Task < ActiveRecord::Base has_one owner, class_name: :User end
تنفيذ DSL
الجوهر الرئيسي لـ DSL هو طريقة touch
. الوسيطة الأولى هي الارتباط ، والوسيطة التالية هي قائمة الحقول التي تؤدي إلى touch
على هذا الارتباط:
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
يتم توفير هذه الطريقة من خلال وحدة Touchable
:
module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end
في هذا الكود ، النقطة الأساسية هي أننا نقوم بتخزين وسيطات استدعاء touch
. وبعد ذلك ، قبل حفظ الكيان ، نقوم بوضع علامة "سيئ" على الاقتران إذا تم تعديل الحقل المحدد. نلمس الكيانات في هذا الاتحاد بعد الحفظ إذا كانت الجمعية متسخة.
ثم ، الجزء الخاص من القلق هو:
... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present? end …
في طريقة check_touchable_entities
، نتحقق مما إذا كان الحقل المعلن قد تغير . إذا كان الأمر كذلك ، فإننا نضع علامة على الارتباط على أنه متسخ عن طريق تعيين meta_info[association]
على " true
".
بعد ذلك ، بعد حفظ الكيان ، نتحقق من جمعياتنا القذرة ونتطرق إلى الكيانات الموجودة فيها إذا لزم الأمر:
… def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …
وهذا هو عليه! يمكنك الآن تنفيذ إبطال ذاكرة التخزين المؤقت على مستوى الحقل في ريلز باستخدام DSL بسيط.
خاتمة
يعد التخزين المؤقت في ريلز بتحسينات في الأداء في تطبيقك بسهولة نسبية. ومع ذلك ، يمكن أن تكون التطبيقات الواقعية معقدة وغالبًا ما تشكل تحديات فريدة. يعمل سلوك ذاكرة التخزين المؤقت الافتراضية في ريلز بشكل جيد مع معظم السيناريوهات ، ولكن هناك سيناريوهات معينة حيث يمكن أن يؤدي المزيد من التحسين في إبطال ذاكرة التخزين المؤقت إلى قطع شوط طويل.
الآن بعد أن عرفت كيفية تنفيذ إبطال ذاكرة التخزين المؤقت على مستوى الحقل في ريلز ، يمكنك منع إبطال ذاكرة التخزين المؤقت غير الضرورية في تطبيقك.