الفرق بين المراجعتين لصفحة: «Rails/active record querying»
جميل-بيلوني (نقاش | مساهمات) مراجعة وتدقيق. |
جميل-بيلوني (نقاش | مساهمات) طلا ملخص تعديل |
||
(3 مراجعات متوسطة بواسطة نفس المستخدم غير معروضة) | |||
سطر 1: | سطر 1: | ||
<noinclude>{{DISPLAYTITLE:واجهة استعلامات | <noinclude>{{DISPLAYTITLE:واجهة استعلامات Active Record في ريلز}}</noinclude> | ||
[[تصنيف:Rails]] | [[تصنيف:Rails]] | ||
يغطي هذا الدليل مختلف الطرائق المستعملة لجلب واستعادة البيانات من قاعدة البيانات باستعمال | [[تصنيف:Rails Models]] | ||
يغطي هذا الدليل مختلف الطرائق المستعملة لجلب واستعادة البيانات من قاعدة البيانات باستعمال [[Rails/active record|Active Record]]. بعد قراءة هذا الدليل، ستتعلم: | |||
* كيفية البحث عن السجلات باستعمال توابع وشروط متعددة. | * كيفية البحث عن السجلات باستعمال توابع وشروط متعددة. | ||
* كيفية تحديد الترتيب، والتجميع، والخاصيات المعادة، والخاصيات الأخرى للسجلات التي عُثِر عليها. | * كيفية تحديد الترتيب، والتجميع، والخاصيات المعادة، والخاصيات الأخرى للسجلات التي عُثِر عليها. | ||
* كيفية استعمال التحميل الحثيث (eager loading) للتقليل من عدد استعلامات قاعدة البيانات الضرورية لجلب البيانات. | * كيفية استعمال التحميل الحثيث (eager loading) للتقليل من عدد استعلامات قاعدة البيانات الضرورية لجلب البيانات. | ||
* كيفية استعمال توابع البحث الديناميكية. | * كيفية استعمال توابع البحث الديناميكية. | ||
* كيفية استعمال تسلسل التوابع (method chaining) لاستعمال توابع متعددة | * كيفية استعمال تسلسل التوابع (method chaining) لاستعمال توابع متعددة [[Rails/active record|Active Record]] سويةً. | ||
* كيفية التحقق من تواجد سجلات محددة. | * كيفية التحقق من تواجد سجلات محددة. | ||
* كيفية تنفيذ حسابات مختلفة على نماذج | * كيفية تنفيذ حسابات مختلفة على نماذج [[Rails/active record|Active Record]]. | ||
* كيفية تشغيل EXPLAIN على العلاقات. | * كيفية تشغيل EXPLAIN على العلاقات. | ||
في حال كنت معتادًا على تنفيذ استعلامات [[SQL]] لإيجاد السجلات في قاعدة البيانات، فستجد أنَّ هناك طرقًا أفضل لتنفيذ هذه الاستعلامات في ريلز. يغنيك | في حال كنت معتادًا على تنفيذ استعلامات [[SQL]] لإيجاد السجلات في قاعدة البيانات، فستجد أنَّ هناك طرقًا أفضل لتنفيذ هذه الاستعلامات في ريلز. يغنيك [[Rails/active record|Active Record]] عن الحاجة لتنفيذ تعليمات [[SQL]] في معظم الحالات. | ||
إن الأمثلة الموجودة في هذا التوثيق ستستخدم النماذج التالية: | إن الأمثلة الموجودة في هذا التوثيق ستستخدم النماذج التالية: | ||
سطر 32: | سطر 33: | ||
has_and_belongs_to_many :clients | has_and_belongs_to_many :clients | ||
end | end | ||
</syntaxhighlight>سينفذ | </syntaxhighlight>سينفذ [[Rails/active record|Active Record]] استعلامات [[SQL]] من أجلك، وجميع هذه الاستعلامات مطابقة مع معظم أنظمة قواعد البيانات، بما فيها MySQL، و MariaDB، و PostgreSQL، و SQLite. بغض النظر عن نمط قاعدة البيانات المستخدمة، تكون توابع [[Rails/active record|Active Record]] المستخدمة في الاستعلامات ذاتها لجميع الأنظمة السابقة الذكر. | ||
== جلب الكائنات من قاعدة البيانات == | == جلب الكائنات من قاعدة البيانات == | ||
لقراءة الكائنات من قاعدة البيانات، يزوّدك | لقراءة الكائنات من قاعدة البيانات، يزوّدك [[Rails/active record|Active Record]] بمجموعة من توابع البحث. يسمح كل تابع بحث بتمرير وسائط له لتنفيذ استعلامات محددة على قاعدة البيانات دون كتابة استعلامات [[SQL]] خام. | ||
هذه التوابع هي: | هذه التوابع هي: | ||
سطر 71: | سطر 72: | ||
=== جلب كائن وحيد === | === جلب كائن وحيد === | ||
يزوّد | يزوّد [[Rails/active record|Active Record]] مجموعة من الطرق المختلفة لجلب كائن وحيد. | ||
==== التابع <code>find</code> ==== | ==== التابع <code>find</code> ==== | ||
سطر 196: | سطر 197: | ||
</syntaxhighlight>لكن هذا الحل يصبح غير عملي عند ازدياد حجم الجدول، بما أن التابع <code>User.all.each</code> يخبر | </syntaxhighlight>لكن هذا الحل يصبح غير عملي عند ازدياد حجم الجدول، بما أن التابع <code>User.all.each</code> يخبر [[Rails/active record|Active Record]] بضرورة تحميل كل الجدول في مرور واحد، وبناء كائن لكل سجل، ومن ثم الحفاظ على كامل مصفوفة كائنات النموذج في الذاكرة. بالطبع، عندما نملك كمًا كبيرًا من السجلات، قد يزيد حجم المجموعة عن الحجم المسموح حجزه في الذاكرة أساسًا. | ||
يزوّد ريلز بتابعين لحل هذه المشكلة وتقسيم السجلات على دفعات مناسبة للذاكرة من أجل معالجتها. التابع الأول هو <code>find_each</code> الذي يسترد دفعة من السجلات ومن ثم يدفع كل سجل إلى الكتلة المعطاة بشكل إفرادي ككائن من النموذج. أمَّا التابع الثاني فهو <code>find_in_batches</code> الذي يسترد دفعة من السجلات ومن ثم يدفع كامل الدفعة إلى الكتلة المعطاة كمصفوفة من كائنات النموذج. | يزوّد ريلز بتابعين لحل هذه المشكلة وتقسيم السجلات على دفعات مناسبة للذاكرة من أجل معالجتها. التابع الأول هو <code>find_each</code> الذي يسترد دفعة من السجلات ومن ثم يدفع كل سجل إلى الكتلة المعطاة بشكل إفرادي ككائن من النموذج. أمَّا التابع الثاني فهو <code>find_in_batches</code> الذي يسترد دفعة من السجلات ومن ثم يدفع كامل الدفعة إلى الكتلة المعطاة كمصفوفة من كائنات النموذج. | ||
سطر 277: | سطر 278: | ||
ماذا لو احتجت إلى تغيير الرقم السابق مثلًا ليكون وسيطًا من مكان آخر؟ سيأخذ تابع البحث الشكل التالي:<syntaxhighlight lang="rails"> | ماذا لو احتجت إلى تغيير الرقم السابق مثلًا ليكون وسيطًا من مكان آخر؟ سيأخذ تابع البحث الشكل التالي:<syntaxhighlight lang="rails"> | ||
Client.where("orders_count = ?", params[:orders]) | Client.where("orders_count = ?", params[:orders]) | ||
</syntaxhighlight>يأخذ | </syntaxhighlight>يأخذ [[Rails/active record|Active Record]] السلسلة النصية من الشروط المطلوبة وسيطًا أولًا له، وأيّة وسطاء إضافية ستحل مكان المحرف <code>?</code> في السلسلة المعطاة. | ||
في حال أردت تحديد مجموعة من الشروط:<syntaxhighlight lang="rails"> | في حال أردت تحديد مجموعة من الشروط:<syntaxhighlight lang="rails"> | ||
سطر 300: | سطر 301: | ||
=== شروط الكائن <code>[[Ruby/Hash|Hash]]</code> === | === شروط الكائن <code>[[Ruby/Hash|Hash]]</code> === | ||
يمكّنك | يمكّنك [[Rails/active record|Active Record]] من تمرير شروط على شكل كائن <code>[[Ruby/Hash|Hash]]</code> والتي تزيد من قابلية قراءة نمط كتابة الشروط. باستخدام شروط <code>[[Ruby/Hash|Hash]]</code>، يمكنك تمرير <code>[[Ruby/Hash|Hash]]</code> بالمفاتيح التي تمثل الحقول التي تريد التحقق منها، والقيم التي تحدد القيم المطلوبة لهذه الحقول: | ||
'''ملاحظة''': يمكن فقط استخدام تحقق المساواة، والمجال، والمجموعة الجزئية في شروط <code>[[Ruby/Hash|Hash]]</code>. | '''ملاحظة''': يمكن فقط استخدام تحقق المساواة، والمجال، والمجموعة الجزئية في شروط <code>[[Ruby/Hash|Hash]]</code>. | ||
سطر 311: | سطر 312: | ||
</syntaxhighlight>يمكن أن يكون اسم الحقل أيضًا سلسلة نصية:<syntaxhighlight lang="rails"> | </syntaxhighlight>يمكن أن يكون اسم الحقل أيضًا سلسلة نصية:<syntaxhighlight lang="rails"> | ||
Client.where('locked' => true) | Client.where('locked' => true) | ||
</syntaxhighlight>في حالة ارتباط الانتماء (belongs_to)، يمكن استخدام مفتاح الارتباط لتحديد فيما إذا كان من المراد استخدام كائن | </syntaxhighlight>في حالة ارتباط الانتماء (belongs_to)، يمكن استخدام مفتاح الارتباط لتحديد فيما إذا كان من المراد استخدام كائن [[Rails/active record|Active Record]] كالقيمة الممررة. يعمل هذا التابع مع العلاقات متعددة الأشكال أيضًا.<syntaxhighlight lang="rails"> | ||
Article.where(author: author) | Article.where(author: author) | ||
Author.joins(:articles).where(articles: { author: author }) | Author.joins(:articles).where(articles: { author: author }) | ||
سطر 465: | سطر 466: | ||
=== التابع <code>only</code> === | === التابع <code>only</code> === | ||
يمكنك أيضًا تجاوز الشروط عن طريق التابع < | يمكنك أيضًا تجاوز الشروط عن طريق التابع <code>only</code>. مثلًا:<syntaxhighlight lang="rails"> | ||
Article.where('id > 10').limit(20).order('id desc').only(:order, :where) | Article.where('id > 10').limit(20).order('id desc').only(:order, :where) | ||
</syntaxhighlight>سيبدو استعلام [[SQL]] المنفذ كالتالي:<syntaxhighlight lang="sql"> | </syntaxhighlight>سيبدو استعلام [[SQL]] المنفذ كالتالي:<syntaxhighlight lang="sql"> | ||
سطر 533: | سطر 534: | ||
== كائنات القراءة فقط == | == كائنات القراءة فقط == | ||
يزوّد | يزوّد Active Record بالتابع <code>readonly</code> في العلاقات لتحديد عدم إمكانية تعديل أي من الكائنات المعادة بشكل ظاهري. لن تنجح أي عملية لتعديل سجلات القراءة فقط، الأمر الذي يرمي استنثناءً من النوع <code>ActiveRecord::ReadOnlyRecord</code>.<syntaxhighlight lang="rails"> | ||
client = Client.readonly.first | client = Client.readonly.first | ||
client.visits += 1 | client.visits += 1 | ||
سطر 542: | سطر 543: | ||
إن عملية القفل هي عملية مفيدة لتجنب حالات التسابق (أو حالات التعارض - Race Conditions) عند تحديث السجلات في قاعدة البيانات. | إن عملية القفل هي عملية مفيدة لتجنب حالات التسابق (أو حالات التعارض - Race Conditions) عند تحديث السجلات في قاعدة البيانات. | ||
يزوّد | يزوّد [[Rails/active record|Active Record]] بطريقتان للقفل: | ||
* القفل المستبشر (Optimistic Locking). | * القفل المستبشر (Optimistic Locking). | ||
* القفل المستطير (Pessimistic Locking). | * القفل المستطير (Pessimistic Locking). | ||
سطر 550: | سطر 551: | ||
==== حقل القفل المستبشر ==== | ==== حقل القفل المستبشر ==== | ||
لاستخدام القفل المستبشر، يجب على الجدول أن يحوي حقلًا يسمى <code>lock_version</code> من نوع القيمة الصحيحة (integer). عند تحديث سجل، | لاستخدام القفل المستبشر، يجب على الجدول أن يحوي حقلًا يسمى <code>lock_version</code> من نوع القيمة الصحيحة (integer). عند تحديث سجل، يزيد [[Rails/active record|Active Record]] قيمة الحقل <code>lock_version</code>. في حال طلب تحديث بقيمة <code>lock_version</code> أصغر من القيمة الموجودة حاليًا في الحقل <code>lock_version</code> في قاعدة البيانات، سيفشل التعديل وسيرمى الاستثناء <code>ActiveRecord::StaleObjectError</code>. مثلًا:<syntaxhighlight lang="rails"> | ||
c1 = Client.find(1) | c1 = Client.find(1) | ||
c2 = Client.find(1) | c2 = Client.find(1) | ||
سطر 600: | سطر 601: | ||
== دمج الجداول == | == دمج الجداول == | ||
يزوّد | يزوّد [[Rails/active record|Active Record]] بتابعي بحث لتحديد تعليمات الدمج (التعليمة <code>[[SQL/join|JOIN]]</code>) على استعلام [[SQL]] المنفذ: التابع <code>joins</code> والتابع <code>left_outer_joins</code>. في حين أن التابع <code>joins</code> يجب استخدامه من أجل الدمج الداخلي ([[SQL/inner join|<code>INNER JOIN</code>]]) أو الاستعلامات المخصصة، يستخدم التابع <code>left_outer_joins</code> للاستعلامات التي تستخدم الدمج الخارجي (<code>[[SQL/left join|LEFT OUTER JOIN]]</code>). | ||
=== التابع <code>joins</code> === | === التابع <code>joins</code> === | ||
سطر 613: | سطر 614: | ||
==== باستخدام مصفوفة/كائن Hash من الارتباطات المسماة ==== | ==== باستخدام مصفوفة/كائن Hash من الارتباطات المسماة ==== | ||
يمكّنك | يمكّنك [[Rails/active record|Active Record]] من استخدام أسماء الارتباطات المعرّفة على النموذج كطريق قصير لتحديد بنية تعليمة الدمج لهذه الارتباطات باستخدام التابع <code>joins</code>. | ||
مثلًا، لنفرض النماذج التالية:<syntaxhighlight lang="rails"> | مثلًا، لنفرض النماذج التالية:<syntaxhighlight lang="rails"> | ||
سطر 711: | سطر 712: | ||
=== الحل لمشكلة الـ N + 1 استعلام === | === الحل لمشكلة الـ N + 1 استعلام === | ||
يمكّنك | يمكّنك [[Rails/active record|Active Record]] من تحديد الارتباطات المراد تحميلها بشكل مسبق. يمكن هذا عن طريق تحديد التابع <code>includes</code> لاستدعاء <code>Model.find</code>. باستخدام <code>includes</code>، سيتأكد [[Rails/active record|Active Record]] من أن جميع الارتباطات قد تم تحميلها باستخدام أقل عدد من الاستعلامات. | ||
بالعودة للتعليمات السابقة، يمكننا كتابة التالي لتحميل العناوين بشكل حثيث:<syntaxhighlight lang="rails"> | بالعودة للتعليمات السابقة، يمكننا كتابة التالي لتحميل العناوين بشكل حثيث:<syntaxhighlight lang="rails"> | ||
سطر 727: | سطر 728: | ||
=== التحميل الحثيث لمجموعة من الارتباطات === | === التحميل الحثيث لمجموعة من الارتباطات === | ||
يمكّنك | يمكّنك [[Rails/active record|Active Record]] من تحميل مجموعة من الارتباطات بشكل حثيث ضمن استدعاء <code>Model.find</code> وحيد، عن طريق استخدام مصفوفة، كائن <code>[[Ruby/Hash|Hash]]</code>، أو كائن <code>[[Ruby/Hash|Hash]]</code> متداخل مع مصفوفات باستخدام التابع <code>includes</code>. | ||
==== مصفوفة ارتباطات ==== | ==== مصفوفة ارتباطات ==== | ||
سطر 740: | سطر 741: | ||
=== فرض شروط على التحميل الحثيث === | === فرض شروط على التحميل الحثيث === | ||
في حين أن | في حين أن [[Rails/active record|Active Record]] يمكّنك من فرض شروط على الارتباطات المحملة بشكل حثيث كما هو الحال في التابع <code>joins</code>، إلّا أنّه من المفضّل استخدام التابع <code>joins</code> بدلًا من ذلك. | ||
لكن إذا أردت استخدام هذه الطريقة، يمكنك استخدام التابع <code>where</code> كما هو الحال طبيعيًا.<syntaxhighlight lang="rails"> | لكن إذا أردت استخدام هذه الطريقة، يمكنك استخدام التابع <code>where</code> كما هو الحال طبيعيًا.<syntaxhighlight lang="rails"> | ||
سطر 888: | سطر 889: | ||
== توابع البحث الديناميكية == | == توابع البحث الديناميكية == | ||
من أجل كل حقل (أو خاصية) تعرّفه في جدولك، يزوّدك | من أجل كل حقل (أو خاصية) تعرّفه في جدولك، يزوّدك [[Rails/active record|Active Record]] بتابع بحث. إذا ملكت حقلًا مسمى <code>first_name</code> على النموذج <code>Client</code> مثلًا، ستحصل على التابع <code>find_by_first_name</code> بالمجان من [[Rails/active record|Active Record]]. وإذا ملكت حقلًا مسمى <code>locked</code> على النموذج <code>Client</code>، ستحصل أيضًا على التابع <code>find_by_locked</code>. | ||
يمكنك أيضًا تحديد إشارة التعجب في نهاية توابع البحث الديناميكية لتجعلها ترمي الاستثناء <code>ActiveRecord::RecordNotFound</code> في حال لم تعد أيّة سجلات، مثلًا:<syntaxhighlight lang="rails"> | يمكنك أيضًا تحديد إشارة التعجب في نهاية توابع البحث الديناميكية لتجعلها ترمي الاستثناء <code>ActiveRecord::RecordNotFound</code> في حال لم تعد أيّة سجلات، مثلًا:<syntaxhighlight lang="rails"> | ||
سطر 914: | سطر 915: | ||
== فهم سَلسَلة التوابع == | == فهم سَلسَلة التوابع == | ||
يطبّق نمط | يطبّق نمط Active Record مفهوم سَلْسَلة (Chaining) التوابع، الذي يمكننا من استخدام مجموعة من توابع [[Rails/active record|Active Record]] معًا في طريقة سَلِسَلةٍ وبسيطةٍ. | ||
يمكنك سَلسَلة التوابع في تعليمة واحدة عندما يعيد التابع السابق علاقة (كائنًا من نوع <code>ActiveRecord::Relation</code>)، مثل التوابع <code>all</code> و <code>where</code> و <code>joins</code>. التوابع التي تعيد كائنًا وحيدًا يجب أن تكون في نهاية التعليمة. | يمكنك سَلسَلة التوابع في تعليمة واحدة عندما يعيد التابع السابق علاقة (كائنًا من نوع <code>ActiveRecord::Relation</code>)، مثل التوابع <code>all</code> و <code>where</code> و <code>joins</code>. التوابع التي تعيد كائنًا وحيدًا يجب أن تكون في نهاية التعليمة. | ||
هناك بعض الأمثلة أدناه، ولكن لا يغطي هذا التوثيق جميع الحالات المحتملة بل يتطرق إلى كم صغير منها فقط. عند استدعاء تابع في | هناك بعض الأمثلة أدناه، ولكن لا يغطي هذا التوثيق جميع الحالات المحتملة بل يتطرق إلى كم صغير منها فقط. عند استدعاء تابع في [[Rails/active record|Active Record]]، لن يتم تنفيذ الاستعلام وإرساله إلى قاعدة البيانات، إذ سيحصل ذلك فقط عند الحاجة للبيانات. لذلك كل مثال من الأمثلة التالية سيولّد فقط استعلامًا واحدًا. | ||
=== جلب بيانات مرشَّحة من مجموعة جداول === | === جلب بيانات مرشَّحة من مجموعة جداول === | ||
سطر 973: | سطر 974: | ||
</syntaxhighlight>والثانية باستخدام كتلة معطاة:<syntaxhighlight lang="rails"> | </syntaxhighlight>والثانية باستخدام كتلة معطاة:<syntaxhighlight lang="rails"> | ||
Client.find_or_create_by(first_name: ' | Client.find_or_create_by(first_name: 'Sara') do |c| | ||
c.locked = false | c.locked = false | ||
end | end | ||
سطر 1٬047: | سطر 1٬048: | ||
# أو | # أو | ||
Client.pluck(:id, :name) | Client.pluck(:id, :name) | ||
</syntaxhighlight>على نقيض التابع <code>select</code>، سيحول التابع <code>pluck</code> نتيجة قاعدة البيانات مباشرةً إلى مصفوفة، دون الحاجة لتهيئة كائنات | </syntaxhighlight>على نقيض التابع <code>select</code>، سيحول التابع <code>pluck</code> نتيجة قاعدة البيانات مباشرةً إلى مصفوفة، دون الحاجة لتهيئة كائنات [[Rails/active record|Active Record]]. يؤدي ذلك إلى تحسين الأداء من أجل استعلام ضخم ومنفذ بشكل متردد. لكن، لن تكون التوابع المتجاوزة في النموذج متاحةً. مثلًا:<syntaxhighlight lang="rails"> | ||
class Client < ApplicationRecord | class Client < ApplicationRecord | ||
def name | def name | ||
سطر 1٬157: | سطر 1٬158: | ||
يمكنك تنفيذ تعليمة <code>EXPLAIN</code> على الاستعلامات المولّدة من العلاقات، مثلًا:<syntaxhighlight lang="rails"> | يمكنك تنفيذ تعليمة <code>EXPLAIN</code> على الاستعلامات المولّدة من العلاقات، مثلًا:<syntaxhighlight lang="rails"> | ||
User.where(id: 1).joins(:articles).explain | User.where(id: 1).joins(:articles).explain | ||
</syntaxhighlight>قد يولّد:<syntaxhighlight lang=" | </syntaxhighlight>قد يولّد:<syntaxhighlight lang="text"> | ||
EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 | EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 | ||
+----+-------------+----------+-------+---------------+ | +----+-------------+----------+-------+---------------+ | ||
سطر 1٬175: | سطر 1٬176: | ||
</syntaxhighlight>عند استخدام قواعد البيانات MySQL و MariaDB. | </syntaxhighlight>عند استخدام قواعد البيانات MySQL و MariaDB. | ||
ينفّذ | ينفّذ [[Rails/active record|Active Record]] طباعة جميلة لمحاكاة طباعة سطر أوامر قاعدة البيانات. لذا من أجل نفس الاستعلام وعند استخدام قاعدة بيانات PostgreSQL، ستولّد تعليمة <code>EXPLAIN</code> التالي:<syntaxhighlight lang="text"> | ||
EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 | EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 | ||
QUERY PLAN | QUERY PLAN |
المراجعة الحالية بتاريخ 09:50، 24 مارس 2019
يغطي هذا الدليل مختلف الطرائق المستعملة لجلب واستعادة البيانات من قاعدة البيانات باستعمال Active Record. بعد قراءة هذا الدليل، ستتعلم:
- كيفية البحث عن السجلات باستعمال توابع وشروط متعددة.
- كيفية تحديد الترتيب، والتجميع، والخاصيات المعادة، والخاصيات الأخرى للسجلات التي عُثِر عليها.
- كيفية استعمال التحميل الحثيث (eager loading) للتقليل من عدد استعلامات قاعدة البيانات الضرورية لجلب البيانات.
- كيفية استعمال توابع البحث الديناميكية.
- كيفية استعمال تسلسل التوابع (method chaining) لاستعمال توابع متعددة Active Record سويةً.
- كيفية التحقق من تواجد سجلات محددة.
- كيفية تنفيذ حسابات مختلفة على نماذج Active Record.
- كيفية تشغيل EXPLAIN على العلاقات.
في حال كنت معتادًا على تنفيذ استعلامات SQL لإيجاد السجلات في قاعدة البيانات، فستجد أنَّ هناك طرقًا أفضل لتنفيذ هذه الاستعلامات في ريلز. يغنيك Active Record عن الحاجة لتنفيذ تعليمات SQL في معظم الحالات.
إن الأمثلة الموجودة في هذا التوثيق ستستخدم النماذج التالية:
ملاحظة: جميع النماذج التالية تستخدم الحقل id مفتاحًا رئيسيًا لها، إلّا عند تحديد عكس ذلك.
class Client < ApplicationRecord
has_one :address
has_many :orders
has_and_belongs_to_many :roles
end
class Address < ApplicationRecord
belongs_to :client
end
class Order < ApplicationRecord
belongs_to :client, counter_cache: true
end
class Role < ApplicationRecord
has_and_belongs_to_many :clients
end
سينفذ Active Record استعلامات SQL من أجلك، وجميع هذه الاستعلامات مطابقة مع معظم أنظمة قواعد البيانات، بما فيها MySQL، و MariaDB، و PostgreSQL، و SQLite. بغض النظر عن نمط قاعدة البيانات المستخدمة، تكون توابع Active Record المستخدمة في الاستعلامات ذاتها لجميع الأنظمة السابقة الذكر.
جلب الكائنات من قاعدة البيانات
لقراءة الكائنات من قاعدة البيانات، يزوّدك Active Record بمجموعة من توابع البحث. يسمح كل تابع بحث بتمرير وسائط له لتنفيذ استعلامات محددة على قاعدة البيانات دون كتابة استعلامات SQL خام.
هذه التوابع هي:
find
create_with
distinct
eager_load
extending
from
group
having
includes
joins
left_outer_joins
limit
lock
none
offset
order
preload
readonly
references
reorder
reverse_order
select
where
إن توابع البحث التي تعيد مجموعات، مثل التابع where
والتابع group
، تعيد نسخةً من ActiveRecord::Relation
. والتوابع التي تبحث عن كائن وحيد، مثل التابع find
والتابع first
، تعيد نسخةً وحيدةً من نوع النموذج المطلوب.
يمكن تلخيص العمليات الأساسية للتابع (Model.find(options
كالتالي:
- تحويل الخيارات المعطاة إلى استعلام SQL خام.
- تنفيذ الاستعلام وقراءة النتائج الموافقة من قاعدة البيانات.
- تهيئة كائنات روبي الموافقة للنموذج المطلوب تنفيذ الاستعلام عليه، وذلك من أجل كل حقل من الحقول الناتجة.
- تشغيل توابع رد النداء مثل
after_find
ثمafter_initialize
، في حال وجودهما.
جلب كائن وحيد
يزوّد Active Record مجموعة من الطرق المختلفة لجلب كائن وحيد.
التابع find
باستخدام التابع find
، يمكنك جلب الكائن المقابل لقيمة المفتاح الرئيسي الممرر له. مثلًا:
# 10 (id) ابحث عن الزبون بالمفتاح الرئيسي
client = Client.find(10)
# => #<Client id: 10, first_name: "Ryan">
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
يرمي التابع find
استثناءً من النوع ActiveRecord::RecordNotFound
في حال لم يتم العثور على أي سجل مطابق.
يمكنك أيضًا استخدام هذا التابع للاستعلام عن مجموعة من الكائنات، عن طريق تمرير مصفوفة من قيم المفتاح الرئيسي للتابع. تكون القيمة المعادة مصفوفة تحتوي على جميع السجلات المطابقة لقيم المفتاح الرئيسي الممررة. مثلًا:
# ابحث عن الزبائن بقيم المفتاح الرئيسي 1 و 10.
clients = Client.find([1, 10]) # Client.find(1, 10) أو يمكن استعمال
# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients WHERE (clients.id IN (1,10))
تحذير: يرمي التابع find
استثناءً من النوع ActiveRecord::RecordNotFound
إلّا إذا تم العثور على سجل واحد على الأقل من أجل جميع قيم المفتاح الرئيسي الممررة.
التابع take
يعيد التابع take
سجلًا بدون أي ترتيب واضح. مثلًا:
client = Client.take
# => #<Client id: 1, first_name: "Lifo">
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients LIMIT 1
يعيد التابع take
القيمة nil
في حال لم يتم العثور على أي سجل، ولن يتم رمي استثناء.
يمكنك تمرير وسيط رقمي للتابع take
لإعادة مجموعة السجلات التي عددها يوافق الوسيط الممرر. مثلًا:
clients = Client.take(2)
# => [
# #<Client id: 1, first_name: "Lifo">,
# #<Client id: 220, first_name: "Sara">
# ]
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients LIMIT 2
إن التابع !take
يعمل تمامًا مثل التابع take
، إلا أنّه يرمي استثناء من النوع ActiveRecord::RecordNotFound
في حال عدم العثور على أي سجل.
ملاحظة: قد يختلف السجل المعاد بناءً على محرك قاعدة البيانات المستخدم.
التابع first
يبحث التابع first
عن أول سجل، مرتّب حسب ترتيب المفتاح الرئيسي (بشكل افتراضي). مثلًا:
client = Client.first
# => #<Client id: 1, first_name: "Lifo">
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
يعيد التابع first
القيمة nil
في حال لم يتم العثور على أي سجل، ولن يتم رمي استثناء.
في حال احتوى النطاق الافتراضي على تابع ترتيب، سيعيد التابع first
السجل الأول بناءً على الترتيب المحدد في النطاق.
يمكنك تمرير وسيط عددي للتابع first
لإعادة مجموعة السجلات التي عددها يوافق الوسيط الممرر. مثلًا:
clients = Client.first(3)
# => [
# #<Client id: 1, first_name: "Lifo">,
# #<Client id: 2, first_name: "Fifo">,
# #<Client id: 3, first_name: "Filo">
# ]
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
عند استخدام التابع first
على مجموعة مرتبة باستخدام التابع order
، سيعيد التابع first
أول سجل بناءً على الترتيب المحدد في التابع order
.
client = Client.order(:first_name).first
# => #<Client id: 2, first_name: "Fifo">
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1
إن التابع !first
يعمل تمامًا مثل التابع first
إلا أنّه يرمي استثناء من النوع ActiveRecord::RecordNotFound
في حال عدم العثور على أي سجل.
التابع last
يبحث التابع last
عن آخر سجل، مرتّب حسب المفتاح الرئيسي (بشكل افتراضي). مثلًا:
client = Client.last
# => #<Client id: 221, first_name: "Russel">
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
يعيد التابع last
القيمة nil
في حال لم يتم العثور على أي سجل، ولن يتم رمي استثناء.
في حال احتوى النطاق الافتراضي على تابع ترتيب، سيعيد التابع last
السجل الأول بناءً على الترتيب المحدد في النطاق.
يمكنك تمرير وسيط رقمي للتابع last
لإعادة مجموعة السجلات التي عددها يوافق الوسيط الممرر. مثلًا:
clients = Client.last(3)
# => [
# #<Client id: 219, first_name: "James">,
# #<Client id: 220, first_name: "Sara">,
# #<Client id: 221, first_name: "Russel">
# ]
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
عند استخدام التابع last
على مجموعة مرتبة باستخدام التابع order
، سيعيد التابع last
آخر سجل بناءً على الترتيب المحدد في التابع order
.
client = Client.order(:first_name).last
# => #<Client id: 220, first_name: "Sara">
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1
إن التابع !last
يعمل تمامًا مثل التابع last
إلا أنّه يرمي استثناء من النوع ActiveRecord::RecordNotFound
في حال عدم العثور على أي سجل.
التابع find_by
يبحث التابع find_by
عن أول سجل يطابق الشروط المعطاة. مثلًا:
Client.find_by first_name: 'Lifo'
# => #<Client id: 1, first_name: "Lifo">
Client.find_by first_name: 'Jon'
# => nil
يماثل كتابة:
Client.where(first_name: 'Lifo').take
استعلام SQL الموافق للتعليمة السابقة هو:
SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1
إن التابع !find_by
يعمل تمامًا مثل التابع find_by
إلا أنّه يرمي استثناء من النوع ActiveRecord::RecordNotFound
في حال عدم العثور على أي سجل. مثلًا:
Client.find_by! first_name: 'does not exist'
# => ActiveRecord::RecordNotFound
هذا يماثل كتابة:
Client.where(first_name: 'does not exist').take!
جلب مجموعة من الكائنات على دفعات
من المعتاد أن نحتاج إلى المرور على مجموعة كبيرة من السجلات، كما هو الحال عندما نريد أن نرسل خبرًا ما إلى قائمة بريدية من المستخدمين، أو عند تصدير البيانات.
قد تبدو هذه القضية سهلة:
# سيأخذ هذا حيّزًا كبيرًا من الذاكرة في حال كان الجدول كبيرًا جدًا
User.all.each do |user|
NewsMailer.weekly(user).deliver_now
end
لكن هذا الحل يصبح غير عملي عند ازدياد حجم الجدول، بما أن التابع User.all.each
يخبر Active Record بضرورة تحميل كل الجدول في مرور واحد، وبناء كائن لكل سجل، ومن ثم الحفاظ على كامل مصفوفة كائنات النموذج في الذاكرة. بالطبع، عندما نملك كمًا كبيرًا من السجلات، قد يزيد حجم المجموعة عن الحجم المسموح حجزه في الذاكرة أساسًا.
يزوّد ريلز بتابعين لحل هذه المشكلة وتقسيم السجلات على دفعات مناسبة للذاكرة من أجل معالجتها. التابع الأول هو find_each
الذي يسترد دفعة من السجلات ومن ثم يدفع كل سجل إلى الكتلة المعطاة بشكل إفرادي ككائن من النموذج. أمَّا التابع الثاني فهو find_in_batches
الذي يسترد دفعة من السجلات ومن ثم يدفع كامل الدفعة إلى الكتلة المعطاة كمصفوفة من كائنات النموذج.
ملاحظة: يراد استعمال التوابع find_each
و find_in_batches
لمعالجة عدد كبير من السجلات التي لا تتسع في الذاكرة بآنٍ معًا. إذا احتجت فقط للمرور على ألف سجل مثلًا، يفضّل استخدام توابع البحث العادية.
التابع find_each
يجلب التابع find_each
دفعة من السجلات ومن ثم يدفع كل سجل من هذه السجلات إلى الكلتة المعطاة. في المثال التالي، يسترد التابع find_each
المستخدمين على دفعات من 1000 عنصر، ومن ثم يدفعها إلى الكتلة واحدة تلو الأخرى:
User.find_each do |user|
NewsMailer.weekly(user).deliver_now
end
تعاد هذه العملية، ويتم استرداد المزيد من الدفعات عند الحاجة، حتى تتم معالجة جميع السجلات.
يعمل التابع find_each
على أصناف النماذج، كما هو مبين أعلاه، إضافةً إلى العلاقات:
User.where(weekly_subscriber: true).find_each do |user|
NewsMailer.weekly(user).deliver_now
end
طالما أنَّ العلاقات لا تملك ترتيبًا معينًا، التابع find_each
سيحتاج إلى فرض ترتيب معين داخليًا للمرور على العناصر.
في حال وجود ترتيب معين في المستقبل، سيعتمد السلوك على الراية config.active_record.error_on_ignored_order
. في حال كانت قيمتها true
، سيتم رمي الاستثناء ArgumentError
، وإلّا سيتم تجاهل الترتيب وتزويد تحذير، وهو السلوك الافتراضي. يمكن تجاوز هذا السلوك عن طريق الخيار error_on_ignore:
، كما هو مشروح أدناه.
خيارات التابع find_each
batch_size:
يمكّنك هذا الخيار من تحديد عدد السجلات المراد جلبها في كل دفعة، قبل دفعها بشكل إفرادي إلى الكتلة. مثلًا، لقراءة السجلات على دفعات من 5000 عنصر:
User.find_each(batch_size: 5000) do |user|
NewsMailer.weekly(user).deliver_now
end
start:
بشكل افتراضي، يتم استرداد السجلات بترتيب تصاعدي بناءً على المفتاح الرئيسي، الذي يجب أن يكون قيمة صحيحة. يمكّنك الخيار start:
من تحديد أول معرّف من السلسلة عندما لا يكون أصغر معرّف هو المعرّف المطلوب. قد يكون هذا مفيدًا على سبيل المثال عندما تريد إعادة بدء معالجة دفعة محددة تمّت مقاطعتها، بفرّض أنّك تملك آخر معرّف تمّت معالجته كنقطة تحقق.
مثلًا، لإرسال رسائل بريدية لمستخدمي القائمة البريدية التي تبدأ معرّفاتهم بالقيمة 2000:
User.find_each(start: 2000) do |user|
NewsMailer.weekly(user).deliver_now
end
finish:
يشبه هذا الخيار الخيار start:
، إذ يمكنك من تهيئة آخر معرف من السلسلة عندما لا يكون أعلى معرّف هو المعرّف المطلوب. قد يكون هذا مفيدًا على سبيل المثال في حال أردت معالجة دفعة بيانات محددة بناءً على مجموعة جزئية من السجلات. يمكن تنفيذ هذ ا عبر الخيارين start:
و finish:
.
مثلًا، لإرسال رسائل بريدية لمستخدمي القائمة البريدية التي تكون معرّفاتهم بين 2000 و 10000:
User.find_each(start: 2000, finish: 10000) do |user|
NewsMailer.weekly(user).deliver_now
end
مثال آخر عن استخدام هذين الخيارين معًا هو في حال أردت من مجموعة من العمليات العاملة (workers) أن تعالج نفس رتل المعالجة. يمكنك تحديد 10000 سجل لكل عملية عاملة عن طريق تحديد الخيارين start:
و finish:
لكل عملية.
error_on_ignore:
يتجاوز تهيئة التطبيق الافتراضية ويحدّد فيما إذا وجب رمي استثناء عند وجود ترتيب معين على العلاقة.
التابع find_in_batches
إن التابع find_in_batches
مماثل جدًا للتابع find_each
، بما أن كليهما يجلبان دفعات من السجلات. يقع موقع الاختلاف في طريقة دفع الكائنات إلى الكتلة المعطاة، إذ يدفع التابع find_in_batches
دفعات من الكائنات إلى الكتلة كمصفوفة كائنات النموذج، بدلًا من الطريقة الإفرادية. يبيّن المثال التالي طريقة استخدام التابع عند دفع مصفوفة من 1000 فاتورة على حدة إلى الكتلة المعطاة، مع احتواء الكتلة الأخيرة على الفواتير المتبقية:
# بـ 1000 فاتورة بآن معًا add_invoices يزوّد التابع
Invoice.find_in_batches do |invoices|
export.add_invoices(invoices)
end
يعمل التابع find_in_batches
على أصناف النماذج، كما هو مبين أعلاه، إضافةً إلى العلاقات:
Invoice.pending.find_in_batches do |invoices|
pending_invoices_export.add_invoices(invoices)
end
طالما لا تحتاج العلاقات إلى ترتيب معين، سيحتاج التابع إلى فرض ترتيب معين داخليًا للمرور على العناصر.
خيارات التابع find_in_batches
يقبل التابع find_in_batches
نفس الخيارات التي يقبلها التابع find_each
.
الشروط
يمكّنك التابع where
من تحديد شروط معيّنة لحدّ عدد السجلات المعادة، والذي يمثل القسم WHERE
في استعلام SQL. يمكن تحديد الشروط إمّا كسلسلة نصية، أو مصفوفة، أو جدول hash.
شروط السلاسل النصية
في حال أردت إضافة شروط إلى بحثك، يمكنك تحديدها مباشرةً، مثل ("'Client.where("orders_count = '2
. سيبحث هذا عن جميع الزبائن الذين يملكون القيمة 2 للحقل orders_count
.
تحذير: إن بناء الشروط عن طريق السلاسل النصية قد يجعل تطبيقك عرضةً لهجمات حقن الاستعلامات (SQL Injection). مثلًا، الاستعلام التالي غير آمن:
Client.where("first_name LIKE '%#{params[:first_name]}%'")
أكمل قراءة القسم التالي من أجل التعرف على الطريقة المفضلة لمعالجة الشروط باستخدام المصفوفات.
الشروط المصفوفية
ماذا لو احتجت إلى تغيير الرقم السابق مثلًا ليكون وسيطًا من مكان آخر؟ سيأخذ تابع البحث الشكل التالي:
Client.where("orders_count = ?", params[:orders])
يأخذ Active Record السلسلة النصية من الشروط المطلوبة وسيطًا أولًا له، وأيّة وسطاء إضافية ستحل مكان المحرف ?
في السلسلة المعطاة.
في حال أردت تحديد مجموعة من الشروط:
Client.where("orders_count = ? AND locked = ?", params[:orders], false)
في هذا المثال، سيتم استبدال أول إشارة استفهام بالقيمة [params[:orders
، والثانية بالقيمة false
.
إن التعليمة التالية هي المفضلة بشدة:
Client.where("orders_count = ?", params[:orders])
عن استعمال التعليمة التالية:
Client.where("orders_count = #{params[:orders]}")
بسبب أمان الوسائط.
سيؤدي وضع المتحول مباشرةً في سلسلة الشروط إلى تمرير قيمة هذا المتحول إلى قاعدة البيانات كما هي. هذا يعني أنّه سيكون متحول غير معالج مباشر من المستخدم الذي قد يملك نوايا خبيثة. إذا فعلت ذلك، قد تجعل كامل قاعدة بياناتك بخطر لأنه في حال عرف أحد المستخدمين أنّه يمكنه اختراق قاعدة بياناتك، فهذا سيجعله قابلًا لفعل أي شيء بها. لا تقم أبدًا بوضع متحولاتك مباشرةً في سلسلة شروط نصية.
ملاحظة: للمزيد من المعلومات حول مخاطر حقن الاستعلامات، اقرأ دليل الأمان.
الشروط النائبة
بشكل مماثل لطريقة استبدال إشارة الاستفهام بالوسائط، يمكنك أيضًا تحديد مفاتيح في سلسلة الشروط النصية مع مفاتيح/قيم الكائن Hash
الممرر:
Client.where("created_at >= :start_date AND created_at <= :end_date",
{start_date: params[:start_date], end_date: params[:end_date]})
سيعزّز هذا سهولة قراءة التعليمات بوجود مجموعة كبيرة من الشروط المتغيرة.
شروط الكائن Hash
يمكّنك Active Record من تمرير شروط على شكل كائن Hash
والتي تزيد من قابلية قراءة نمط كتابة الشروط. باستخدام شروط Hash
، يمكنك تمرير Hash
بالمفاتيح التي تمثل الحقول التي تريد التحقق منها، والقيم التي تحدد القيم المطلوبة لهذه الحقول:
ملاحظة: يمكن فقط استخدام تحقق المساواة، والمجال، والمجموعة الجزئية في شروط Hash
.
شروط المساواة
Client.where(locked: true)
سيولّد هذا استعلام SQL التالي:
SELECT * FROM clients WHERE (clients.locked = 1)
يمكن أن يكون اسم الحقل أيضًا سلسلة نصية:
Client.where('locked' => true)
في حالة ارتباط الانتماء (belongs_to)، يمكن استخدام مفتاح الارتباط لتحديد فيما إذا كان من المراد استخدام كائن Active Record كالقيمة الممررة. يعمل هذا التابع مع العلاقات متعددة الأشكال أيضًا.
Article.where(author: author)
Author.joins(:articles).where(articles: { author: author })
شروط المجال
Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
سيبحث هذا عن الزبائن الذين تم إنشاؤهم البارحة باستخدام استعلام BETWEEN
.
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
يمثّل هذا استخدامًا أبسط من الشروط المصفوفية.
شروط المجموعات الجزئية
إذا أردت البحث عن السجلات باستخدام تعليمة IN
، يمكنك تمرير مصفوفة للكائن Hash
الممثل للشرط:
Client.where(orders_count: [1,3,5])
سيولّد هذا استعلام SQL التالي:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
شروط النفي NOT
يمكن بناء شروط النفي باستخدام where.not
:
Client.where.not(locked: true)
كتعبير آخر، يمكن توليد هذا الاستعلام عن طريق استدعاء التابع where
بدون وسائط، ومن ثم سلسلته مع التابع not
وتمرير شروط where
. سيولّد هذا استعلام SQL التالي:
SELECT * FROM clients WHERE (clients.locked != 1)
شروط OR
يمكن استخدام شروط OR بين علاقتين عن طريق استدعاء التابع Or على أول علاقة، ومن ثم تمرير الثانية كوسيط لهذا التابع.
Client.where(locked: true).or(Client.where(orders_count: [1,3,5]))
SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5))
الترتيب
لجلب السجلات من قاعدة البيانات بترتيب معين، يمكنك استخدام التابع order
.
مثلًا، لاسترداد مجموعة من السجلات المرتّبة تصاعديًا حسب الحقل created_at
في الجدول، جرب ما يلي:
Client.order(:created_at)
# OR
Client.order("created_at")
يمكنك أيضًا تحديد فيما إذا كان التحديد تصاعدي (ASC) أو تنازلي (DESC):
Client.order(created_at: :desc)
# OR
Client.order(created_at: :asc)
# OR
Client.order("created_at DESC")
# OR
Client.order("created_at ASC")
أو الترتيب عن طريق مجموعة من الحقول:
Client.order(orders_count: :asc, created_at: :desc)
# OR
Client.order(:orders_count, created_at: :desc)
# OR
Client.order("orders_count ASC, created_at DESC")
# OR
Client.order("orders_count ASC", "created_at DESC")
في حال أردت استدعاء الترتيب عدة مرات، يمكنك سلسلة مجموعة الاستدعاءات كالتالي:
Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
تحذير: عند استخدامك الإصدار 5.7.5 وما فوقه من MySQL، فعند اختيارك للحقول باستخدام التوابع مثل select
و pluck
و ids
، سيرمي التابع order
استثناءً من النوع ActiveRecord::StatementInvalid
إلّا إذا كانت الحقول المستخدمة للترتيب مضمّنة في قائمة الحقول المختارة. اطلع على القسم التالي من أجل اختيار الحقول في مجموعة النتائج.
اختيار حقول محددة
افتراضيًا، يختار التابع Model.find
جميع الحقول في مجموعة النتائج عن طريق استخدام * select
. لاختيار مجموعة جزئية فقط من الحقول، يمكنك تحديدها باستخدام التابع select
.
مثلًا، لاختيار الحقول viewable_by
و locked
:
Client.select("viewable_by, locked")
استعلام SQL المستخدم من قبل تعليمة البحث هذه ستبدو كالتالي:
SELECT viewable_by, locked FROM clients
كن حذرًا لأن هذا يعني أنّك ستهيّء كائنًا بالحقول المختارة فقط. في حال حاولت الوصول لحقول غير مختارة، فستحصل على الخطأ التالي:
ActiveModel::MissingAttributeError: missing attribute: <attribute>
حيث تكون <attribute>
هو الخاصية المختارة. لن يرمِ التابع id
أي استثناء، لذا من المفضل أن تكون حذرًا عند التعامل مع الارتباطات، لأنها تحتاج من التابع id
أن يعمل بشكل صحيح.
في حال أردت التقاط سجل وحيد من أجل كل قيمة فريدة في حقل معين، يمكنك استخدام distinct
:
Client.select(:name).distinct
ستولّد التعليمة السابقة استعلام SQL التالي:
SELECT DISTINCT name FROM clients
يمكنك أيضًا حذف قيد الفردية:
query = Client.select(:name).distinct
# => يعيد الأسماء الفريدة
query.distinct(false)
# => يعيد جميع الأسماء، حتى في وجود التكرارات
الحد وتجاوز السجلات
لتطبيق تعليمة الحد LIMIT
على استعلام SQL المنفذ من قبل التابع Model.find
، يمكنك تحديده باستخدام التابعين limit
و offset
على العلاقة.
يمكنك استخدام التابع limit
لتحديد عدد السجلات المراد جلبها، والتابع offset
لتحديد عدد السجلات المراد تجاوزها قبل البدء بإعادة السجلات. مثلًا:
Client.limit(5)
سيعيد 5 زبائن كحد أقصى. بنا أنه لا يتجاوز السجلات، فسيعيد أول 5 سجلات من الجدول. سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM clients LIMIT 5
وبإضافة التابع offset
:
Client.limit(5).offset(30)
سيعاد 5 زبائن بشكل أعظمي بدءًا من الزبون ذي الرقم 31. سيبدو استعلام SQL كالتالي:
SELECT * FROM clients LIMIT 5 OFFSET 30
التجميع
لتطبيق تعليمة التجميع GROUP BY
على استعلام SQL المنفذ من توابع البحث، يمكنك استخدام التابع group
.
مثلًا، إذا أردت البحث عن مجموعة التواريخ التي أُنشئت فيها الطلبات:
Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
وهذا سيعطيك كائنًا وحيدًا من النوع Order
من أجل كل تاريخ أنشئت فيه طلبات في قاعدة البيانات.
سيبدو استعلام SQL كالتالي:
SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)
مجموع العناصر المجمعة
للحصول على مجموع العناصر المجمعة في استعلام وحيد، يمكنك استدعاء التابع count
بعد التابع group
.
Order.group(:status).count
# => { 'awaiting_approval' => 7, 'paid' => 12 }
سيبدو استعلام SQL المنفذ كالتالي:
SELECT COUNT (*) AS count_all, status AS status
FROM "orders"
GROUP BY status
تعليمة Having
تستخدم SQL التعليمة HAVING
لوضع شروط على الحقول المجمعة باستخدام GROUP BY
. يمكنك إضافة هذه الشروط في بنية HAVING
للتابع Model.find
عن طريق إضافة التابع having
إلى تابع البحث. إليك مثال عن ذلك:
Order.select("date(created_at) as ordered_date, sum(price) as total_price").
group("date(created_at)").having("sum(price) > ?", 100)
سيبدو استعلام SQL المنفذ كالتالي:
SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)
HAVING sum(price) > 100
سيعيد هذا التاريخ ومجموع أسعار كل كائن طلب، بعد تجميعهم عن طريق تاريخ إنشاء الطلب، حيث يكون مجموع أسعار الطلبات أكثر من 100$.
تجاوز الشروط
التابع unscope
يمكنك تحديد شروط معينة لتتم إزالتها باستخدام التابع unscope
. مثلًا:
Article.where('id > 10').limit(20).order('id asc').unscope(:order)
سيبدو استعلام SQL كالتالي:
SELECT * FROM articles WHERE id > 10 LIMIT 20
# unscope الاستعلام الأساسي دون استدعاء
SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
يمكنك أيضًا تجاوز بنى شروط where
أيضًا. مثلًا:
Article.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "articles".* FROM "articles" WHERE trashed = 0
العلاقة التي تستخدم التابع unscope
تأثر أيضًا على أي علاقة مدمجة معها:
Article.order('id asc').merge(Article.unscope(:order))
# SELECT "articles".* FROM "articles"
التابع only
يمكنك أيضًا تجاوز الشروط عن طريق التابع only
. مثلًا:
Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
# الاستعلام الأصلي دون التابع only
SELECT * FROM articles WHERE id > 10 ORDER BY id DESC LIMIT 20
التابع reorder
يتجاوز التابع reorder
الترتيب المجالي الافتراضي. مثلًا:
class Article < ApplicationRecord
has_many :comments, -> { order('posted_at DESC') }
end
Article.find(10).comments.reorder('name')
سيبدو استعلام SQL كالتالي:
SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY name
في حال عدم استخدام التابع reorder
، سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
التابع reverse_order
يعكس التابع reverse_order
الترتيب المحدد سابقًا.
Client.where("orders_count > 10").order(:name).reverse_order
سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
في حال عدم تحديد ترتيب سابقًا، يعكس التابع reverse_order
ترتيب السجلات حسب حقل المفتاح الرئيسي.
Client.where("orders_count > 10").reverse_order
سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
لا يقبل هذا التابع أيّة وسائط.
التابع rewhere
يتجاوز التابع rewhere
شروط where
موجودة مسبقًا ومسمّاة. مثلًا:
Article.where(trashed: true).rewhere(trashed: false)
سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM articles WHERE `trashed` = 0
في حال عدم استخدام التابع rewhere
:
Article.where(trashed: true).where(trashed: false)
سيبدو استعلام SQL المنفذ كالتالي:
SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
العلاقة الفارغة (Null Relation)
يعيد التابع none
علاقة قابلة للسلسلة بدون سجلات، إذ عند سلسلة المزيد من الشروط للعلاقة المعادة، سيتم توليد المزيد من العلاقات الفارغة. يفيد هذا في الحالات التي تحتاج بها إلى رد قابل للسلسلة لتابع أو نطاق يمكن أن لا يعيد سجلات إطلاقًا.
Article.none # يعيد علاقة فارغة ولا ينفذ أي استعلامات
# أدناه من المتوقع أن يعيد علاقة visible_articles التابع
@articles = current_user.visible_articles.where(name: params[:name])
def visible_articles
case role
when 'Country Manager'
Article.where(country: country)
when 'Reviewer'
Article.published
when 'Bad User'
Article.none # => returning [] or nil breaks the caller code in this case
end
end
كائنات القراءة فقط
يزوّد Active Record بالتابع readonly
في العلاقات لتحديد عدم إمكانية تعديل أي من الكائنات المعادة بشكل ظاهري. لن تنجح أي عملية لتعديل سجلات القراءة فقط، الأمر الذي يرمي استنثناءً من النوع ActiveRecord::ReadOnlyRecord
.
client = Client.readonly.first
client.visits += 1
client.save
بما أن الكائن client
محدد ليكون قابلًا للقراءة فقط، سترمي التعليمات السابقة استثناءً من النوع ActiveRecord::ReadOnlyRecord
عند استدعاء التابع client.save
بالقيمة المحدثة للحقل visits
.
منع تعديل السجلات
إن عملية القفل هي عملية مفيدة لتجنب حالات التسابق (أو حالات التعارض - Race Conditions) عند تحديث السجلات في قاعدة البيانات.
يزوّد Active Record بطريقتان للقفل:
- القفل المستبشر (Optimistic Locking).
- القفل المستطير (Pessimistic Locking).
القفل المستبشر
يمكّن القفل المستبشر مجموعة من المستخدمين الوصول إلى السجل ذاته من أجل التعديل، ويفرض أن عددًا أصغريًا من التعارضات تحصل في البيانات. يقوم بذلك عن طريق التحقق فيما إذا قامت عملية أخرى بتعديلات على السجل منذ فتحه. سيرمى الاستثناء ActiveRecord::StaleObjectError
في حال حصل ذلك، وسيتم تجاهل التحديث.
حقل القفل المستبشر
لاستخدام القفل المستبشر، يجب على الجدول أن يحوي حقلًا يسمى lock_version
من نوع القيمة الصحيحة (integer). عند تحديث سجل، يزيد Active Record قيمة الحقل lock_version
. في حال طلب تحديث بقيمة lock_version
أصغر من القيمة الموجودة حاليًا في الحقل lock_version
في قاعدة البيانات، سيفشل التعديل وسيرمى الاستثناء ActiveRecord::StaleObjectError
. مثلًا:
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
c2.name = "should fail"
c2.save # Raises an ActiveRecord::StaleObjectError
ستكون مسؤولًا بالتالي عن التحقق من التضاربات، سواءً عن طريق المرور التعاودي في الاستثناء أو عن طريق الاستعادة أو الدمج أو تطبيق منطق عمل مختلف لإصلاح التضارب.
يمكن تعطيل هذا السلوط عن طريق تعيين الخيار ActiveRecord::Base.lock_optimistically
للقيمة false
.
لتجاوز اسم الحقل lcok_version
، يزوّد الصنف ActiveRecord::Base
خاصية تدعى locking_column
:
class Client < ApplicationRecord
self.locking_column = :lock_client_column
end
القفل المستطير
يستخدم القفل غير الفعال طريقة قفل مزودة من قبل طبقة قاعدة البيانات. باستخدام التابع lock
عند بناء علاقة، سيتم بناء قفل حصري (exclusive lock) على السجلات المحددة. إن العلاقات التي تستخدم التابع lock
تكون محاطة بعملية (transaction) معظم الأوقات، وذلك لتجنب حالات القفل الميت (deadlock).
مثلًا:
Item.transaction do
i = Item.lock.first
i.name = 'Jones'
i.save!
end
الجلسة السابقة تولّد استعلام SQL التالي من أجل قاعدة بيانات MySQL:
SQL (0.2ms) BEGIN
Item Load (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE
Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
SQL (0.8ms) COMMIT
يمكنك أيضًا تمرير تعليمات SQL خام للتابع lock
من أجل السماح بأنواع مختلفة من الأقفال. مثلًا، تملك MySQL تعبيرًا يسمّى LOCK IN SHARE MODE
، حيث يمكنك فيه قفل سجل، والسماح بالاستعلامات الأخرى أن تقرأه. لتحديد هذا التعبير، مرّره كخيار للتابع lock
:
Item.transaction do
i = Item.lock("LOCK IN SHARE MODE").find(1)
i.increment!(:views)
end
في حال كان لديك كائنًا من النموذج، يمكنك بدء عملية (transaction) والحصول على القفل باستخدام تعليمة واحدة كالتالي:
item = Item.first
item.with_lock do
# ستتم استدعاء هذه الكتلة ضمن العملية
# والعنصر يكون مقفولًا
item.increment!(:views)
end
دمج الجداول
يزوّد Active Record بتابعي بحث لتحديد تعليمات الدمج (التعليمة JOIN
) على استعلام SQL المنفذ: التابع joins
والتابع left_outer_joins
. في حين أن التابع joins
يجب استخدامه من أجل الدمج الداخلي (INNER JOIN
) أو الاستعلامات المخصصة، يستخدم التابع left_outer_joins
للاستعلامات التي تستخدم الدمج الخارجي (LEFT OUTER JOIN
).
التابع joins
هناك عدة طرائق لاستخدام التابع joins
.
باستخدام قطعة SQL كسلسلة نصية
يمكنك تحديد تعليمة SQL خام لتحديد بنية الدمج JOIN
كسلسلة نصية، وتمريرها للتابع joins
:
Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")
سيولّد هذا استعلام SQL التالي:
SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'
باستخدام مصفوفة/كائن Hash من الارتباطات المسماة
يمكّنك Active Record من استخدام أسماء الارتباطات المعرّفة على النموذج كطريق قصير لتحديد بنية تعليمة الدمج لهذه الارتباطات باستخدام التابع joins
.
مثلًا، لنفرض النماذج التالية:
class Category < ApplicationRecord
has_many :articles
end
class Article < ApplicationRecord
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ApplicationRecord
belongs_to :article
has_one :guest
end
class Guest < ApplicationRecord
belongs_to :comment
end
class Tag < ApplicationRecord
belongs_to :article
end
والآن، ستولّد جميع الطرق التالية الدمج الداخلي المتوقع.
دمج ارتباط وحيد
Category.joins(:articles)
سيولّد ذلك:
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = categories.id
أو، باللغة الفصيحة: "أعد الكائن Category
من أجل جميع الفئات (categories) التي تملك مقالات (articles)". والآن عند رؤيتك لقيم مكررة فهذا يعني أنها تملك أكثر من مقالة واحدة لنفس الفئة. في حال أردت الأصناف الفريدة فقط، يمكنك استخدام:
Category.joins(:articles).distinct
دمج مجموعة ارتباطات
Article.joins(:category, :comments)
سيولّد ذلك:
SELECT articles.* FROM articles
INNER JOIN categories ON categories.id = articles.category_id
INNER JOIN comments ON comments.article_id = articles.id
أو، باللغة الفصيحة: "أعد جميع المقالات (articles) التي تملك فئةً (category)، وتعليقًا (comment) واحدًا على الأقل". لاحظ أنه قد تجد مقالات مكررة مجددًا.
دمج الارتباطات المتداخلة (مستوى واحد)
Article.joins(comments: :guest)
سيولّد ذلك:
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
أو، باللغة الفصيحة: "أعد جميع المقالات التي تملك تعليقًا مكتوبًا من قبل زائر (guest)".
دمج الارتباطات المتداخلة (مستويات متعددة)
Category.joins(articles: [{ comments: :guest }, :tags])
سيولّد ذلك:
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = categories.id
INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
INNER JOIN tags ON tags.article_id = articles.id
أو، باللغة الفصيحة: "أعد جميع الأصناف التي تملك مقالات تحتوي بدورها على تعليق مكتوب من قبل زائر، وتحتوي أيضًا على وسم (tag)".
تحديد الشروط على الجداول المدمجة
يمكنك تحديد شروط على الجداول المدمجة باستخدام الشروط المصفوفية أو النصية المعتادة. يمكن الحصول على نمط خاص لكتابة الشروط للجداول المدمجة باستخدام كائنات Hash
:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)
يمكن دمج شروط hash بطريقة أكثر سلاسة وأسهل للقراءة:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(orders: { created_at: time_range })
سيبحث ذلك عن كل الزبائن الذين يملكون طلبات أنشئت البارحة، مجدًدا باستخدام تعليمة BETWEEN
الخاصة بـ SQL.
التابع left_outer_joins
في حال أردت اختيار مجموعة من السجلات بغض النظر عن امتلاكها لسجلات مرتبطة، يمكنك استخدام التابع left_outer_joins
.
Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
سيولّد ذلك:
SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id
الذي يعني: "أعد جميع الكتّاب (authors) مع عدد المنشورات (posts) الخاصة بهم، بغض النظر عن امتلاكهم أو عدم امتلاكهم للمنشورات".
التحميل الحثيث للارتباطات
إن التحميل الحثيث (Eager loading) هو طريقة من طرائق تحميل ارتباطات السجلات المعادة من التابع Model.find
، والتي تستخدم أقل عدد ممكن من الاستعلامات.
مشكلة الـ N + 1 استعلام
لنفرض التعليمات التالية التي تبحث عن 10 زبائن وتطبع عناوينهم البريدية:
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
قد تبدو هذه التعليمات صحيحة من النظرة الأولى، لكن المشكلة تكمن بعدد الاستعلامات المنفذة. ستنفّذ التعليمات السابقة استعلامًا واحدًا من أجل تحميل 10 زبائن، و10 استعلامات من أجل كل زبون لتحميل عنوانه، مما ينفذ 11 استعلام ككل.
الحل لمشكلة الـ N + 1 استعلام
يمكّنك Active Record من تحديد الارتباطات المراد تحميلها بشكل مسبق. يمكن هذا عن طريق تحديد التابع includes
لاستدعاء Model.find
. باستخدام includes
، سيتأكد Active Record من أن جميع الارتباطات قد تم تحميلها باستخدام أقل عدد من الاستعلامات.
بالعودة للتعليمات السابقة، يمكننا كتابة التالي لتحميل العناوين بشكل حثيث:
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
ستولّد التعليمات السابقة استعلامين فقط، بدلًا من 11 استعلام في الحالة السابقة، وهذه الاستعلامات هي:
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
التحميل الحثيث لمجموعة من الارتباطات
يمكّنك Active Record من تحميل مجموعة من الارتباطات بشكل حثيث ضمن استدعاء Model.find
وحيد، عن طريق استخدام مصفوفة، كائن Hash
، أو كائن Hash
متداخل مع مصفوفات باستخدام التابع includes
.
مصفوفة ارتباطات
Article.includes(:category, :comments)
سيحمّل هذا جميع المقالات والفئات المرتبطة بها والتعليقات لكل مقال.
كائنات Hash
المتداخلة للارتباطات
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
سيبحث هذا عن الفئة ذات المعرف1 وسيقوم بتحميل حثيث لجميع المقالات المرتبطة، وجميع وسوم المقالات المرتبطة، وتعليقات الزوار المرتبطة أيضًا.
فرض شروط على التحميل الحثيث
في حين أن Active Record يمكّنك من فرض شروط على الارتباطات المحملة بشكل حثيث كما هو الحال في التابع joins
، إلّا أنّه من المفضّل استخدام التابع joins
بدلًا من ذلك.
لكن إذا أردت استخدام هذه الطريقة، يمكنك استخدام التابع where
كما هو الحال طبيعيًا.
Article.includes(:comments).where(comments: { visible: true })
سيولّد هذا استعلامًا يحوي LEFT OUTER JOIN
، في حين أنّه يقوم التابع joins
بتوليد استعلام يحوي INNER JOIN
بدلًا من ذلك.
SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)
If
في حال عدم وجود شرط where
، سيولّد هذا مجموعة عادية من استعلامين.
ملاحظة: إن استخدام where
، فسيعمل فقط عند تمريرك للكائن Hash
. من أجل قطع SQL، يجب أن تستخدم references
لفرض دمج الجداول:
Article.includes(:comments).where("comments.visible = true").references(:comments)
في حال استخدام هذه الطريقة من التحميل، وعند عدم وجود تعليقات لأي من المقالات، سيتم مع ذلك تحميل جميع المقالات. باستخدام joins
(أي INNER JOIN
)، تجب مطابقة شرط الدمج، وإلّا لن تعاد أيّة سجلات.
ملاحظة: في حال تمّ تحميل ارتباط بشكل حثيث كجزء من عملية دمج، لن تجد أي حقول مستخدمة في بنية اختيار مخصصة على النماذج المحملة. هذا لتجنب تضارب الحقول بين السجلات الآباء والأبناء.
النطاقات
تمكّنك النطاقات (Scopes) من تحديد استعلامات مستخدمة غالبًا والتي يمكن الرجوع لها كاستدعاءات توابع على الكائنات المرتبطة أو النماذج. مع هذه النطاقات، يمكنك استخدام جميع التوابع المذكورة مسبقًا مثل where
و joins
و includes
. تعيد جميع توابع النطاقات الكائن ActiveRecord::Relation
والذي يمكّن التوابع الأخرى (مثل النطاقات الأخرى) من أن يتم استدعاؤها على العلاقة نفسها.
لتعريف نطاق بسيط، يمكنك استخدام التابع scope
داخل الصنف، وتمرير الاستعلام المستخدم عند استدعاء النطاق:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
يطابق هذا تعريف تابع في الصنف، والطريقة المستخدمة هي حسب تفضيلك الشخصي:
class Article < ApplicationRecord
def self.published
where(published: true)
end
end
يمكن أيضًا سلسلة النطاقات داخل النطاقات:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
لاستدعاء النطاق published
، يمكننا استدعاؤها إمّا على الصنف:
Article.published # => [published articles]
أو على ارتباط يحوي على الكائن Article
:
category = Category.first
category.articles.published # => [published articles belonging to this category]
تمرير الوسائط
يمكن للنطاقات أن تأخذ وسائطًا مثل:
class Article < ApplicationRecord
scope :created_before, ->(time) { where("created_at < ?", time) }
end
ومن ثم استدعاؤها كما لو كانت تابعًا في الصنف:
Article.created_before(Time.zone.now)
لكن هذا قد يكرر الوظيفية المزودة من تعريف تابع الصنف.
class Article < ApplicationRecord
def self.created_before(time)
where("created_at < ?", time)
end
end
إن استخدام تابع الصنف هو الطريقة المفضلة لإنشاء النطاقات التي تقبل وسائط ممررة لها. لا يزال بالإمكان استدعاء هذه التوابع من الكائنات المرتبطة:
category.articles.created_before(time)
استخدام الشروط
يمكن لنطاقك أن يستخدم شروطًا معينة:
class Article < ApplicationRecord
scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
end
كما هو الحال في الأمثلة السابقة، سيعمل هذا بشكل مطابق لتوابع الصنف:
class Article < ApplicationRecord
def self.created_before(time)
where("created_at < ?", time) if time.present?
end
end
لكن هناك اختلاف وحيد: سيعيد النطاق كائنًا من النوع ActiveRecord::Relation
بشكل دائم، حتى لو لم يحقّق الشرط الموجود، في حين أن تابع الصنف سيعيد nil
عند عدم تحقيق الشرط. قد يسبب هذا بالخطأ NoMethodError
عند سلسلة (chaining) توابع الصنف مع الشروط، وذلك عندما يكون أحد الشروط false
.
تطبيق نطاق افتراضي
إذا أردنا تطبيق نطاق بشكل دائم على كل الاستعلامات المستخدمة في النموذج، يمكننا تحديد ذلك في التابع default_scope
ضمن النموذج نفسه:
class Client < ApplicationRecord
default_scope { where("removed_at IS NULL") }
end
عند تنفيذ الاستعلامات على هذا النموذج، سيبدو استعلام SQL المنفذ الآن كالتالي:
SELECT * FROM clients WHERE removed_at IS NULL
في حال أردنا فعل أشياء أكثر تعقيدًا ضمن النطاق الافتراضي، يمكننا تعريفه تابعًا في الصنف:
class Client < ApplicationRecord
def self.default_scope
# ActiveRecord::Relation يجب أن يعيد
end
end
ملاحظة: يطبق النطاق الافتراضي default_scope
أيضًا أثناء إنشاء أو بناء سجل عند تمرير وسائط النطاق ككائن Hash
. ولن يطبق عند تحديث السجل، مثلًا:
class Client < ApplicationRecord
default_scope { where(active: true) }
end
Client.new # => #<Client id: nil, active: true>
Client.unscoped.new # => #<Client id: nil, active: nil>
لاحظ أنه عند استخدام الشكل المصفوفي، لن يكون من الممكن تحويل وسائط الاستعلامات إلى كائن Hash
من أجل الإسناد الافتراضي للحقول. مثلًا:
class Client < ApplicationRecord
default_scope { where("active = ?", true) }
end
Client.new # => #<Client id: nil, active: nil>
دمج النطاقات
كما هو الحال في where
، يمكن دمج النطاقات باستخدام شروط AND
.
class User < ApplicationRecord
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
User.active.inactive
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
يمكننا دمج ومطابقة التابع scope
مع التابع where
، وسيحوي الاستعلام الأخير على جميع الشروط المدموجة بواسطة AND
.
User.active.where(state: 'finished')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
في حال أردنا من آخر تعليمة where
أن تتحقق وتُطبَّق، يمكن استخدام Relation.merge
.
User.active.merge(User.inactive)
# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
الاختلاف الوحيد المهم هو أن النطاق الافتراضي سيتم إلحاقه افتراضيًا في شروط scope
و where
.
class User < ApplicationRecord
default_scope { where state: 'pending' }
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
User.all
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
User.active
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
User.where(state: 'inactive')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
كما ترى أعلاه، يتم دمج النطاق الافتراضي في شرطي scope
و where
.
إزالة جميع النطاقات
في حال أردنا إزالة جميع النطاقات لأي سبب كائن، يمكننا استخدام التابع unscope
. يكون هذا مفيدًا عندما يتم تحديد نطاق افتراضي على نموذج ما، ولا نريد تطبيقه على استعلام ما.
Client.unscoped.load
يقوم هذا بحذف جميع النطاقات وتنفيذ استعلام عادي على الجدول.
Client.unscoped.all
# SELECT "clients".* FROM "clients"
Client.where(published: false).unscoped.all
# SELECT "clients".* FROM "clients"
يقبل للتابع unscoped
هيكلًا من التعليمات.
Client.unscoped {
Client.created_before(Time.zone.now)
}
توابع البحث الديناميكية
من أجل كل حقل (أو خاصية) تعرّفه في جدولك، يزوّدك Active Record بتابع بحث. إذا ملكت حقلًا مسمى first_name
على النموذج Client
مثلًا، ستحصل على التابع find_by_first_name
بالمجان من Active Record. وإذا ملكت حقلًا مسمى locked
على النموذج Client
، ستحصل أيضًا على التابع find_by_locked
.
يمكنك أيضًا تحديد إشارة التعجب في نهاية توابع البحث الديناميكية لتجعلها ترمي الاستثناء ActiveRecord::RecordNotFound
في حال لم تعد أيّة سجلات، مثلًا:
Client.find_by_name!("Ryan")
وفي حال أردت البحث عن طريق الحقلين name
و locked
معًا، يمكنك إمّا سلسلة هذه التوابع معًا، أو كتابة "and" بين أسماء الحقلين. مثلًا:
Client.find_by_first_name_and_locked("Ryan", true)
الماكرو enums
يعين الماكرو enum
حقلًا عدديًّا صحيحًا لمجموعة من القيم الممكنة.
class Book < ApplicationRecord
enum availability: [:available, :unavailable]
end
سينشئ هذا النطاقات الموافقة على النموذج تلقائيًا. تضاف أيضًا التوابع للتنقل بين الحالات الممكنة والاستعلام عن الحالة الحالية للنموذج.
# يبحث المثلان التاليان عن الكتب المتوافرة
Book.available
# أو
Book.where(availability: :available)
book = Book.new(availability: :available)
book.available? # => true
book.unavailable! # => true
book.available? # => false
اقرأ التوثيق الكامل حول الماكرو enums لمزيد من التفاصيل.
فهم سَلسَلة التوابع
يطبّق نمط Active Record مفهوم سَلْسَلة (Chaining) التوابع، الذي يمكننا من استخدام مجموعة من توابع Active Record معًا في طريقة سَلِسَلةٍ وبسيطةٍ.
يمكنك سَلسَلة التوابع في تعليمة واحدة عندما يعيد التابع السابق علاقة (كائنًا من نوع ActiveRecord::Relation
)، مثل التوابع all
و where
و joins
. التوابع التي تعيد كائنًا وحيدًا يجب أن تكون في نهاية التعليمة.
هناك بعض الأمثلة أدناه، ولكن لا يغطي هذا التوثيق جميع الحالات المحتملة بل يتطرق إلى كم صغير منها فقط. عند استدعاء تابع في Active Record، لن يتم تنفيذ الاستعلام وإرساله إلى قاعدة البيانات، إذ سيحصل ذلك فقط عند الحاجة للبيانات. لذلك كل مثال من الأمثلة التالية سيولّد فقط استعلامًا واحدًا.
جلب بيانات مرشَّحة من مجموعة جداول
Person
.select('people.id, people.name, comments.text')
.joins(:comments)
.where('comments.created_at > ?', 1.week.ago)
سيبدو الناتج كالتالي:
SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
ON comments.person_id = people.id
WHERE comments.created_at > '2015-01-01'
جلب بيانات محددة من مجموعة جداول
Person
.select('people.id, people.name, companies.name')
.joins(:company)
.find_by('people.name' => 'John') # this should be the last
سيولّد ذلك الاستعلام التالي:
SELECT people.id, people.name, companies.name
FROM people
INNER JOIN companies
ON companies.person_id = people.id
WHERE people.name = 'John'
LIMIT 1
ملاحظة: في حال عثر الاستعلام على مجموعة من السجلات، سيقرأ التابع find_by
أول سجل فقط وسيتجاهل السجلات الأخرى.
البحث عن كائن أو بناؤه
من الضروري أحيانًا البحث عن سجل أو بناؤه في حال عدم وجوده. يمكنك تحقيق ذلك باستخدام التوابع find_or_create_by
أو !find_or_create_by
.
التابع find_or_create_by
يتحقق التابع find_or_create_by
من أن سجلًا ما موجود بالحقول المعطاة. وفي حال عدم وجوده، سيتم استدعاء التابع create
.
مثلًا، في حال أردت البحث عن زبون يدعى Sara
، وفي حال عدم وجوده يجب إنشاؤه. يمكنك تحقيق ذلك عن طريق تنفيذ التالي:
Client.find_or_create_by(first_name: 'Sara')
# => #<Client id: 1, first_name: "Sara", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
سيبدو استعلام SQL كالتالي:
SELECT * FROM clients WHERE (clients.first_name = 'Sara') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Sara', 1, NULL, '2011-08-30 05:22:57')
COMMIT
يعيد التابع find_or_create_by
إما السجل الموجود مسبقًا، أو السجل الجديد. في حالتنا، كنا لا نملك زبونًا يدعى Sara، لذا تم إنشاء السجل وإعادته مباشرةً.
قد لا يتم حفظ السجل في قاعدة البيانات، إذ يعتمد ذلك على مرور السجل للتأكيدات الموجودة أو لا (كما هو الحال في create
).
لنفرض أننا بحاجة لتعيين الحقل locked
ليصبح false
عند إنشائنا لسجل جديد، لكننا لا نحتاج لتضمينه في الاستعلام. لذا سنحتاج للبحث عن الزبون المسمى Sara، وفي حال عدم وجود هذا الزبون، إنشاء زبون باسم "Sara"، وتعيين الحقل locked
إلى false
.
يمكننا تحقيق ذلك بطريقتين، الأولى باستخدام create_with
:
Client.create_with(locked: false).find_or_create_by(first_name: 'Sara')
والثانية باستخدام كتلة معطاة:
Client.find_or_create_by(first_name: 'Sara') do |c|
c.locked = false
end
سيتم تنفيذ الكتلة فقط عند إنشاء الزبون، إذ أنه في المرة الثانية من تنفيذ هذه التعليمة، سيتم تجاهل الكتلة.
التابع !find_or_create_by
يمكنك أيضًا استخدام التابع !find_or_create_by
لرمي استثناء في حال كان السجل الجديد خطأ. لن تتم تغطية التحققات من الصحة في هذا التوثيق، لكننا لنفرض أنك قمت بإضافة التحقق التالي:
validates :orders_count, presence: true
في نموذج الزبون Client
. والآن عند محاولتك لإنشاء زبون دون تمرير الحقل orders_count
، سيكون السجل خطأ وسيتم رمي استثناء:
Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
التابع find_or_initialize_by
يعمل التابع find_or_initialize_by
تمامًا مثل التابع find_or_create_by
، لكنه سيستدعي التابع new
بدلًا من create
. مما يعني أنه لن يتم حفظ الكائن في قاعدة البيانات، وإنما فقط في الذاكرة. باستكمال المثال الموضح أعلاه، نحتاج الآن إلى إنشاء الزبون المسمى Fadi:
fadi = Client.find_or_initialize_by(first_name: 'Fadi')
# => #<Client id: nil, first_name: "Fadi", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
fadi.persisted?
# => false
fadi.new_record?
# => true
لأن الكائن لم يتم حفظه بعد في قاعدة البيانات، سيبدو استعلام SQL المولّد كالتالي:
SELECT * FROM clients WHERE (clients.first_name = 'Fadi') LIMIT 1
عندما تريد حفظه في قاعدة البيانات، استدعِ التابع save:
fadi.save
# => true
البحث بواسطة SQL
في حال أردت استخدام تعليمات SQL الخاصة بك للبحث عن سجلات في جدول ما، يمكنك استخدام التابع find_by_sql
. يعيد هذا التابع مصفوفة من الكائنات حتى لو كانت مجموعة النتائج تحوي كائنًا وحيدًا. مثلًا، يمكنك تنفيذ الاستعلام التالي:
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER BY clients.created_at desc")
# => [
# #<Client id: 1, first_name: "Lucas" >,
# #<Client id: 2, first_name: "Jan" >,
# ...
# ]
يزوّدك التابع find_by_sql
بطريقة بسيطة لإنشاء استدعاءات مخصصة لقاعدة البيانات واسترداد الكائنات المهيّئة.
التابع select_all
يملك التابع find_by_sql
تابعًا قريبًا منه يسمى connection.select_all
. يسترد هذا التابع الكائنات من قاعدة البيانات بواسطة SQL مخصص كما هو الحال في التابع find_by_sql
، لكنه لن يهيّء هذه الكائنات. إذ سيعيد كائنًا من نوع ActiveRecord::Result
، وعند استدعاء to_hash
على هذا الكائن، سيتم تحويله إلى مصفوفة من الكائنات Hash
حيث كل جدول Hash
يمثل سجلًا.
Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'").to_hash
# => [
# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
# ]
التابع pluck
يمكن استخدام التابع pluck
للاستعلام عن حقل وحيد أو مجموعة حقول من الجدول الموافق للنموذج. يقبل هذا التابع قائمة بأسماء الحقول كوسيط له، ويعيد مصفوفة القيم بالحقول المحددة وأنواع بياناتها.
Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]
Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']
Client.pluck(:id, :name)
# SELECT clients.id, clients.name FROM clients
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
يجعل التابع pluck
من الممكن أن يستبدل بتعليمات مثل:
Client.select(:id).map { |c| c.id }
# أو
Client.select(:id).map(&:id)
# أو
Client.select(:id, :name).map { |c| [c.id, c.name] }
التعليمات التالية:
Client.pluck(:id)
# أو
Client.pluck(:id, :name)
على نقيض التابع select
، سيحول التابع pluck
نتيجة قاعدة البيانات مباشرةً إلى مصفوفة، دون الحاجة لتهيئة كائنات Active Record. يؤدي ذلك إلى تحسين الأداء من أجل استعلام ضخم ومنفذ بشكل متردد. لكن، لن تكون التوابع المتجاوزة في النموذج متاحةً. مثلًا:
class Client < ApplicationRecord
def name
"I am #{super}"
end
end
Client.select(:name).map &:name
# => ["I am David", "I am Jeremy", "I am Jose"]
Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]
أيضًا على نقيض sleect
أو نطاقات العلاقة Relation
، ينفذ التابع pluck
استعلامًا مباشرًا، وبالتالي لا يمكن سَلسَلته بنطاقات لاحقة، لكنه يعمل بنطاقات مطبقة سابقًا:
Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
Client.limit(1).pluck(:name)
# => ["David"]
التابع ids
يستخدم التابع ids
لتحديد فقط المعرفات الخاصة بالعلاقات باستخدام المفتاح الرئيسي للجدول.
Person.ids
# SELECT id FROM people
class Person < ApplicationRecord
self.primary_key = "person_id"
end
Person.ids
# SELECT person_id FROM people
وجود الكائنات
في حال أردت التحقق من وجود الكائنات، يوجد تابع يسمى ?exists
. يقوم هذا التابع بتنفيذ استعلام على قاعدة البيانات باستخدام نفس استعلام التابع find
، لكن بدلًا من إعادة الكائن ككل، يعيد القيمة true
أو false
.
Client.exists?(1)
يأخذ التابع ?exists
مجموعة قيم، لكن الاختلاف أنه سيعيد true
في حال وجود أي من هذه السجلات.
Client.exists?(id: [1,2,3])
# أو
Client.exists?(name: ['John', 'Sergei'])
يمكن أيضًا استخدام التابع ?exists
دون تمرير الوسائط على النموذج أو العلاقة.
Client.where(first_name: 'Ryan').exists?
تعيد التعليمة السابقة القيمة true
في حال كان هناك زبون واحد على الأقل بالاسم Ryan، والقيمة false
فيما عدا ذلك.
Client.exists?
تعيد التلعيمة السابقة القيمة false
في حال كان الجدول clients
فارغًا، والقيمة true
فيما عدا ذلك.
يمكنك أيضًا استخدام التوابع ?any
و ?many
للتحقق من وجود نموذج أو علاقة.
# عبر نموذج
Article.any?
Article.many?
# عبر مجال مسمى
Article.recent.any?
Article.recent.many?
# عبر علاقة
Article.where(published: true).any?
Article.where(published: true).many?
# عبر ارتباط
Article.first.categories.any?
Article.first.categories.many?
الحسابات
يستخدم هذا القسم التابع count
كتابع مثال عن هذا المفهوم، لكن الخيارات المشروحة يمكن استخدامها لجميع الأقسام الفرعية.
تعمل جميع توابع الحساب على النموذج مباشرةً:
Client.count
# SELECT COUNT(*) FROM clients
أو على علاقة:
Client.where(first_name: 'Ryan').count
# SELECT COUNT(*) FROM clients WHERE (first_name = 'Ryan')
يمكنك أيضًا استخدام توابع البحث المتعددة على علاقة لتنفيذ عمليات حساب معقدة:
Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
والذي سينفذ:
SELECT COUNT(DISTINCT clients.id) FROM clients
LEFT OUTER JOIN orders ON orders.client_id = clients.id
WHERE (clients.first_name = 'Ryan' AND orders.status = 'received')
التابع count
في حال أردت معرفة عدد السجلات الموجودة في جدول النموذج، يمكنك استدعاء Client.count
. في حال أردت أن تكون أكثر دقة، وأردت البحث عن الزبائن الذين يملكون أعمارًا في قاعدة البيانات، يمكنك استخدام (Client.count(:age
.
التابع average
في حال أردت معرفة معدل رقم معين في جدول من جداولك، يمكنك استدعاء التابع average
مع الصنف المتعلق بالجدول. يعمل الاستدعاء هذا كالتالي:
Client.average("orders_count")
سيعيد ذلك عددًا (غالبًا عددًا عشريًا مثل 3.14159265) الذي يمثل معدل قيم هذا الحقل.
التابع minimum
في حال أردت معرفة أصغر قيمة موجودة في حقل من حقول جدولك، يمكنك استدعاء التابع minumum
على الصنف المتعلق بالجدول. يعمل استدعاء التابع الكتالي:
Client.minimum("age")
التابع maximum
في حال أردت معرفة أكبر قيمة موجودة في حقل من حقول جدولك، يمكنك استدعاء التابع maximum
على الصنف المتعلق بالجدول. يعمل استدعاء التابع الكتالي:
Client.maximum("age")
التابع sum
في حال أردت معرفة مجموع عناصر حقل مين في جدولك، يمكنك استدعاء التابع sum
على الصنف المتعلق بالجدول. يعمل استدعاء التابع كالتالي:
Client.sum("orders_count")
تنفيذ تعليمة EXPLAIN
يمكنك تنفيذ تعليمة EXPLAIN
على الاستعلامات المولّدة من العلاقات، مثلًا:
User.where(id: 1).joins(:articles).explain
قد يولّد:
EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+----------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+----------+-------+---------------+
| 1 | SIMPLE | users | const | PRIMARY |
| 1 | SIMPLE | articles | ALL | NULL |
+----+-------------+----------+-------+---------------+
+---------+---------+-------+------+-------------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------------+
| PRIMARY | 4 | const | 1 | |
| NULL | NULL | NULL | 1 | Using where |
+---------+---------+-------+------+-------------+
2 rows in set (0.00 sec)
عند استخدام قواعد البيانات MySQL و MariaDB.
ينفّذ Active Record طباعة جميلة لمحاكاة طباعة سطر أوامر قاعدة البيانات. لذا من أجل نفس الاستعلام وعند استخدام قاعدة بيانات PostgreSQL، ستولّد تعليمة EXPLAIN
التالي:
EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1
QUERY PLAN
------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
Join Filter: (articles.user_id = users.id)
-> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
Index Cond: (id = 1)
-> Seq Scan on articles (cost=0.00..28.88 rows=8 width=4)
Filter: (articles.user_id = 1)
(6 rows)
قد يولّد التحميل الحثيث أكثر من استعلام في الخلفية، وقد تحتاج بعض الاستعلامات نتائج الاستعلامات السابقة. بسبب ذلك، ينفذ التابع explain
الاستعلام أولًا، ومن ثم يطلب خطة الاستعلام. مثلًا:
User.where(id: 1).includes(:articles).explain
يولّد:
EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+-------+-------+---------------+
| 1 | SIMPLE | users | const | PRIMARY |
+----+-------------+-------+-------+---------------+
+---------+---------+-------+------+-------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4 | const | 1 | |
+---------+---------+-------+------+-------+
1 row in set (0.00 sec)
EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1)
+----+-------------+----------+------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+----------+------+---------------+
| 1 | SIMPLE | articles | ALL | NULL |
+----+-------------+----------+------+---------------+
+------+---------+------+------+-------------+
| key | key_len | ref | rows | Extra |
+------+---------+------+------+-------------+
| NULL | NULL | NULL | 1 | Using where |
+------+---------+------+------+-------------+
1 row in set (0.00 sec)
عند استخدام قواعد البيانات MySQL و MariaDB.
فهم خرج تعليمة EXPLAIN
إن فهم خرج تعليمة EXPLAIN
هو خارج حدود هذا التوثيق. قد تفيدك مراجع أخرى في ذلك: