ردود نداء Active Record في ريلز

من موسوعة حسوب
مراجعة 09:40، 24 مارس 2019 بواسطة جميل-بيلوني (نقاش | مساهمات)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)

ستتعلم في هذا الدليل كل ما يتعلَّق بدورة حياة كائنات Active Record. بعد قراءة هذا الدليل، ستتعرَّف على:

  • دورة حياة كائنات Active Record.
  • كيفية إنشاء توابع ردود النداء التي تستجيب إلى الأحداث في دورة حياة الكائن.
  • كيفية إنشاء أصناف خاصة تُغلِّف سلوكًا مشتركًا من أجل ردود النداء الخاصة بك.

دورة حياة الكائن

خلال عملية عادية في تطبيق ريلز، يمكن للكائنات أن تُنشأ، تُحدّث، أو تُدمّر. يزوّدك Active Record بنقاط الوصول اللازمة في دورة حياة الكائن حتى تتمكن من التحكم بتطبيقك وبياناته.

تمكّنك توابع رد النداء من تنفيذ عمليات قبل أو بعد انتقال الكائنات من حالة إلى أخرى.

نظرة عامة على ردود النداء

إن ردود النداء هي توابع يتم استدعاؤها عند نقاط ولحظات معينة من دورة حياة الكائن. باستخدمها، يمكنك كتابة التعليمات التي سُتنفّذ عند إنشاء الكائنات أو حفظها أو تحديثها أو حذفها أو التحقق منها أو تحميلها من قاعدة البيانات.

تسجيل رد النداء

من أجل التمكن من استخدام توابع رد النداء المتاحة، يجب عليك تسجيلها أولًا. يمكنك كتابة هذه التوابع كتوابع عادية واستخدام تابع صنف بنمط الماكرو (macro-style class) لتسجيلها كتوابع رد نداء:

class User < ApplicationRecord
  validates :login, :email, presence: true
 
  before_validation :ensure_login_has_a_value
 
  private
    def ensure_login_has_a_value
      if login.nil?
        self.login = email unless email.blank?
      end
    end
end

إن توابع الأصناف بنمط الماكرو تستقبل أيضًا كتلةً. يمكنك استخدام هذا النمط عندما يكون عدد التعليمات داخل الكتلة صغير جدًا لدرجة أنّه يتّسع بسطر وحيد:

class User < ApplicationRecord
  validates :login, :email, presence: true
 
  before_create do
    self.name = login.capitalize if name.blank?
  end
end

يمكن أيضًا تسجيل توابع رد النداء لتُشغّل فقط في أحداث معينة ضمن دورة الحياة:

class User < ApplicationRecord
  before_validation :normalize_name, on: :create
 
  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]
 
  private
    def normalize_name
      self.name = name.downcase.titleize
    end
 
    def set_location
      self.location = LocationService.query(self)
    end
end

من المفضّل تعريف توابع رد النداء كتوابع خاصة (private). ففي حال تركها عامة (public)، يمكن استدعاؤها من خارج النموذج مما يتعارض مع مبدأ تغليف الكائنات (object encapsulation).

ردود النداء المتاحة

إليك قائمة بكل توابع رد النداء المتاحة، مرتّبة بذات الترتيب الذي به ستُشغّل هذه التوابع خلال العمليات المرافقة:

إنشاء كائن

  • before_validation (قبل التحقق)
  • after_validation (بعد التحقق)
  • before_save (قبل الحفظ)
  • around_save (خلال الحفظ)
  • before_create (قبل الإنشاء)
  • around_create (خلال الإنشاء)
  • after_create (بعد الإنشاء)
  • after_save (بعد الحفظ)
  • after_commit/after_rollback (بعد التراجع / بعد التسليم)

تحديث كائن

  • before_validation (قبل التحقق)
  • after_validation (بعد التحقق)
  • before_save (قبل الحفظ)
  • around_save (خلال الحفظ)
  • before_update (قبل التحديث)
  • around_update (خلال التحديث)
  • after_update (بعد التحديث)
  • after_save (بعد الحفظ)
  • after_commit/after_rollback (بعد التراجع / بعد التسليم)

تدمير كائن

  • before_destroy (قبل التدمير)
  • around_destroy (خلال التدمير)
  • after_destroy (بعد التدمير)
  • after_commit/after_rollback (بعد التراجع / بعد التسليم)

