ردود نداء Active Record في ريلز
ستتعلم في هذا الدليل كل ما يتعلَّق بدورة حياة كائنات 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