تحذير: تشغّل after_save بعد الإنشاء وبعد التعديل، لكن دائمًا بعد توابع رد النداء المحددة after_create و after_update، بغض النظر عن ترتيب تشغيل توابع الماكرو.

ملاحظة: يجب وضع ردود النداء before_destroy قبل الارتباطات dependent: :destroy (أو استخدام الخيار prepend: true)، للتأكد من أنها تُشغّل قبل حذف السجلات عبر الخيار dependent: :destroy.

ردا النداء after_initialize و after_find

يُشغّل تابع رد النداء after_initialize متى ما تمّت تهيئة كائن من Active Record، سواءً باستخدام التابع new مباشرةً أو من خلال تحميل كائن من قاعدة البيانات. من المفضّل تفادي إعادة كتابة التابع initialize الخاص بأصناف Active Record.

يُشغّل التابع after_find متى ما تم تحميل كائن من قاعدة البيانات؛ يشغّل هذا التابع قبل التابع after_initialize في حال تعريفهما كلاهما سويًا.

لا تملك التوابع after_initialize و after_find نظراء من النمط before_*‎، لكن يمكن تسجيلها مثل توابع رد النداء العادية.

class User < ApplicationRecord
  after_initialize do |user|
    puts "You have initialized an object!"
  end
 
  after_find do |user|
    puts "You have found an object!"
  end
end
 
>> User.new
You have initialized an object!
=> #<User id: nil>
 
>> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>

رد النداء after_touch

يشغّل التابع after_touch عندما يتم المساس (تحديث من ارتباط) بأي كائن في Active Record.

class User < ApplicationRecord
  after_touch do |user|
    puts "You have touched an object"
  end
end
 
>> u = User.create(name: 'Kuldeep')
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
 
>> u.touch
You have touched an object
=> true

يمكن استخدامه مع belongs_to:

class Employee < ApplicationRecord
  belongs_to :company, touch: true
  after_touch do
    puts 'An Employee was touched'
  end
end
 
class Company < ApplicationRecord
  has_many :employees
  after_touch :log_when_employees_or_company_touched
 
  private
  def log_when_employees_or_company_touched
    puts 'Employee/Company was touched'
  end
end
 
>> @employee = Employee.last
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
 
# triggers @employee.company.touch
>> @employee.touch
Employee/Company was touched
An Employee was touched
=> true

تشغيل توابع رد النداء

تشغّل التوابع التالية توابع رد النداء المسجلة:

  • create
  • create!‎
  • destroy
  • destroy!‎
  • destroy_all
  • save
  • save!‎
  • save(validate: false)‎
  • toggle!‎
  • touch
  • update_attribute
  • update
  • update!‎
  • valid?‎

علاوةً على ذلك، يشغّل تابع رد النداء after_find عن طريق توابع الإيجاد التالية:

  • all
  • first
  • find
  • find_by
  • find_by_*‎
  • find_by_*!‎
  • find_by_sql
  • last

يشغّل تابع رد النداء after_initialize عندما تتم تهيئة كائن جديد من الصنف.

ملاحظة: التوابع *_find_by و !*_find_by هي توابع ديناميكية تولّد من أجل كل حقل. تعلّم المزيد عنها في قسم توابع الإيجاد الديناميكية.

تجاوز توابع رد النداء

كما هو الحال في التأكيدات، يمكن تجاوز توابع رد النداء عن طريق استخدام التوابع التالية:

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • increment
  • increment_counter
  • toggle
  • update_column
  • update_columns
  • update_all
  • update_counters

لكن يجب استخدام هذه التوابع بحذر، لأنّه قد يتم الاحتفاظ ببعض قواعد العمل المنطقية المهمة في توابع رد النداء التي تخص التطبيق، والذي يؤدي تجاوزها بدون حذر إلى احتمالية وجود بيانات غير صحيحة تتنافى مع منطق عمل التطبيق.

إيقاف التشغيل

في كل مرة تقوم بها بتسجيل توابع رد النداء في نماذجك، ستوضع في طابور من أجل التنفيذ. يضم هذا الطابور جميع عمليات التحقق، وتوابع رد النداء المسجلة، وعمليات قاعدة البيانات التي ستشغّل لنموذجك.

تُحاط سلسلة توابع رد النداء بأكملها ضمن عملية واحدة (transaction). وفي حال إطلاق أيٍّ من توابع رد النداء استثناء، سيتم إيقاف تشغيل سلسلة توابع رد النداء والتراجع عن تشغيلها (ROLLBACK). لإيقاف السلسلة بشكل قسري، استخدم:

throw :abort

تحذير: أي استثناء ليس من النوع ActiveRecord::Rollback أو ActiveRecord::RecordInvalid يتم إعادة إطلاقه من قبل ريلز بعد إيقاف تشغيل سلسلة توابع رد النداء. قد يؤدي إطلاق الاستثناءات المغايرة للنوع ActiveRecord::Rollback أو ActiveRecord::RecordInvalid لإيقاف عمل الشيفرة في حال استدعاء توابع غير متوقعة مثل save و update_attributes (التي تعيد عادةً القيم true أو false) أن تطلق استثناءً.

ردود النداء العلائقية

تعمل توابع رد النداء ضمن علاقات النماذج، كما يمكن تعريف هذه العلاقات من خلالها. بفرض أن لدينا مستخدم لديه أكثر من مقال، بحيث أن مقالات المستخدم يجب أن يتم تدميرها في حال تدمير المستخدم. لنضف أولًا تابع رد النداء after_destroy للنموذج User عن طريق علاقته مع الصنف Article:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end
 
class Article < ApplicationRecord
  after_destroy :log_destroy_action
 
  def log_destroy_action
    puts 'Article destroyed'
  end
end
 
>> user = User.first
=> #<User id: 1>
>> user.articles.create!
=> #<Article id: 1, user_id: 1>
>> user.destroy
Article destroyed
=> #<User id: 1>

ردود النداء الشرطية

كما هو الحال في عمليات التحقق من الصحة، يمكن أن نجعل استدعاء توابع رد النداء شرطيًا بناءً على تحقق شرط معين. يمكن تحقيق ذلك من خلال استخدام الخيارات if: و unless:، التي تقبل رمزًا، أو كائن من النوع Proc أو مصفوفة. يمكنك استخدام الخيار if: عندما تريد تحديد الشروط التي تشغّل توابع رد النداء بناءً عليها. وفي حال أردت تحديد الشروط التي تشغّل توابع رد النداء في حال عدم تحقيقها، يمكنك استخدام الخيار unless:.

استخدام الخيارات if: و unless: مع رمز

يمكنك ربط الخيارات if: و unless: مع رمز يوافق اسم تابع الشرط المنادى قبل تابع رد النداء. عند استخدام الخيار if:، لن يتم تشغيل تابع رد النداء في حال أعاد تابع الشرط القيمة false؛ أمّا عند استخدام الخيار unless:، لن يتم تشغيل تابع رد النداء في حال أعاد تابع الشرط القيمة true. هذا الخيار هو الأكثر شيوعًا. من الممكن استخدام هذا النمط من التسجيل لتسجيل مجموعة من التوابع الشرطية التي يجب مناداتها للتحقق من ضرورة تنفيذ تابع رد النداء.

class Order < ApplicationRecord
  before_save :normalize_card_number, if: :paid_with_card?
end

استخدام الخيارات if: و unless: مع الكائن Proc

أخيرًا، من الممكن ربط الخيارين if: و unless: مع الكائن Proc.  باستخدام الكائن Proc، يمكنك تنفيذ توابع رد النداء شرطيًا ضمن سطر من التعليمات بدلًا من تابع منفصل. هذا الخيار هو مفضّل لمحبّي إجراء عمليات التحقق في أسطر واحدة:

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: Proc.new { |order| order.paid_with_card? }
end

شروط متعددة لتوابع رد النداء

عند كتابة شروط لتوابع رد النداء، من الممكن دمج الخيارين if: و unless: في نفس تعريف تابع رد النداء:

class Comment < ApplicationRecord
  after_create :send_email_to_author, if: :author_wants_emails?,
    unless: Proc.new { |comment| comment.article.ignore_comments? }
end

أصناف توابع رد النداء

في بعض الأحيان، قد يلزم لتوابع رد النداء التي تكتبها أن تقوم بإعادة استخدامها ضمن نماذج أخرى. يمكّنك Active Record من كتابة الأصناف التي تحوي توابع رد النداء، وبالتالي تزوّدك بإمكانية إعادة استخدامها مرارًا وتكرارًا.

إليك مثال قمنا فيه بتعريف صنف يحوي التابع after_destroy من أجل نموذج من النوع PictureFile:

class PictureFileCallbacks
  def after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

عند تعريفها ضمن صنف، تستقبل توابع رد النداء كائن النموذج كوسيط لها. يمكننا الآن استخدام صنف تابع رد النداء في نموذجنا:

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks.new
end

لاحظ أننا بحاجة إلى تهيئة كائن جديد من النوع PictureFileCallbacks، بما أننا قمنا بتعريف توابع رد النداء كتوابع نسخ (instance methods) في الصنف. من المفيد القيام بذلك من أجل استخدام حالة الكائن المُهيّأ. لكن في معظم الأحيان، سيبدو أكثر منطقية لو قمنا بتعريف توابع رد النداء كتوابع أصناف (class methods) بدلًا من توابع نسخ:

class PictureFileCallbacks
  def self.after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

في حال تعريف توابع رد النداء هكذا، ليس من الضروري تهيئة كائن جديد من الصنف PictureFileCallbacks.

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks
end

يمكنك تعريف العدد الذي تريده من توابع رد النداء ضمن أصنافك.

عمليات (Transactions) توابع رد النداء

هناك تابعا رد نداء إضافيين يتم تشغيلهما عند إنهاء عملية قاعدة بيانات هما: after_commit و after_rollback. تُعد توابع رد النداء هذه مماثلة جدًا للتابع after_save، إلا أنها لا تشغّل إلّا بعد تسليم تغييرات قاعدة البيانات أو التراجع عنها. من الضروري جدًا استخدام هذه التوابع في حال كانت نماذج Active Record لديك تتفاعل مع نظم خارجية غير مرتبطة بعملية قاعدة البيانات.

فلنفرض مثلًا بناءً على المثال السابق أن النموذج PictureFile بحاجة إلى حذف ملف بعد تدمير السجل الموافق. في حال رفع استثناء بعد استدعاء تابع رد النداء after_destroy واسترجاع العملية، سيكون الملف محذوفًا وسيبقى السجل في حالة غير متناسقة. مثلًا، لنفترض أن الكائن picture_file_2 في التعليمات التالية غير صحيح بحيث أن التابع !save يرفع خطأ.

PictureFile.transaction do
  picture_file_1.destroy
  picture_file_2.save!
end

باستخدام تابع رد النداء after_commit، يمكن معالجة هذه الحالة.

class PictureFile < ApplicationRecord
  after_commit :delete_picture_file_from_disk, on: :destroy
 
  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

ملاحظة: الخيار on: يحدِّد متى يجب تشغيل تابع رد النداء. في حال لم تقم بتزويد هذا الخيار، سيتم تشغيل التابع في كل الأحداث.

بما أن after_commit معروف أنه سيشغّل عند الإنشاء والتحديث والحذف، يمكن تعريف توابع رد نداء منفصلة من أجل كل عملية:

  • after_create_commit
  • after_update_commit
  • After_destroy_commit
class PictureFile < ApplicationRecord
  after_destroy_commit :delete_picture_file_from_disk
 
  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

تحذير: التوابع after_commit و after_rollback يتم تشغيلها من أجل كل السجلات التي تُنشأ، تحدّث أو تُدمّر ضمن هيكل عملية. لكن في حال إطلاق استثناء ضمن أحد هذه التوابع، سينتشر هذا الاستثناء للأعلى وكل التوابع after_commit و after_rollback المتبقية لن يتم تشغيلها. لذلك، في حال كانت التعليمات المكتوبة في توابع رد النداء عرضةً لإطلاق استثناء، فيجب أن تعالجها ضمن هذه التوابع من أجل تشغيل توابع رد النداء الأخرى. تحذير: استخدام التابعان after_create_commit و after_update_commit كلاهما في نفس النموذج يسمح لآخر تابع فقط أن يتم تشغيله وسيتم تجاوز كل التوابع الأخرى.

class User < ApplicationRecord
  after_create_commit :log_user_saved_to_db
  after_update_commit :log_user_saved_to_db
 
  private
  def log_user_saved_to_db
    puts 'User was saved to database'
  end
end
 
# prints nothing
>> @user = User.create
 
# updating @user
>> @user.save
=> User was saved to database

لتسجيل توابع رد النداء من أجل كلا عمليات الإنشاء والتحديث، استخدم التابع after_commit عوضًا عن ذلك.

class User < ApplicationRecord
  after_commit :log_user_saved_to_db, on: [:create, :update]
end

مصادر