ارتباطات Active Record في ريلز

من موسوعة حسوب
اذهب إلى: تصفح، ابحث

يغطي هذا الدليل مزايا الارتباطات Active Record. بعد قراءة هذا الدليل، ستتعلم:

  • كيفية التصريح عن الارتباطات بين نماذج Active Record.
  • الأنواع المختلفة للارتباطات في Active Record.
  • كيفية استعمال التوابع المضافة إلى نماذجك عبر إنشاء الارتباطات.

محتويات

لمَ نستخدم الارتباطات؟

في ريلز، يعدّ الارتباط اتصالًا بين نموذجين من Active Record. لمَ نحتاج إلى الارتباطات بين النماذج؟ لأنها تجعل العمليات الشائعة سهلة وبسيطة في تطبيقك. مثلًا، لنفرض أن لدينا تطبيق ريلز بسيط يحوي نموذجًا للكتّاب ونموذجًا للكتب؛ كل كاتب يملك العديد من الكتب. بدون الارتباطات، سيبدو تعريف النموذج بالشكل التالي:
class Author < ApplicationRecord
end
 
class Book < ApplicationRecord
end
الآن، لنفرض أننا بحاجة إلى إضافة كتاب جديد لكاتب موجود. علينا أن نفعل التالي:
@book = Book.create(published_at: Time.now, author_id: @author.id)
أو لنفرض أننا بحاجة لحذف كاتب، والتأكد من حذف جميع كتبه كذلك:
@books = Book.where(author_id: @author.id)
@books.each do |book|
  book.destroy
end
@author.destroy
باستخدام ارتباطات Active Record، يمكننا تسهيل هذه العمليات (والكثير من العمليات الأخرى) عن طريق إخبار ريلز أن هناك اتصالًا بين هذين النموذجين. إليك النمط المحسّن من التعليمات السابقة، باستخدام الارتباطات:
class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end
 
class Book < ApplicationRecord
  belongs_to :author
end
بهذا التعديل، يمكن بسهولة إنشاء كتاب جديد من أجل كاتب محدد:
@book = @author.books.create(published_at: Time.now)
أيضًا، حذف الكاتب مع جميع كتبه أصبح أمرًا سهلًا جدًا:
@author.destroy
لتتعلّم المزيد عن الأنواع المختلفة من الارتباطات، اقرأ القسم التالي من هذا التوثيق؛ يليه بعض النصائح عن العمل مع الارتباطات، وثمّ مرجع كامل للتوابع والخيارات المستخدمة في ارتباطات Active Record.

أنواع Active Record

يدعم ريلز ستة أنواع من الارتباطات:

  • belongs_to (ينتمي إلى)
  • has_one (ارتباط الفردية)
  • has_many (ارتباط التعددية)
  • has_many :through (ارتباط التعددية الضمني)
  • has_one :through (ارتباط الفردية الضمني)
  • has_and_belongs_to_many (ارتباط الانتماء والتعددية)

تُنفَّذ الارتباطات عن طريق الاستدعاء بنمط الماكرو (macro-style calls)، وبذلك تتمكن من إضافة الميزات بشكل صريح لنماذجك. مثلًا، عن طريق التعريف أن نموذجًا ما ينتمي إلى (belongs_to) نموذج آخر، تخبر بذلك ريلز أن تُبقي على معلومات المفاتيح الرئيسية والأجنبية بين سجلات النموذجين، وتحصل أيضًا على مجموعة من التوابع المساعدة المضافة على نموذجك.

في بقية هذا التوثيق، ستتعلم كيف تعرّف وتستخدم الأشكال المختلفة من الارتباطات. لكن أولًا، من المفضل الاطلاع على الحالات المناسبة لكل نوع من أنواع الارتباطات.

ارتباط الانتماء (belongs_to)

يحدد ارتباط الانتماء اتصال واحد إلى واحد (one-to-one) مع نموذج آخر، بحيث أنّ كل سجل من النموذج المعرّف للارتباط ينتمي إلى سجل واحد من النموذج الآخر. مثلًا، في حال تضمّن تطبيقك الكتب والكتّاب، ويمكن لكل كتاب أن يملك كاتبًا واحدًا فقط، ستعرّف النموذج كالتالي:
class Book < ApplicationRecord
  belongs_to :author
end
إنشاء ارتباط انتماء (belongs_to) بين نموذج الكتب ونموذج الكتَّاب.
إنشاء ارتباط انتماء (belongs_to) بين نموذج الكتب ونموذج الكتَّاب.

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

سيبدو التهجير الموافق كالتالي:
class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :books do |t|
      t.belongs_to :author, index: true
      t.datetime :published_at
      t.timestamps
    end
  end
end

ارتباط الملكية (has_one)

يحدّد ارتباط الفردية أيضًا علاقة واحد إلى واحد (one-to-one) مع نموذج آخر، لكن باستخدام مخططات ونتائج مختلفة. يحدّد هذا الارتباط أن كل سجل من نموذج ما يملك أو يحوي سجلًا آخر من نموذج آخر. مثلًا، في حال كان المزوّد في تطبيقك يملك فقط حسابًا وحيدًا، فستُعرّف علاقتك كالتالي:
class Supplier < ApplicationRecord
  has_one :account
end
إنشاء ارتباط الملكية بين نموذج الحسابات ونموذج المزودين.
إنشاء ارتباط الملكية بين نموذج الحسابات ونموذج المزودين.
قد يبدو التهجير الموافق كالتالي:
class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps
    end
  end
end
بناءً على حالة الاستخدام، قد تحتاج إلى إنشاء قيد فهرس فريد و/أو قيد مفتاح أجنبي على حقل المزوّد في جدول الحسابات. في هذه الحالة، سيبدو تعريف الحقل كالتالي:
create_table :accounts do |t|
  t.belongs_to :supplier, index: { unique: true }, foreign_key: true
  # ...
end

ارتباط التعددية (has_many)

يحدد ارتباط التعددية علاقة واحد إلى كثير (one-to-many) مع نموذج آخر. تجد هذا الارتباط عادةً في الطرف الثاني من ارتباط الانتماء. يحدد هذا الارتباط أن كل سجل من النموذج يملك صفر أو أكثر من السجلات من نموذج آخر. مثلًا، في تطبيق يحوي كتبًا وكتّابًا، يمكن تعريف نموذج الكاتب كالتالي:
class Author < ApplicationRecord
  has_many :books
end
ملاحظة: إن اسم النموذج الآخر يكون بنمط الجمع عند تعريف ارتباط التعددية.
إنشاء ارتباط تعددية (has_many) بين نموذج الكتب ونموذج الكتَّاب.
إنشاء ارتباط تعددية (has_many) بين نموذج الكتب ونموذج الكتَّاب.
قد يبدو التهجير الموافق كالتالي:
class CreateAuthors < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :books do |t|
      t.belongs_to :author, index: true
      t.datetime :published_at
      t.timestamps
    end
  end
end

ارتباط التعددية الضمني (has_many :through)

يُستخدم ارتباط التعددية الضمني عادةً من أجل تهيئة اتصال كثير إلى كثير (many-to-many) مع نموذج آخر. يحدّد هذا الارتباط أن النموذج المعرّف يمكن مطابقته مع صفر أو أكثر من النموذج الآخر عن طريق العبور (through) ضمن نموذج ثالث. مثلًا، لنفرض أنه لدينا تطبيقًا طبّيًا يحجز فيه المرضى مواعيدًا مع الأطباء. يمكن أن تبدو الارتباطات الموافقة كالتالي:
class Physician < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end
 
class Appointment < ApplicationRecord
  belongs_to :physician
  belongs_to :patient
end
 
class Patient < ApplicationRecord
  has_many :appointments
  has_many :physicians, through: :appointments
end
إنشاء ارتباط التعددية الضمني بين ثلاثة نماذج في تطبيق طبي لحجز المواعيد بين المريض والطبيب.
إنشاء ارتباط التعددية الضمني بين ثلاثة نماذج في تطبيق طبي لحجز المواعيد بين المريض والطبيب.
قد يبدو التهجير الموافق كالتالي:
class CreateAppointments < ActiveRecord::Migration[5.0]
  def change
    create_table :physicians do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :patients do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :appointments do |t|
      t.belongs_to :physician, index: true
      t.belongs_to :patient, index: true
      t.datetime :appointment_date
      t.timestamps
    end
  end
end
يمكن إدارة مجموعات نماذج الربط (join models) من خلال توابع ارتباط التعددية. مثلًا، في حال قمت بالتالي:
physician.patients = patients
سيتم تلقائيًا إنشاء نماذج الربط من أجل الكائنات المترابطة حديثًا. وفي حال كانت بعض هذه السجلات موجودة مسبقًا ولم تعد كذلك الآن، فسيتم حذف سجلاتها المرتبطة تلقائيًا.

تحذير: يتم حذف نماذج الربط مباشرةً، ولا يتم تشغيل أي من توابع رد النداء الخاصة بتدمير السجلات.

يفيد ارتباط التعددية الضمني أيضًا في تعيين الاختصارات من أجل ارتباطات التعددية المتداخلة. مثلًا، في حال كان لمستند معين أكثر من قسم، ولكل قسم أكثر من فقرة، قد تحتاج أحيانًا إلى الحصول على مجموعة تحوي جميع الفقرات في مستند معيّن. يمكنك تهيئة هذا كالتالي:
class Document < ApplicationRecord
  has_many :sections
  has_many :paragraphs, through: :sections
end
 
class Section < ApplicationRecord
  belongs_to :document
  has_many :paragraphs
end
 
class Paragraph < ApplicationRecord
  belongs_to :section
end
مع تعيين الخيار through: :sections، سيفهم ريلز الارتباط ويمكن بعد ذلك القيام بالتالي:
@document.paragraphs

ارتباط الفردية الضمني (has_one :through)

يهيّء ارتباط الفردية الضمني علاقة واحد إلى واحد مع نموذج آخر. يحدّد هذا الارتباط أن كل سجل من النموذج المعرّف يمكن مطابقته مع سجل واحد من نموذج آخر عن طريق (through) ضمن نموذج ثالث. مثلًا، في حال كان لكل مزوّد حسابًا واحدًا، وكل حساب مرتبط مع تاريخ واحد، فقد يبدو نموذج المزوّد كالتالي:
class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account
end
 
class Account < ApplicationRecord
  belongs_to :supplier
  has_one :account_history
end
 
class AccountHistory < ApplicationRecord
  belongs_to :account
end
إنشاء ارتباط الفردية الضمني بين نموذج المزودين ونموذج تاريخ الحساب عبر نموذج الحسابات.
إنشاء ارتباط الفردية الضمني بين نموذج المزودين ونموذج تاريخ الحساب عبر نموذج الحسابات.
قد يبدو التهجير الموافق كالتالي:
class CreateAccountHistories < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps
    end
 
    create_table :account_histories do |t|
      t.belongs_to :account, index: true
      t.integer :credit_rating
      t.timestamps
    end
  end
end

ارتباط الانتماء والتعددية (has_and_belongs_to_many)

ينشئ ارتباط الانتماء والتعددية علاقة كثير إلى كثير مباشرةً مع نموذج آخر، دون نموذج وسيطي. مثلًا، في حال احتاج تطبيقك إلى تضمين التجميعات (assemblies) والقطع (parts)، ولكل تجميع أكثر من قطعة وكل قطعة تظهر في أكثر من تجميع، فيمكنك تعريف نموذج كالتالي:
class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end
 
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
إنشاء ارتباط الانتماء والتعددية بين ثلاثة نماذج.
إنشاء ارتباط الانتماء والتعددية بين ثلاثة نماذج.
قد يبدو التهجير الموافق كالتالي:
class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
  def change
    create_table :assemblies do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :parts do |t|
      t.string :part_number
      t.timestamps
    end
 
    create_table :assemblies_parts, id: false do |t|
      t.belongs_to :assembly, index: true
      t.belongs_to :part, index: true
    end
  end
end

الاختيار بين ارتباط الانتماء وارتباط الفردية

في حال أردت تعيين علاقة واحد إلى واحد بين نموذجين، فيجب أن تضيف ارتباط الانتماء إلى نموذج، وارتباط الفردية إلى نموذج آخر ولكن كيف يمكن معرفة أين أضيف كلا هذين الارتباطين؟

يحدّد الاختلاف في مكان توضّع المفتاح الأجنبي (إذ يظهر في الجدول الذي يعرّف نموذجه ارتباط الانتماء)، لكن يجب أن تضيف بعض المنطق إلى المعنى الحقيقي للبيانات أيضًا. تقول علاقة الفردية (has_one) أن شيئًا من شيء هو ملكك؛ أي أنّ هذا الشيء يشير إليك. مثلًا، قد يبدو الأمر أكثر منطقية فيما لو قلنا أنّ المزوّد يملك حسابًا ممّا لو قلنا أن الحساب يملك مزوّدًا. وبناءً على ذلك، يفضّل تعريف العلاقات كالتالي:
class Supplier < ApplicationRecord
  has_one :account
end
 
class Account < ApplicationRecord
  belongs_to :supplier
end
وقد يبدو التهجير الموافق كالتالي:
class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :accounts do |t|
      t.integer :supplier_id
      t.string  :account_number
      t.timestamps
    end
 
    add_index :accounts, :supplier_id
  end
end
ملاحظة: إن استخدام t.integer :supplier_id يجعل تسمية حقل المفتاح الأجنبي واضحة وظاهرة. في الإصدارات الحالية من ريلز، يمكن تجريد هذا التفصيل التنفيذي من خلال استخدام t.references :supplier بدلًا من ذلك.

الاختيار بين ارتباط التعددية الضمني وارتباط الانتماء والتعددية

يزوّدك ريلز بطريقتين مختلفتين من أجل التعريف عن علاقات الكثير إلى كثير بين النماذج. الطريقة الأبسط هي باستخدام ارتباط الانتماء والتعددية، الذي يمكّنك من تعريف العلاقة مباشرةً:
class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end
 
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
الطريقة الأخرى هي من خلال تعريف ارتباط التعددية الضمني (has_many :through). يقوم ذلك بإنشاء الارتباط بشكل ضمني وغير مباشر، وذلك من خلال نموذج وسيطي أو رابط:
class Assembly < ApplicationRecord
  has_many :manifests
  has_many :parts, through: :manifests
end
 
class Manifest < ApplicationRecord
  belongs_to :assembly
  belongs_to :part
end
 
class Part < ApplicationRecord
  has_many :manifests
  has_many :assemblies, through: :manifests
end
القاعدة الأساسية في الاختيار بين الطريقتين هي أنّه يجب استخدام ارتباط التعددية الضمني في حال أردت العمل مع نموذج العلاقة الوسيطي ككيان مستقل. وفي حال لم ترد أي شيء من النموذج الوسيطي، الأسهل أن نقوم بتعريف ارتباط الانتماء والتعددية (إلّا أنّك بحاجة لإنشاء الجدول الوسيطي في قاعدة البيانات).

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

الارتباطات متعددة الأشكال (Polymorphic Associations)

يعد الارتباط متعدد الأشكال نمطًا متقدّمًا من الارتباطات. باستخدام هذا النوع من الارتباطات، يمكن لنموذج ما أن ينتمي لأكثر من نموذج آخر على ارتباط وحيد. مثلًا، قد تملك نموذج يمثّل صورة ما ينتمي بدوره إمّا إلى نموذج موظّف أو نموذج منتج. إليك طريقة تعريف هذا الارتباط:
class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end
يمكنك أن تفكّر بالارتباط الانتماء متعدد الأشكال كطريقة لتعريف واجهة يمكن لأي نموذج استخدامها. من سجل من نموذج الموظف، يمكنك تحصيل مجموعة الصور المرتبطة بالشكل ‎@employee.pictures. بالمثل، يمكنك استرجاع الصور بالشكل ‎@product.Pictures. في حال كان لديك سجلًا من نموذج الصورة، يمكنك الحصول على الأب لهذا السجل عن طريق picture.imageable@. للحصول على هذا، يجب أن تعرف حقل مفتاح أجنبي وحقل نوع في النموذج المعرّف للارتباط متعدد الأشكال:
class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.integer :imageable_id
      t.string  :imageable_type
      t.timestamps
    end
 
    add_index :pictures, [:imageable_type, :imageable_id]
  end
end
يمكن تبسيط التهجير باستخدام t.reference ليبدو كالتالي:
class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, polymorphic: true, index: true
      t.timestamps
    end
  end
end
مثال عن ارتباط متعدد الأشكال.
مثال عن ارتباط متعدد الأشكال.

الروابط الذاتية

عند تصميم نماذج البيانات، قد تحتاج أحيانًا إلى نموذج يملك ارتباطًا بنفسه. مثلًا، قد تحتاج إلى تخزين كل الموظفين في نموذج بيانات وحيد، لكن تحتاج أيضًا إلى إقامة علاقة بين المدير وموظّفيه التابعين. يمكن نمذجة هذه الحالة باستخدام الارتباطات الذاتية (self-joining associations):
class Employee < ApplicationRecord
  has_many :subordinates, class_name: "Employee",
                          foreign_key: "manager_id"
 
  belongs_to :manager, class_name: "Employee"
end
من خلال هذه التهيئة، يمكنك استرجاع employee.subordinates@ و employee.manager@. في التهجير، يجب أن تضيف حقلًا مرجعيًّا للنموذج نفسه.
class CreateEmployees < ActiveRecord::Migration[5.0]
  def change
    create_table :employees do |t|
      t.references :manager, index: true
      t.timestamps
    end
  end
end

نصائح وحيل وتحذيرات

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

  • التحكم بالذاكرة المؤقتة (caching).
  • تجنّب تصادم الأسماء.
  • تحديث مخطط البيانات.
  • التحكم بمجال الارتباط.
  • الارتباطات ثنائية الاتجاه.

التحكم بالذاكرة المؤقتة (caching)

تُبنى جميع توابع العلاقات على الذاكرة المؤقتة، التي تبقي على نتائج أجدد استعلام متاح من أجل المزيد من العمليات. تُشارك الذاكرة المؤقتة أيضًا خلال التوابع. مثلًا:
author.books          	# يعيد كتبًامن قاعدة البيانات
author.books.size     	# يستخدم النسخة المخزَّنة في الذاكرة الموقتة للكتب
author.books.empty?    	# يستخدم النسخة المخزَّنة في الذاكرة الموقتة للكتب
لكن ماذا لو أردت إعادة تحميل الذاكرة المؤقتة، وذلك بسبب تغيير بعض أجزاء البيانات من قبل قسم آخر في التطبيق؟ فقط استدعِ التابع reload على الارتباط:
author.books            	# يعيد كتبًا من قاعدة البيانات
author.books.size        	# يستخدم نسخة الكتب المخزنة في الذاكرة
author.books.reload.empty?   # يهمل نسخة الكتب المخزنة في الذاكرة المؤقتة
                             # ويعود لقاعدة البيانات

تجنّب تضارب الأسماء

لست حرًا بتعريف أي اسم من أجل علاقاتك. لكون إنشاء الارتباط يضيف تابعًا بنفس الاسم إلى نموذجك، ليس من المحبّذ إنشاء ارتباط باسم مشابه لاسم تابع مثل موجود في الصنف الأب ActiveRecord::Base؛ إذ يتم في هذه الحالة إعادة تعريف التابع الأب وقد لا تعمل بعض الميزات بناءً على المشاكل التي سببها تضارب الأسماء. مثلًا، الاسمان attributes أو connection هما اسمان غير مفضّلين للارتباطات.

تحديث مخطط البيانات

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

إنشاء مفاتيح أجنبية من أجل ارتباطات الانتماء

عند تعريف ارتباط الانتماء، يجب أن تنشئ حقل مفتاح أجنبي مناسب. مثلًا، لنفرض النموذج التالي:
class Book < ApplicationRecord
  belongs_to :author
end
يجب أن يرافق هذا التعريف حقل مفتاح أجنبي معرّف على جدول الكتب. من أجل جدول جديد كليًا، قد يبدو التهجير كالتالي:
class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.datetime   :published_at
      t.string     :book_number
      t.references :author
    end
  end
end
في حين أنّه من أجل جدول موجود مسبقًا، قد يبدو كالتالي:
class AddAuthorToBooks < ActiveRecord::Migration[5.0]
  def change
    add_reference :books, :author
  end
end
ملاحظة: في حال أردت تحقيق السلامة المرجعية في مستوى قاعدة البيانات، أضف الخيار foreign_key: true إلى تعريف حقل المرجع reference أعلاه.

إنشاء جداول وسيطية من أجل ارتباطات الانتماء والتعددية

في حال أنشأت ارتباط انتماء وتعددية، يجب أن تقوم بشكل ظاهري بإنشاء الجدول الوسيطي. في حال لم يتم تعريف اسم الجدول الوسيطي بشكل ظاهري عن طريق الخيار join_table:، يقوم Active Record بإنشاء اسم الجدول الوسيطي عن طريق الترتيب الأبجدي لأسماء الأصناف. لذا، من أجل ارتباط بين الكتب والكتّاب، سيكون اسم الجدول الوسيطي "authors_books" لأن الحرف a يسبق الحرف b بالترتيب الأبجدي.

تحذير: إن الأفضلية بين أسماء النماذج تحسب باستخدام المعامل <=> الخاصة بالسلاس النصية. هذا يعني أنّه إذا كانت أطوال السلاسل النصية مختلفة، وكانت السلاسل النصية متساوية عند مقارنتها إلى أقل طول، ففي هذه الحالة تكون السلسلة النصية ذات عدد المحارف الأكبر تملك أفضلية أبجدية أعلى من السلسلة النصية ذات عدد المحارف الأقل. مثلًا، قد يُعتقد أن اسمي الجدولين paper_boxes و papers سيولّد اسم الجدول الوسيطي "papers_paper_boxes" بسبب طول الاسم paper_boxes، لكن في الحقيقة، سيتم توليد اسم الجدول الوسيطي paper_boxes_papers (بسبب أنّ الشرطة السفلية "_" هي ذات ترتيب أبجدي أقل من الحرف s في الترميزات الشائعة).

مهما كان الاسم، يجب أن تقوم بتوليد اسم الجدول الوسيطي باستخدام التهجير الموافق. مثلًا، لنفرض هذه الارتباطات:
class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end
 
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
يجب أن يوافقها تهجيرًا لإنشاء الجدول assembles_parts. يجب أن يُنشَأ هذا الجدول دون حقل مفتاح رئيسي:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
  def change
    create_table :assemblies_parts, id: false do |t|
      t.integer :assembly_id
      t.integer :part_id
    end
 
    add_index :assemblies_parts, :assembly_id
    add_index :assemblies_parts, :part_id
  end
end
نمرّر الخيار id: false إلى التابع create_table لأن الجدول لا يمثل نموذجًا. إن هذا السلوك مطلوب من أجل عمل الارتباط بشكل صحيح. في حال لاحظت سلوكًا غريبًا في أي ارتباط انتماء وتعددية مثل معرِّفات النماذج الفاسدة، أو الاستثناءات حول المعرِّفات المتضاربة، فمن المتوقع أن تكون نسيت هذا الخيار. يمكنك أيضًا استخدام التابع create_join_table.
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
  def change
    create_join_table :assemblies, :parts do |t|
      t.index :assembly_id
      t.index :part_id
    end
  end
end

التحكم بمجال الارتباط

افتراضيًا، تبحث الارتباطات عن الكائنات في مجال الوحدة (module) الحالية. يكون هذا مهمًا عند تعريف نماذج Active Record في وحدة. مثلًا:
module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end
 
    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end
سيعمل هذا جيّدًا، لأن كلا الصنفين Supplier و Account مكتوبين في المجال نفسه. لكن التنفيذ التالي لن يعمل لأن الصنفين معرّفان في مجالين مختلفين:
module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end
  end
 
  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end
لتضمين نموذجين من مجالات أسماء مختلفة، يجب أن تحدد الاسم الكامل للصنف في تعريف الارتباط:
module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account,
        class_name: "MyApplication::Billing::Account"
    end
  end
 
  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier,
        class_name: "MyApplication::Business::Supplier"
    end
  end
end

الارتباطات ثنائية الاتجاه

من الطبيعي على الارتباطات أن تعمل باتجاهين، مما يتطلّب من التعريف أن يتم على نموذجين مختلفين:
class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :author
end
يحاول Active Record أن يتعرف تلقائيًا على أن هذين النموذجين يشاركان ارتباطًا ثنائي الاتجاه (bi-directional association) بناءً على اسم الارتباط. في هذه الطريقة، يحمّل Active Record فقط نسخة واحدة من الكائن Author، مما يجعل تطبيقك أكثر فعالية ويضمن تناسق البيانات:
a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
a.first_name = 'David'
a.first_name == b.author.first_name # => true
يدعم Active Record التعرف التلقائي على معظم أنواع الارتباطات بأسماء معيارية. لكن، لن يتعرف Active Record على ارتباط ثنائي الاتجاه في حال احتوى على مجال أو أي من الخيارات التالية:
  • :‎through
  • :‎foreign_key
مثلًا، لنفرض تعريف النموذج التالي:
class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
لن يتعرف Active Record تلقائيًا على الارتباطات ثنائية الاتجاه:
a = Author.first
b = a.books.first
a.first_name == b.writer.first_name # => true
a.first_name = 'David'
a.first_name == b.writer.first_name # => false
يزوّد Active Record بالخيار inverse_of:‎ حتى تتمكّن من تعريف الارتباطات ثنائية الاتجاه بشكل ظاهري:
class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
بتضمين هذا الخيار في تعريف ارتباط التعددية (has_many)، يتعرّف Active Record الآن على الارتباط ثنائي الاتجاه:
a = Author.first
b = a.books.first
a.first_name == b.writer.first_name # => true
a.first_name = 'David'
a.first_name == b.writer.first_name # => true

مرجع مفصّل عن الارتباطات

إن القسم التالي يعطي تفاصيل أكثر عن كل نوع من أنواع الارتباطات، بما فيها التوابع التي تضيفها والخيارات التي يمكنك استخدامها عند تعريف الارتباط.

مرجع ارتباط الانتماء (belongs_to)

ينشئ ارتباط الانتماء علاقة واحد إلى واحد بين نموذجين. بمصطلحات قواعد البيانات، يقول هذا الارتباط أن الصنف الحالي يملك المفتاح الأجنبي. في حال كان الصنف الآخر يملك المفتاح الأجنبي، فيجب عليك استخدام ارتباط الفردية has_one بدلًا من ذلك.

التوابع المضافة من ارتباط الانتماء

عند تعريف ارتباط الانتماء، يحصل الصنف المعرّف للارتباط تلقائيًا على 6 توابع متعلقة بالارتباط هي:

  • association
  • association=(associate)‎
  • build_association(attributes = {})‎
  • create_association(attributes = {})‎
  • create_association!(attributes = {})‎
  • reload_association
في كل هذه الارتباطات، تبدّل الكلمة association بالرمز الممرر كوسيط أول للتابع belongs_to. مثلًا، بفرض التعريف التالي:
class Book < ApplicationRecord
  belongs_to :author
end
كل كائن من النموذج Book سيملك هذه التوابع:
author
author=
build_author
create_author
create_author!
reload_author
ملاحظة: عند تعريف ارتباط فردية أو ارتباط انتماء جديدين، يجب أن تستخدم السابقة _build لبناء الارتباط، بدلًا من التابع association.build الذي يتم استخدامه لارتباط التعددية أو ارتباط الانتماء والتعددية. لإنشاء واحد، استخدم السابقة _create.
association
يعيد التابع association الكائن المرتبط، في حال وجوده. في حال لم يكن هناك كائن مرتبط، ستعاد القيمة nil.
@author = @book.author
في حال تمّت إعادة الكائن المرتبط مسبقًا من قاعدة البيانات، ستتم إعادة الإصدار المخزن في الذاكرة المؤقتة. لتجنب هذا السلوك (وفرض إعادة قراءة البيانات الموجودة في قاعدة البيانات)، استدعِ التابع reload_association. على الكائن الأب.
@author = @book.reload_author
(association=(associate
يسند التابع =association الكائن المرتبط إلى الكائن المعطى. وراء الكواليس، يعني هذا قراءة المفتاح الرئيسي من الكائن المسند وتحديد المفتاح الأجنبي للكائن الأساسي للقيمة نفسها.
@book.author = @author
({} = build_association(attributes
يعيد التابع build_association كائنًا جديدًا من نوع الارتباط. يتم تهيئة هذا الكائن من الحقول الممررة، وسيتم إنشاء الرابط من خلال المفتاح الأجنبي، لكن لن يتم حفظ الكائن المرتبط.
@author = @book.build_author(author_number: 123,
                                  author_name: "John Doe")
create_association(attributes = {})‎
يعيد التابع create_association كائنًا جديدًا من نوع الارتباط (associated type). يتم تهيئة هذا الكائن من الحقول الممررة، وسيتم إنشاء الرابط من خلال المفتاح الأجنبي، وبعد مرور الكائن بكل التأكيدات المحددة للنموذج، سيتم حفظ الكائن في قاعدة البيانات.
@author = @book.create_author(author_number: 123,
                                   author_name: "John Doe")
({} = create_association!(attributes

يفعل ما يفعله التابع create_attributes، لكن يرمي استثناءً من النوع ActiveRecord::RecordInvalid في حال كان السجل غير صحيح.

خيارات ارتباط الانتماء (belongs_to)

في حين أن ريلز يستخدم الخيارات الافتراضية التي تعمل جيّدًا في معظم الحالات، قد تحتاج في بعض الأوقات إلى تعديل السلوك الافتراضي للتابع belongs_to. يمكن تحقيق هذه التعديلات بواسطة تمرير خيارات وهياكل مجالية عند إنشاء الارتباط. مثلًا، الارتباط التالي يستخدم خيارين:
class Book < ApplicationRecord
  belongs_to :author, dependent: :destroy,
    counter_cache: true
end
يدعم ارتباط الانتماء الخيارات التالية:
  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :foreign_key
  • :primary_key
  • :inverse_of
  • :polymorphic
  • :touch
  • :validate
  • :optional
الخيار autosave:‎

في حال حددت الخيار autosave:‎ إلى true، يقوم ريلز بحفظ أي كائنات مرتبطة ويدمر أي كائنات محددة للتدمير عند حفظ الكائن الأب. تحديد الخيار autosave: إلى false لا يساوي عدم تحديده إطلاقًا، ففي حال لم يتم تحديده فسيتم حفظ الكائنات المرتبطة الجديدة، لكن لن يتم حفظ الكائنات المرتبطة المحدثة.

الخيار class_name:‎
في حال عدم إمكانية اشتقاق اسم النموذج الآخر من اسم الارتباط، يمكنك استخدام الخيار class_name:‎ لتحديد اسم النموذج الآخر. مثلًا، في حال انتمى الكتاب إلى أكثر من كاتب، لكن الاسم الحقيقي للنموذج الممثل للكائنات هو Patron، ستعرّف الارتباط كالتالي:
class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron"
end
الخيار counter_cache:‎
يمكن استخدام هذا الخيار لجعل عملية إيجاد عدد الكائنات المرتبطة أكثر فعالية. لنفرض هذه النماذج:
class Book < ApplicationRecord
  belongs_to :author
end
class Author < ApplicationRecord
  has_many :books
end
مع هذه التعريفات، عند طلب القيمة author.book.size@، سيتم تنفيذ الاستعلام (*)COUNT في قاعدة البيانات. لتجنب هذا الاستدعاء، يمكنك استخدام عداد مؤقت (counter cache) للنموذج المالك للارتباط:
class Book < ApplicationRecord
  belongs_to :author, counter_cache: true
end
class Author < ApplicationRecord
  has_many :books
end
بواسطة هذا التعريف، يقوم ريلز بحفظ عدد الكائنات المرتبطة بشكل مؤقت، وإعادتها في حال استدعاء التابع size.

بالرغم من أن الخيار counter_cache:‎ معرَّف على النموذج الذي يضم ارتباط الانتماء، إلى أنَّ الحقل الحقيقي يجب أن يضاف إلى النموذج المرتبط (المعرف لارتباط التعددية). في الحالة أعلاه، ستحتاج إلى إضافة الحقل المسمى books_count إلى النموذج Author.

يمكنك استبدال الاسم الافتراضي للحقل عن طريق تحديد اسم مخصص للخيار counter_cache بدلًا من true. مثلًا، لاستخدام الاسم counter_of_books بدلًا من books_count، جرب الشيفرة التالية:
class Book < ApplicationRecord
  belongs_to :author, counter_cache: :count_of_books
end
class Author < ApplicationRecord
  has_many :books
end
ملاحظة: تحتاج إلى تضمين الخيار counter_cache:‎ فقط على طرف الارتباط الذي يحوي ارتباط الانتماء.

يتم إضافة حقول العدادات المؤقتة إلى قائمة خاصيات القراءة فقط (read-only attributes) الخاصة بالنموذج الحاوي (containing model) عن طريق التابع attr_readonly.

الخيار dependent:‎

في حال عيّنت الخيار dependent:‎ إلى:

  • القيمة destroy:، فسيتم استدعاء التابع destroy على كل الكائنات المرتبطة عند تدمير الكائن.
  • القيمة delete:، فجميع الكائنات المرتبطة يتم حذفها من قاعدة البيانات دون استدعاء التابع destory الخاص بها عند تدمير الكائن.

تحذير: يجب ألّا تحدد هذا الخيار على ارتباط الانتماء (belongs_to) المتعلق بارتباط تعددية (has_many) في الصنف الآخر، إذ يؤدي هذا إلى وجود سجلات يتيمة (orphaned records) في قاعدة بياناتك.

الخيار foreign_key:‎
كعرف، يعتقد ريلز أن اسم الحقل المستخدم للمفتاح الأجنبي على النموذج المعرف هو اسم الارتباط مع اللاحقة id_. يسمح الخيار foreign_key: بتحديد اسم حقل المفتاح الأجنبي مباشرةً:
class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron",
                        foreign_key: "patron_id"
end
ملاحظة: في أي حالة، لن ينشئ ريلز المفاتيح الأجنبية تلقائيًا. يجب عليك تعريفها كجزء من تهجيراتك.
الخيار primary_key:‎

كعرف، يعتقد ريلز أن الحقل المسمى id يستخدم لحفظ المفتاح الرئيسي لجداوله. يمكّنك الخيار primary_key:‎ من تحديد اسم مختلف لحقل المفتاح الرئيسي.

مثلًا، من أجل الجدول المسمى users بحقل مسمى guid كفتاح رئيسي؛ إذا أردنا فصل الجدول todos بحيث يحوي حقل مفتاح أجنبي مسمى user_id يأخذ قيمه من الحقل guid، يمكننا تحقيق ذلك كالتالي:
class User < ApplicationRecord
  self.primary_key = 'guid' # primary key is guid and not id
end
 
class Todo < ApplicationRecord
  belongs_to :user, primary_key: 'guid'
end
عند تشغيل user.todos.create@، سيأخذ سجل todo@ قيمة الحقل user_id من الحقل guid الخاص بالسجل user@.
الخيار inverse_of:‎
يحدد الخيار inverse_of اسم ارتباط التعددية أو الفردية الموجود في عكس هذا الارتباط.
class Author < ApplicationRecord
  has_many :books, inverse_of: :author
end
 
class Book < ApplicationRecord
  belongs_to :author, inverse_of: :books
end
الخيار polymorphic:‎

إن تمرير القيمة true للخيار polymorphic:‎ يحدد أن هذا الارتباط هو ارتباط متعدد الأشكال. تمّ الحديث عن الارتباطات متعددة الأشكال سابقًا في هذا التوثيق.

الخيار touch:‎
إن تمرير القيمة true للخيار touch:‎ سيحدِّث الحقول updated_at أو update_on للكائن المرتبط بناءً على تاريخ حفظ أو تدمير الكائن الأب:
class Book < ApplicationRecord
  belongs_to :author, touch: true
end
 
class Author < ApplicationRecord
  has_many :books
end
في أي حالة، سيؤدي حفظ أو تدمير كائن من النوع Book (كتاب) إلى تحديث التاريخ على الكاتب المرتبط به. يمكنك أيضًا تحديد حقل التاريخ الواجب تحديثه:
class Book < ApplicationRecord
  belongs_to :author, touch: :books_updated_at
end
الخيار validate:‎

إن تمرير القيمة true للخيار validate:‎ سيؤدي إلى تأكيد الكائنات المرتبطة عند حفظ الكائن الأب. افتراضيًا، تكون قيمة هذا الخيار false، مما يعني أنه لن يتم تأكيد الكائنات المرتبطة عند تحديث الكائنات الآباء.

الخيار optional:‎

إن تمرير القيمة true للخيار optional:‎ يعني أن وجود الكائن المرتبط ليس ضروريًا ولن يتم تأكيده. افترضيًا، تكون قيمة هذا الخيار false.

مجالات ارتباط الانتماء

هناك بعض الأوقات التي تحتاج فيها إلى تعديل الاستعلام المستخدم في ارتباط الانتماء. يمكن تحقيق هذا التعديل عن طريق نطاق كتلة (scope block). مثلًا:
class Book < ApplicationRecord
  belongs_to :author, -> { where active: true },
                        dependent: :destroy
end
يمكنك استخدام أي من توابع الاستعلام المعيارية داخل نطاق الكتلة. سيتم مناقشة التوابع التالية أدناه:
  • where
  • includes
  • readonly
  • select
التابع where
يمكّنك التابع where من تحديد الشروط التي يجب على الكائن المرتبط مراعاتها.
class Book < ApplicationRecord
  belongs_to :author, -> { where active: true }
end
التابع includes
يمكنك استخدام هذا التابع لتحديد ترتيبٍ ثانٍ للارتباطات (second-order associations) التي يجب أن يتم تحميلها بشكل حثيث (Eager Loading) عند استخدام هذا الارتباط. مثلًا، لنفرض هذه النماذج:
class LineItem < ApplicationRecord
  belongs_to :book
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :line_items
end
 
class Author < ApplicationRecord
  has_many :books
end
إذا كنت تسترجع الكتّاب بشكل متكرر مباشرةً من النموذج LineItem (أي line_item.book.author@)، يمكنك جعل تحميل الارتباطات أكثر فعالية عن طريق تضمين ارتباط الكتاب من النموذج LineItem إلى نموذج الكتاب Book:
class LineItem < ApplicationRecord
  belongs_to :book, -> { includes :author }
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :line_items
end
 
class Author < ApplicationRecord
  has_many :books
end
ملاحظة: ليس من الضروري استخدام التابع includes من أجل الارتباطات المباشرة - أي، في حال كانت لديك علاقة انتماء من نموذج الكتاب Book إلى نموذج الكاتب Author، فسيتم تحميل الكتّاب بشكل حثيث افتراضيًا عند الحاجة إليها.
التابع readonly

عند استخدام التابع readonly، ستكون الكائنات المرتبطة قابلة للقراءة فقط عند تحميلها من الارتباط.

التابع select

يمكّنك التابع select من تجاوز الاستعلام SELECT الافتراضي المستخدم لتحميل البيانات من أجل الكائن المرتبط. افتراضيًا، يحمل ريلز جميع حقول الكائنات المرتبطة.

ملاحظة: في حال استخدامك للتابع select على ارتباط الانتماء، فيجب أن تحدد الخيار foreign_key: أيضًا لضمان صحة النتائج.

هل الكائنات المرتبطة موجودة؟

يمكنك التحقق من وجود الكائنات المرتبطة عن طريق استخدام التابع ?association.nil:
if @book.author.nil?
  @msg = "No author found for this book"
end

متى يتم حفظ الكائنات؟

إن إسناد كائن ما إلى ارتباط الانتماء لا يحفظ هذا الكائن تلقائيًا، ولا يحفظ الكائن المرتبط أيضًا.

مرجع ارتباط الفردية (has_one)

ينشئ ارتباط الانتماء علاقة واحد إلى واحد بين نموذجين. بمصطلحات قواعد البيانات، يقول هذا الارتباط أن الصنف الآخر يملك المفتاح الأجنبي. في حال كان الصنف الحالي يملك المفتاح الأجنبي، يجب عليك استخدام ارتباط الانتماء (belongs_to) بدلًا من ذلك.

التوابع المضافة من ارتباط الفردية

عند تعريف ارتباط الفردية، يحصل الصنف المعرّف للارتباط تلقائيًا على 6 توابع متعلقة بالارتباط:

  • association
  • association=(associate)‎
  • build_association(attributes = {})‎
  • create_association(attributes = {})‎
  • create_association!(attributes = {})‎
  • reload_association
في كل هذه الارتباطات، تبدّل الكلمة association بالرمز الممرر كوسيط أول للتابع has_one. مثلًا، بفرض التعريف التالي:
class Supplier < ApplicationRecord
  has_one :account
end
كل كائن من نموذج الكتاب Book سيملك هذه التوابع:
account
account=
build_account
create_account
create_account!
reload_account
ملاحظة: عند تعريف ارتباط فردية أو ارتباط انتماء جديدين، يجب أن تستخدم السابقة _build لبناء الارتباط، بدلًا من التابع association.build الذي يتم استخدامه لارتباط التعددية أو ارتباط الانتماء والتعددية. لإنشاء واحد، استخدم السابقة _create.
association
يعيد التابع association الكائن المرتبط، في حال وجوده. في حال لم يكن هناك كائن مرتبط، ستعاد القيمة nil.
@account = @supplier.account
في حال تمّت إعادة الكائن المرتبط مسبقًا من قاعدة البيانات، ستتم إعادة الإصدار المخزن في الذاكرة المؤقتة. لتجنب هذا السلوك (وفرض إعادة قراءة البيانات الموجودة في قاعدة البيانات)، استدعِ التابع reload_association. على الكائن الأب.
@account = @supplier.reload_account
(association=(associate
يسند التابع =association الكائن المرتبط إلى الكائن المعطى. وراء الكواليس، يعني هذا قراءة المفتاح الرئيسي من الكائن الحالي وتحديد المفتاح الأجنبي للكائن المسند للقيمة نفسها.
@supplier.account = @account
({} = build_association(attributes
يعيد التابع build_association كائنًا جديدًا من نوع الارتباط. يتم تهيئة هذا الكائن من الحقول الممررة، وسيتم إنشاء الرابط من خلال المفتاح الأجنبي، لكن لن يتم حفظ الكائن المرتبط.
@account = @supplier.build_account(terms: "Net 30")
({} = create_association(attributes
يعيد التابع create_association كائنًا جديدًا من نوع الارتباط. يتم تهيئة هذا الكائن من الحقول الممررة، وسيتم إنشاء الرابط من خلال المفتاح الأجنبي، وبعد مرور الكائن بكل التأكيدات المحددة للنموذج، سيتم حفظ الكائن في قاعدة البيانات.
@account = @supplier.create_account(terms: "Net 30")
({} = create_association!(attributes

يفعل ما يفعله التابع create_attributes، لكن يرمي استثناءً من النوع ActiveRecord::RecordInvalid في حال كان السجل غير صحيح.

خيارات ارتباط الفردية (has_one)

في حين أن ريلز يستخدم الخيارات الافتراضية التي تعمل جيّدًا في معظم الحالات، قد تحتاج في بعض الأوقات إلى تعديل اسلوك الافتراضي للتابع has_one. يمكن تحقيق هذه التعديلات بواسطة تمرير خيارات عند إنشاء الارتباط. مثلًا، الارتباط التالي يستخدم خيارين:
class Supplier < ApplicationRecord
  has_one :account, class_name: "Billing", dependent: :nullify
end
يدعم ارتباط الفردية الخيارات التالية:
  • :as
  • :autosave
  • :class_name
  • :dependent
  • :foreign_key
  • :inverse_of
  • :primary_key
  • :source
  • :source_type
  • :through
  • :validate
الخيار as:‎

إن استخدام الخيار as:‎ يحدد أن هذا الارتباط هو ارتباط متعدد الأشكال.

الخيار autosave:‎

في حال حددت الخيار autosave:‎ إلى true، يقوم ريلز بحفظ أي كائنات مرتبطة ويدمر أي كائنات محددة للتدمير عند حفظ الكائن الأب. تحديد الخيار autosave: إلى false لا يساوي عدم تحديده إطلاقًا، ففي حال لم يتم تحديده فسيتم حفظ الكائنات المرتبطة الجديدة، لكن لن يتم حفظ الكائنات المرتبطة المحدثة.

الخيار class_name:‎
في حال عدم إمكانية اشتقاق اسم النموذج الآخر من اسم الارتباط، يمكنك استخدام الخيار class_name:‎ لتحديد اسم النموذج الآخر. مثلًا، في حال امتلك المزوّد حسابًا، لكن الاسم الحقيقي للنموذج الممثل للحسابات هو Billing، ستعرّف الارتباط كالتالي:
class Supplier < ApplicationRecord
  has_one :account, class_name: "Billing"
end
الخيار dependent:

يتحكّم بالسلوك المطبق على الكائن المرتبط عند تدمير الكائن الأب:

  • القيمة destroy:، سيتم استدعاء التابع destroy على كل الكائنات المرتبطة في حال تدمير الكائن.
  • القيمة delete:، جميع الكائنات المرتبطة يتم حذفها من قاعدة البيانات دون استدعاء توابع رد النداء الخاصة بها عند تدمير الكائن.
  • القيمة nullify:، والتي تجعل قيمة حقل المفتاح الأجنبي NULL. لن يتم تشغيل توابع رد النداء آنذاك.
  • القيمة restrict_with_exception:، ترمي استثناءً عند وجود كائن مرتبط.
  • القيمة restrict_with_error:، تضيف خطأ إلى الأب في حال وجود كائن مرتبط.

من الضروري عدم تحديد أو ترك القيمة nullify: للارتباطات التي تملك قيود NOT NULL في قاعدة البيانات. في حال عدم تحديدك للخيار dependent لتدمير هذه الارتباطات، لن تتمكن من تغيير الكائنات المرتبطة لأن أول مفتاح أجنبي للكائن المرتبط سيتم تحديده للقيمة NULL غير المسموحة.

الخيار foreign_key:‎
كعرف، يعتقد ريلز أن اسم الحقل المستخدم للمفتاح الأجنبي على النموذج المعرف هو اسم الارتباط مع اللاحقة id_. يسمح الخيار foreign_key:‎ بتحديد اسم حقل المفتاح الأجنبي مباشرةً:
class Supplier < ApplicationRecord
  has_one :account, foreign_key: "supp_id"
end
ملاحظة: في أي حالة، لن ينشئ ريلز المفاتيح الأجنبية تلقائيًا. يجب عليك تعريفها كجزء من تهجيراتك.
الخيار inverse_of:‎
يحدد الخيار :inverse_of اسم ارتباط التعددية أو الفردية الموجود في عكس هذا الارتباط.
class Supplier < ApplicationRecord
  has_one :account, inverse_of: :supplier
end
 
class Account < ApplicationRecord
  belongs_to :supplier, inverse_of: :account
end
الخيار primary_key:‎

كعرف، يعتقد ريلز أن الحقل المسمى id يستخدم لحفظ المفتاح الرئيسي لجداوله. يمكّنك الخيار primary_key:‎ من تحديد اسم مختلف لحقل المفتاح الرئيسي.

الخيار source:‎

يحدد الخيار source:‎ مصدر اسم الارتباط من أجل ارتباط الفردية الضمني (has_one :through).

الخيار source_type:‎

يحدد الخيار source_type:‎ مصدر نوع الارتباط من أجل ارتباط الفردية الضمني الذي يعبر ارتباط متعدد الأشكال.

الخيار through:‎

يحدد الخيار through:‎ نموذج وسيطي لتنفيذ الاستعلامات عليه. تمّت مناقشة ارتباطات الفردية الضمنية سابقًا في هذا التوثيق.

الخيار validate:‎

إن تمرير القيمة true للخيار validate:‎ سيؤدي إلى التحقق من صحة الكائنات المرتبطة عند حفظ الكائن الأب. افتراضيًا، تكون قيمة هذا الخيار false، مما يعني أنه لن يتم التحقق من صحة الكائنات المرتبطة عند تحديث الكائنات الآباء.

مجالات ارتباط الانتماء

هناك بعض الأوقات التي تحتاج فيها إلى تعديل الاستعلام المستخدم في ارتباط الفردية. يمكن تحقيق هذا التعديل عن طريق مجال كتلة (scope block). مثلًا:
class Supplier < ApplicationRecord
  has_one :account, -> { where active: true }
end
يمكنك استخدام أي من توابع الاستعلام المعيارية داخل مجال الكتلة. سيتم مناقشة التوابع التالية أدناه:
  • where
  • includes
  • readonly
  • select
التابع where
يمكّنك التابع where من تحديد الشروط التي يجب على الكائن المرتبط مراعاتها.
class Supplier < ApplicationRecord
  has_one :account, -> { where "confirmed = 1" }
end
التابع includes
يمكنك استخدام هذا التابع لتحديد ترتيبٍ ثانٍ للارتباطات التي يجب أن يتم تحميلها بشكل حثيث (Eager Loading) عند استخدام هذا الارتباط. مثلًا، لنفرض هذه النماذج:
class Supplier < ApplicationRecord
  has_one :account
end
 
class Account < ApplicationRecord
  belongs_to :supplier
  belongs_to :representative
end
 
class Representative < ApplicationRecord
  has_many :accounts
end
إذا كنت تسترجع الممثّلين (representatives) بشكل متكرر مباشرةً من المزوّدين (suppliers، أي supplier.account.representative@)، فيمكنك جعل تحميل الارتباطات أكثر فعالية عن طريق تضمين ارتباط الممثلين في ارتباط المزودين من خلال الحسابات:
class Supplier < ApplicationRecord
  has_one :account, -> { includes :representative }
end
 
class Account < ApplicationRecord
  belongs_to :supplier
  belongs_to :representative
end
 
class Representative < ApplicationRecord
  has_many :accounts
end
التابع readonly

عند استخدام التابع readonly، ستكون الكائنات المرتبطة قابلة للقراءة فقط عند تحميلها من الارتباط.

التابع select

يمكّنك التابع select من تجاوز الاستعلام SELECT الافتراضي المستخدم لتحميل البيانات من أجل الكائن المرتبط. افتراضيًا، يحمل ريلز جميع حقول الكائنات المرتبطة.

هل الكائنات المرتبطة موجودة؟

يمكنك التحقق من وجود الكائنات المرتبطة عن طريق استخدام التابع ?association.nil:
if @supplier.account.nil?
  @msg = "No account found for this supplier"
end

متى يتم حفظ الكائنات؟

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

في حال فشل أحد عمليات الحفظ هذه بسبب أخطاء التأكيد، سيقوم الإسناد بإعادة القيمة false وستلغى عملية الإسناد.

في حال عدم حفظ الكائن الأب (المعرّف لارتباط الفردية)، أي أعاد التابع ?new_record القيمة true، لن يتم حفظ الكائنات الأبناء؛ إذ يتم حفظها تلقائيًا عند حفظ الكائن الأب.

في حال أردت إسناد كائن ما إلى ارتباط فردية دون حفظه، استخدم التابع build_association.

مرجع ارتباط التعددية has_many

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

التوابع المضافة من ارتباط التعددية

عند تعريف ارتباط التعددية، يحصل الصنف المعرّف للارتباط تلقائيًا على 17 تابع متعلق بالارتباط:

  • collection
  • collection<<(object, ...)‎
  • collection.delete(object, ...)‎
  • collection.destroy(object, ...)‎
  • collection=(objects)‎
  • collection_singular_ids‎
  • collection_singular_ids=(ids)‎
  • collection.clear
  • collection.empty?‎
  • collection.size
  • collection.find(...)‎
  • collection.where(...)‎
  • collection.exists?(...)‎
  • collection.build(attributes = {}, ...)‎
  • collection.create(attributes = {})‎
  • collection.create!(attributes = {})‎
  • collection.reload
في كل هذه التوابع، تتغير الكلمة collection بالرمز المشير إلى أول وسيط ممرّر للتابع has_many، والكلمة collection_singular بالنمط الإفرادي (singularized version) من هذا الرمز. مثلًا، بفرض التعريف التالي:
class Author < ApplicationRecord
  has_many :books
end
كل عنصر من عناصر النموذج Author سيملك التوابع التالية:
books
books<<(object, ...)
books.delete(object, ...)
books.destroy(object, ...)
books=(objects)
book_ids
book_ids=(ids)
books.clear
books.empty?
books.size
books.find(...)
books.where(...)
books.exists?(...)
books.build(attributes = {}, ...)
books.create(attributes = {})
books.create!(attributes = {})
books.reload
التابع collection
يعيد التابع collection كائنًا من النوع Relation لكل الكائنات المرتبطة. في حال لم يكن هناك أي كائن مرتبط، سيعيد كائن Relation فارغ.
@books = @author.books
التابع (... ,collection<<(object
يضيف التابع >>collection كائنًا أو أكثر إلى المجموعة عن طريق تعيين مفاتيحها الأجنبية إلى المفتاح الرئيسي للنموذج المستدعي.
@author.books << @book1
التابع (... ,collection.delete(object
يحذف التابع collection.delete كائنًا أو أكثر من المجموعة عن طريق تعيين مفاتيحها الأجنبية إلى القيمة NULL.
@author.books.delete(@book1)
تحذير: إضافةً إلى ذلك، سيتم تدمير الكائنات في حال كانت مرتبطة باستخدام الخيار dependent: :destroy، وحذفها في حال كانت مرتبطة بالخيار dependent: :delete_all.
التابع (... ,collection.destroy(object
يحذف التابع collection.destroy كائنًا أو أكثر من المجموعة عن طريق استدعاء تابع التدمير destroy على كل كائن من الكائنات المعطاة.
@author.books.destroy(@book1)
تحذير: سيتم دائمًا حذف الكائنات من قاعدة البيانات، وتجاهل الخيار dependent:.
التابع (collection=(objects

يجعل التابع =collection المجموعة تحوي فقط الكائنات المعطاة، عن طريق الإضافة والحذف بالطريقة المناسبة. سيتم حفظ التعديلات مباشرةً في قاعدة البيانات.

التابع (collection_singular_ids=(ids

يجعل التابع =collection_singular_ids المجموعة تحوي فقط الكائنات المعرّفة بقيم المفاتيح الأجنبية الممررة، عن طريق الإضافة والحذف بالطريقة المناسبة. سيتم حفظ التعديلات مباشرةً في قاعدة البيانات.

التابع collection.clear
يحذف التابع collection.clear جميع الكائنات من المجموعة بناءً على استراتيجية الحذف المحددة بالخيار dependent. في حال عدم تحديد هذا الخيار، سيتم استخدام الاستراتيجية الافتراضية؛ إذ أن الاستراتيجية الافتراضية لارتباط التعددية الضمني هو delete_all، ولارتباط التعددية هو تعيين جميع المفاتيح الأجنبية إلى NULL.
@author.books.clear
تحذير: سيتم حذف الكائنات في حال ارتبطت بالخيار dependent: :destroy، كما هو الحال في dependent: :delete_all.
التابع ?collection.empty
يعيد التابع ?collection.empty القيمة true في حال عدم احتواء المجموعة على أية عناصر.
<% if @author.books.empty? %>
  No Books Found
<% end %>
التابع collection.size
يعيد هذا التابع عدد الكائنات الموجودة في المجموعة.
@book_count = @author.books.size
التابع (...)collection.find
يبحث التابع collection.find عن كائنات ضمن المجموعة، إذ يستخدم نفس نمط كتابة التابع ActiveRecord::Base.find.
@available_book = @author.books.find(1)
التابع (...)collection.where
يبحث التابع collection.where عن كائنات ضمن المجموعة بناءً على الشروط الممررة، لكن الكائنات محملة بشكل خامل، مما يعني أنه لن يتم تنفيذ استعلام قاعدة البيانات إلّا عند الوصول إلى الكائنات.
@available_books = @author.books.where(available: true) # لم يتم تنفيذ الاستعلام بعد
@available_book = @available_books.first # سيتم الآن تنفيذ الاستعلام
التابع (...)?collection.exists

يتحقق هذا التابع من وجود الكائن المحقق للشروط الممررة له. يستخدم هذا التابع نفس نمط الكتابة والخيارات المستخدمة في التابع ActiveRecord::Base.exists.

التابع (..., {} = collection.build(attributes
يعيد التابع collection.build عنصرًا أو مجموعةً من العناصر من النوع المرتبط (associated type). ستتم تهيئة (instantiate) الكائن(ات) من الخاصيات المُمرَّرة، وإنشاء الرابط عن طريق تعيين قيم حقول المفاتيح الأجنبية، لكن لن يتم حفظ الكائنات في قاعدة البيانات.
@book = @author.books.build(published_at: Time.now,
                                book_number: "A12345")
 
@books = @author.books.build([
  { published_at: Time.now, book_number: "A12346" },
  { published_at: Time.now, book_number: "A12347" }
])
التابع ({} = collection.create(attributes
يعيد التابع collection.create عنصرًا أو مجموعةً من العناصر من النوع المرتبط. ستتم تهيئة الكائن(ات) من الحقول الممررة، وإنشاء الرابط عن طريق تعيين قيم حقول المفاتيح الأجنبية؛ وبعد تمرير الكائنات جميع التأكيدات المحددة للنموذج، سيتم حفظها في قاعدة البيانات.
@book = @author.books.create(published_at: Time.now,
                                 book_number: "A12345")
 
@books = @author.books.create([
  { published_at: Time.now, book_number: "A12346" },
  { published_at: Time.now, book_number: "A12347" }
])
التابع ({} = collection.create!(attributes

يفعل ما يفعله التابع collection.create، لكن يرمي استثناء من النوع ActiveRecord::RecordInvalid في حال كان الكائن غير صحيح.

التابع collection.reload
يعيد التابع collection.reload الكائن Relation لكل الكائنات المرتبطة، ويفرض إعادة تحميل بيانات قاعدة البيانات. وفي حال عدم وجود أي كائنات مرتبطة، سيعيد الكائن Relation فارغًا.
@books = @author.books.reload

خيارات ارتباط التعددية (has_many)

في حين أن ريلز يستخدم الخيارات الافتراضية التي تعمل جيّدًا في معظم الحالات، قد تحتاج في بعض الأوقات إلى تعديل اسلوك الافتراضي للتابع has_many. يمكن تحقيق هذه التعديلات بواسطة تمرير خيارات عند إنشاء الارتباط. مثلًا، الارتباط التالي يستخدم خيارين:
class Author < ApplicationRecord
  has_many :books, dependent: :delete_all, validate: false
end
يدعم ارتباط الفردية الخيارات التالية:
  • :as
  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :foreign_key
  • :inverse_of
  • :primary_key
  • :source
  • :source_type
  • :through
  • :validate
الخيار as:

إن استخدام الخيار as:‎ يحدد أن هذا الارتباط هو ارتباط متعدد الأشكال.

الخيار autosave‎:

في حال حددت الخيار autosave:‎ إلى true، يقوم ريلز بحفظ أي كائنات مرتبطة ويدمر أي كائنات محددة للتدمير عند حفظ الكائن الأب. تحديد الخيار autosave: إلى false لا يساوي عدم تحديده إطلاقًا، ففي حال لم يتم تحديده فسيتم حفظ الكائنات المرتبطة الجديدة، لكن لن يتم حفظ الكائنات المرتبطة المحدثة.

الخيار class_name:‎
في حال عدم إمكانية اشتقاق اسم النموذج الآخر من اسم الارتباط، يمكنك استخدام الخيار class_name:‎ لتحديد اسم النموذج الآخر. مثلًا، في حال امتلك الكاتب كتبًا، لكن الاسم الحقيقي للنموذج الممثل للكتب هو Transaction، ستُعرّف الارتباط كالتالي:
class Author < ApplicationRecord
  has_many :books, class_name: "Transaction"
end
الخيار counter_cache:‎

يمكن استخدام هذا الخيار لتهيئة عداد مؤقت (counter cache) باسم مختلف. ستحتاج إلى هذا الخيار فقط عند تعديل اسم العداد في ارتباط الانتماء.

الخيار dependent:‎

يتحكّم بالسلوك المطبق على الكائن المرتبط عند تدمير الكائن الأب:

  • القيمة destroy:، سيتم استدعاء التابع destroy على كل الكائنات المرتبطة في حال تدمير الكائن.
  • القيمة delete_all:، جميع الكائنات المرتبطة يتم حذفها من قاعدة البيانات دون استدعاء توابع رد النداء الخاصة بها عند تدمير الكائن.
  • القيمة nullify:، والتي تجعل قيمة حقل المفاتيح الأجنبية NULL. لن يتم تشغيل توابع رد النداء آنذاك.
  • القيمة restrict_with_exception:، ترمي استثناءً عند وجود كائن مرتبط.
  • القيمة restrict_with_error:، تضيف خطأ إلى الأب في حال وجود كائن مرتبط.
الخيار foreign_key:‎
كعرف، يعتقد ريلز أن اسم الحقل المستخدم للمفتاح الأجنبي على النموذج المعرف هو اسم الارتباط مع اللاحقة id_. يسمح الخيار foreign_key:‎ بتحديد اسم حقل المفتاح الأجنبي مباشرةً:
class Author < ApplicationRecord
  has_many :books, foreign_key: "cust_id"
end
ملاحظة: في أي حالة، لن ينشئ ريلز المفاتيح الأجنبية تلقائيًا. يجب عليك تعريفها كجزء من تهجيراتك.
الخيار inverse_of:‎
يحدد الخيار inverse_of اسم ارتباط الانتماء الموجود في عكس هذا الارتباط.
class Author < ApplicationRecord
  has_many :books, inverse_of: :author
end
 
class Book < ApplicationRecord
  belongs_to :author, inverse_of: :books
end
الخيار primary_key:‎

كعرف، يعتقد ريلز أن الحقل المسمى id يستخدم لحفظ المفتاح الرئيسي لجداوله. يمكّنك الخيار primary_key:‎ من تحديد اسم مختلف لحقل المفتاح الرئيسي.

لنفترض مثلًا أنَّ الجدول المسمى users يملك الحقل id الذي يعد مفتاحًا رئيسيًّا ولكن يملك حقلًا آخر يدعى guid؛ قد يطلب أنَّ يحتوي الجدول todos على قيمة الحقل guid كمفتاح أجنبي وليس على قيمة الحقل id. يمكننا تحقيق ذلك كالتالي:
class User < ApplicationRecord
  has_many :todos, primary_key: :guid
end
عند تشغيل user.todos.create@، سيأخذ السجل todo@ قيمة الحقل user_id من الحقل guid الخاص بالسجل user@.
الخيار source:‎

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

الخيار source_type:‎

يحدد الخيار source_type:‎ مصدر نوع الارتباط من أجل ارتباط التعددية الضمني الذي يعبر ارتباط متعدد الأشكال.

الخيار through:‎

يحدد الخيار through:‎ النموذج الوسيطي لتنفيذ الاستعلامات عليه. تمّت مناقشة ارتباطات التعددية الضمنية سابقًا في هذا التوثيق.

الخيار validate:‎

إن تمرير القيمة true للخيار validate:‎ سيؤدي إلى التحقق من صحة الكائنات المرتبطة عند حفظ الكائن الأب. افتراضيًا، تكون قيمة هذا الخيار true، مما يعني أنه سيتم التحقق من صحة الكائنات المرتبطة عند تحديث الكائن الأب.

مجالات ارتباط التعددية

هناك بعض الأوقات التي تحتاج فيها إلى تعديل الاستعلام المستخدم في ارتباط التعددية. يمكن تحقيق هذا التعديل عن طريق مجال كتلة. مثلًا:
class Author < ApplicationRecord
  has_many :books, -> { where processed: true }
end
يمكنك استخدام أي من توابع الاستعلام المعيارية داخل مجال الكتلة. سيتم مناقشة التوابع التالية أدناه:
  • where
  • extending
  • group
  • includes
  • limit
  • offset
  • order
  • readonly
  • select
  • distinct
التابع where
يمكّنك التابع where من تحديد الشروط التي يجب على الكائنات المرتبطة مراعاتها.
class Author < ApplicationRecord
  has_many :confirmed_books, -> { where "confirmed = 1" },
    class_name: "Book"
end
يمكنك أيضًا تحديد الشروط عن طريق تمرير جدول hash:
class Author < ApplicationRecord
  has_many :confirmed_books, -> { where confirmed: true },
                              class_name: "Book"
end
في حال أردت استخدام نمط الجدول hash، فسيتم تحديد نطاق إنشاء السجلات (record creation) عن طريق هذا الارتباط باستعمال الجدول hash الممرر تلقائيًّا. في هذه الحالة، عند استخدام author.confirmed_books.create@ أو author.confirmed_books.build@، سيتم إنشاء الكتب حيث يملك الحقل confirmed القيمة true.
التابع extending

يمكّنك هذا التابع من تحديد اسم الوحدة (module) التي يورث منها وسيط الارتباط. ستناقش امتدادات الارتباط لاحقًا في هذا التوثيق.

التابع group
يمكّنك هذا التابع من تحديد اسم الحقل الذي يجب تجميع النتائج من خلاله، باستخدام التعليمة GROUP BY في باحث SQL.
class Author < ApplicationRecord
  has_many :line_items, -> { group 'books.id' },
                        through: :books
end
التابع includes
يمكنك استخدام هذا التابع من تحديد ترتيبٍ ثانٍ للارتباطات التي يجب أن يتم تحميلها بشكل حثيث (Eager Loading) عند استخدام هذا الارتباط. مثلًا، لنفرض هذه النماذج:
class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :line_items
end
 
class LineItem < ApplicationRecord
  belongs_to :book
end
إذا كنت تسترجع النموذج LineItem بشكل متكرر مباشرةً من نموذج الكتّاب Author (أي author.books.line_items@)، يمكنك جعل تحميل الارتباطات أكثر فعالية عن طريق تضمين كائنات النموذج LineItem بالارتباط الواصل بين الكتّاب والكتب:
class Author < ApplicationRecord
  has_many :books, -> { includes :line_items }
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :line_items
end
 
class LineItem < ApplicationRecord
  belongs_to :book
end
التابع limit
يمكّنك هذا التابع من تحديد عدد الكائنات الواجب البحث عنها وجلبها عند استخدام هذا الارتباط.
class Author < ApplicationRecord
  has_many :recent_books,
    -> { order('published_at desc').limit(100) },
    class_name: "Book"
end
التابع offset

يمكّنك هذا التابع من تحديد نقطة بداية قراءة الكانئات عن طريق الارتباط مثل { offset(11)‎ } الذي سيؤدي إلى تجاوز أول 11 سجل.

التابع order
يمكّنك هذا التابع من تحديد ترتيب قراءة الكائنات المرتبطة (عن طريق استخدام التعليمة ORDER BY).
class Author < ApplicationRecord
  has_many :books, -> { order "date_confirmed DESC" }
end
التابع readonly

عند استخدام التابع readonly، ستكون الكائنات المرتبطة قابلة للقراءة فقط عند إعادتها من الارتباط.

التابع select

يمكّنك التابع select من تجاوز الاستعلام SELECT الافتراضي المستخدم لإعادة البيانات من أجل الكائنات المرتبطة. افتراضيًا، يعيد ريلز جميع حقول الكائنات المرتبطة.

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

التابع distinct
استخدم هذا التابع لجعل المجموعة المعادة فارغة من التكرارات. من الضروري استخدام هذا التابع مع الخيار through:.
class Person < ApplicationRecord
  has_many :readings
  has_many :articles, through: :readings
end
 
person = Person.create(name: 'John')
article   = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
Reading.all.inspect     # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
في الحالة السابقة، يوجد قراءتان (reading) ويعيد التابع person.articles كليهما رغم أنّهما يشيران إلى نفس المقالة. الآن، لنقم باستخدام التابع distinct:
class Person
  has_many :readings
  has_many :articles, -> { distinct }, through: :readings
end
 
person = Person.create(name: 'Honda')
article   = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect     # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
في الحالة السابقة ما يزال هناك قراءتين، لكن التابع person.articles يعيد فقط مقالة واحدة لأن المجموعة تحمّل القيم الفريدة فقط. في حال أردت التأكد من أنه بعد إدخال البيانات، جميع السجلات في الارتباط المحفوظ هي سجلات فريدة، يجب أن تضيف قيد الفردية إلى الجدول ذاته. مثلًا، إذا كان لدينا جدولًا يسمى readings، وأردت التأكد أنه يمكن إضافة المقالات إلى الشخص مرة واحد فقط، فيمكنك كتابة التعليمة التالية في التهجير:
add_index :readings, [:person_id, :article_id], unique: true
بعد تحديد قيد الفردية، محاولة إضافة مقالة للشخص مرّتين ستؤدي إلى رمي استثناء من النوع ActiveRecord::RecordNotUnique.
person = Person.create(name: 'Honda')
article = Article.create(name: 'a1')
person.articles << article
person.articles << article # => ActiveRecord::RecordNotUnique
ومن الجدير بالذكر أن التحقق عن الفردية باستخدام التوابع مثل ?include هو عرضة لحالات التسابق (race conditions). لا تحاول أن تستخدم ?include لفرض الفردية في ارتباطك. مثلًا، باستخدام مثال المقالات السابق، قد تكون الشيفرة التالية عرضة لحاة تسابق لأنّه من الممكن أن يقوم أكثر من مستخدم بتنفيذ التالي في الوقت ذاته:
person.articles << article unless person.articles.include?(article)

متى يتم حفظ الكائنات؟

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

في حال فشل أحد عمليات الحفظ هذه بسبب أخطاء التحقق من الصحة، سيقوم الإسناد بإعادة القيمة false وستلغى عملية الإسناد.

في حال عدم حفظ الكائن الأب (المعرّف لارتباط التعددية)، أي أعاد التابع ?new_record القيمة true، لن يتم حفظ الكائنات الأبناء؛ إذ يتم حفظها تلقائيًا عند حفظ الكائن الأب.

في حال أردت إسناد كائن ما إلى ارتباط تعددية دون حفظه، استخدم التابع collection.build.

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

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

التوابع المضافة من ارتباط الانتماء والتعددية

عند تعريف ارتباط الانتماء والتعددية، يحصل الصنف المعرّف للارتباط تلقائيًا على 17 تابع متعلقة بالارتباط:

  • collection
  • collection<<(object, ...)‎
  • collection.delete(object, ...)‎
  • collection.destroy(object, ...)‎
  • collection=(objects)‎
  • collection_singular_ids
  • collection_singular_ids=(ids)‎
  • collection.clear
  • collection.empty?‎
  • collection.size
  • collection.find(...)‎
  • collection.where(...)‎
  • collection.exists?(...)‎
  • collection.build(attributes = {}, ...)‎
  • collection.create(attributes = {})‎
  • collection.create!(attributes = {})‎
  • collection.reload
في كل هذه التوابع، تتغير الكلمة collection بالرمز الذي يشير إلى أول وسيط ممرّر إلى has_and_belongs_to_man، والكلمة collection_singular بالنمط الإفرادي (singularized version) من هذا الرمز. مثلًا، بفرض التعريف التالي:
class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end
كل عنصر من عناصر النموذج Part سيملك التوابع التالية:
assemblies
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
assemblies=(objects)
assembly_ids
assembly_ids=(ids)
assemblies.clear
assemblies.empty?
assemblies.size
assemblies.find(...)
assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
assemblies.reload
توابع إضافية للحقول

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

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

التابع collection
يعيد التابع collection كائنًا من النوع Relation لكل الكائنات المرتبطة. في حال لم يكن هناك أي كائن مرتبط، سيعيد كائن Relation فارغًا.
@assemblies = @part.assemblies
التابع (... ,collection<<(object
يضيف التابع >>collection كائنًا أو أكثر إلى المجموعة عن طريق إنشاء سجلات في الجدول الوسيطي (الرابط).
@part.assemblies << @assembly1
ملاحظة: هذا التابع هو اسم بديل للتابعين collection.concat و collection.push.
التابع (... ,collection.delete(object
يحذف التابع collection.delete كائنًا أو أكثر من المجموعة عن طريق حذف السجلات من الجدول الوسيطي. لا يدمر هذا التابع الكائنات.
@part.assemblies.delete(@assembly1)
التابع (... ,collection.destroy(object
يحذف التابع collection.destroy كائنًا أو أكثر من المجموعة عن طريق حذف السجلات من الجدول الوسيطي. لا يدمر هذا التابع الكائنات.
@part.assemblies.destroy(@assembly1)
التابع (collection=(objects

يجعل التابع =collection المجموعة تحوي فقط الكائنات المعطاة، عن طريق الإضافة والحذف بالطريقة المناسبة. سيتم حفظ التعديلات مباشرةً في قاعدة البيانات.

التابع collection_singular_ids
يعيد التابع collection_singular_ids مصفوفة من المفاتيح الرئيسية للكائنات في المجموعة.
@assembly_ids = @part.assembly_ids
التابع (collection_singular_ids=(ids

يجعل التابع =collection_singular_ids المجموعة تحوي فقط الكائنات المعرّفة بقيم المفاتيح الرئيسية الممررة، عن طريق الإضافة والحذف بالطريقة المناسبة. سيتم حفظ التعديلات مباشرةً في قاعدة البيانات.

التابع collection.clear

يحذف التابع collection.clear جميع الكائنات من المجموعة عن طريق حذف السجلات من الجدول الوسيطي. لا يدمر هذا التابع الكائنات المرتبطة.

التابع ?collection.empty
يعيد التابع ?collection.empty القيمة true في حال عدم احتواء المجموعة على أية عناصر.
<% if @part.assemblies.empty? %>
  This part is not used in any assemblies
<% end %>
التابع collection.size
يعيد هذا التابع عدد الكائنات الموجودة في المجموعة.
@assembly_count = @part.assemblies.size
التابع (...)collection.find
يبحث التابع collection.find عن كائنات ضمن المجموعة، إذ يستخدم نفس صياغة وخيارات التابع ActiveRecord::Base.find.
@assembly = @part.assemblies.find(1)
التابع (...)collection.where
يبحث التابع collection.where عن كائنات ضمن المجموعة بناءً على الشروط الممررة، لكن الكائنات محملة بشكل خامل (lazily)، مما يعني أنه لن يتم تنفيذ استعلام قاعدة البيانات إلّا عند الوصول إلى الكائن(ات).
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
التابع (...)?collection.exists

يتحقق هذا التابع من وجود الكائن المحقق للشروط الممررة له. يستخدم هذا التابع نفس الصياغة والخيارات المستخدمة في التابع ActiveRecord::Base.exists.

التابع (..., {} = collection.build(attributes
يعيد التابع collection.build عنصرًا أو مجموعةً من العناصر من النوع المرتبط. ستتم تهيئة الكائنات من الحقول الممررة، وإنشاء الرابط خلال الجدول الوسيطي، لكن لن يتم حفظ الكائنات في قاعدة البيانات.
@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})
التابع ({} = collection.create(attributes
يعيد التابع collection.create عنصرًا أو مجموعةً من العناصر من النوع المرتبط. ستتم تهيئة الكائنات من الحقول الممررة، وإنشاء الرابط خلال الجدول الوسيطي، وبعد مرور الكائنات جميع التأكيدات المحددة للنموذج، سيتم حفظها في قاعدة البيانات.
@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})
التابع ({} = collection.create!(attributes

يفعل ما يفعله التابع collection.create، لكن يرمي استثناءً من النوع ActiveRecord::RecordInvalid في حال كان الكائن غير صحيح.

التابع collection.reload
يعيد التابع collection.reload الكائن Relation لكل الكائنات المرتبطة، ويفرض إعادة تحميل بيانات قاعدة البيانات. وفي حال عدم وجود أي كائنات مرتبطة، سيعيد كائن Relation فارغًا.
@assemblies = @part.assemblies.reload

خيارات ارتباط الانتماء والتعددية (has_and_belongs_to_many)

في حين أن ريلز يستخدم الخيارات الافتراضية التي تعمل جيّدًا في معظم الحالات، فقد تحتاج في بعض الأوقات إلى تعديل سلوك has_and_belongs_to_many الافتراضي. يمكن تحقيق هذه التعديلات بواسطة تمرير خيارات عند إنشاء الارتباط. مثلًا، الارتباط التالي يستخدم خيارين:
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { readonly },
                                       autosave: true
end
يدعم ارتباط الفردية الخيارات التالية:
  • :association_foreign_key
  • :autosave
  • :class_name
  • :foreign_key
  • :join_table
  • :validate
الخيار association_foreign_key:‎

كعرف، يعتقد ريلز أنَّ اسم الحقل في الجدول الوسيطي المستخدم لحفظ المفتاح الأجنبي الذي يشير إلى النموذج الآخر هو اسم هذا النموذج متبوع باللاحقة id_. يمكّنك الخيار association_foreign_key: من تعديل هذا الاسم مباشرةً:

ملاحظة: إن الخيارات foreign_key: و association_foreign_key: هي مفيدة أثناء تهيئة علاقة كثير إلى كثير ذاتية الربط (many-to-many self-join)، مثلًا:
class User < ApplicationRecord
  has_and_belongs_to_many :friends,
      class_name: "User",
      foreign_key: "this_user_id",
      association_foreign_key: "other_user_id"
end
الخيار autosave:‎

في حال حددت الخيار autosave: إلى true، يقوم ريلز بحفظ أي كائنات مرتبطة ويدمر أي كائنات محددة للتدمير عند حفظ الكائن الأب. تحديد الخيار autosave: إلى false لا يساوي عدم تحديده إطلاقًا، ففي حال لم يتم تحديده فسيتم حفظ الكائنات المرتبطة الجديدة، لكن لن يتم حفظ الكائنات المرتبطة المحدثة.

الخيار class_name:‎
في حال عدم إمكانية اشتقاق اسم النموذج الآخر من اسم الارتباط، يمكنك استخدام الخيار class_name: لتحديد اسم النموذج الآخر. مثلًا، في حال امتلك الجزء عدة مجمّعات (assemblies)، لكن الاسم الحقيقي للنموذج الممثل للمجمعات هو Gadget، ستعرّف الارتباط كالتالي:
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
الخيار foreign_key:‎
كعرف، يعتقد ريلز أن اسم الحقل في الجدول الوسيطي المستخدم للمفتاح الأجنبي المشير للنموذج الحالي هو اسم النموذج مع اللاحقة id_. يسمح الخيار foreign_key: بتحديد اسم حقل المفتاح الأجنبي مباشرةً:
class User < ApplicationRecord
  has_and_belongs_to_many :friends,
      class_name: "User",
      foreign_key: "this_user_id",
      association_foreign_key: "other_user_id"
end
الخيار join_table:‎

في حال لم يكن اسم الجدول الوسيطي هو الاسم الافتراضي، يمكنك استخدام الخيار join_table: لاستبدال القيمة الافتراضية.

الخيار validate:‎

إن تمرير القيمة true للخيار validate: سيؤدي إلى التحقق من صحة الكائنات المرتبطة عند حفظ الكائن الأب. افتراضيًا، تكون قيمة هذا الخيار true، مما يعني أنه سيتم التحقق من صحة الكائنات المرتبطة عند تحديث الكائن الأب.

مجالات ارتباط التعددية

هناك بعض الأوقات التي تحتاج فيها إلى تعديل الاستعلام المستخدم في ارتباط الانتماء والتعددية. يمكن تحقيق هذا التعديل عن طريق مجال كتلة (scope block). مثلًا:
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { where active: true }
end
يمكنك استخدام أي من توابع الاستعلام المعيارية داخل مجال الكتلة. سيتم مناقشة التوابع التالية أدناه:
  • where
  • extending
  • group
  • includes
  • limit
  • offset
  • order
  • readonly
  • select
  • distinct
التابع where
يمكّنك التابع where من تحديد الشروط التي يجب على الكائنات المرتبطة مراعاتها.
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { where "factory = 'Seattle'" }
end
يمكنك أيضًا تحديد الشروط عن طريق تمرير جدول hash:
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { where factory: 'Seattle' }
end
في حال أردت استخدام نمط الجدول hash، سيتم تحديد نطاق إنشاء السجلات عن طريق هذا الارتباط باستخدام الجدول hash الممرر تلقائيًّا. في هذه الحالة، عند استخدام parts.assemblies.create@ أو parts.assemblies.build@، سيتم إنشاء الطلبات حيث تكون قيمة الحقل factory هي Seattle.
التابع extending

يمكّنك هذا التابع من تحديد اسم الوحدة (module) التي يورث منها وسيط الارتباط. ستناقش امتدادات الارتباط لاحقًا في هذا التوثيق.

التابع group
يمكّنك هذا التابع من تحديد اسم الحقل الذي يجب تجميع النتائج من خلاله، باستخدام التعليمة GROUP BY في باحث SQL.
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { group "factory" }
end
التابع includes

يمكنك استخدام هذا التابع لتحديد ترتيبٍ ثانٍ للارتباطات التي يجب أن يتم تحميلها بشكل حثيث (Eager Loading) عند استخدام هذا الارتباط.

التابع limit
يمكّنك هذا التابع من تحديد عدد الكائنات الواجب البحث عنها وجلبها عند استخدام هذا الارتباط.
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { order("created_at DESC").limit(50) }
end
التابع offset

يمكّنك هذا التابع من تحديد نقطة بداية قراءة الكانئات عن طريق الارتباط. مثلًا { offset(11)‎ } ستؤدي إلى تجاوز أول 11 سجل.

التابع order
يمكّنك هذا التابع من تحديد ترتيب قراءة الكائنات المرتبطة (عن طريق استخدام الاستعلام ORDER BY).
class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { order "assembly_name ASC" }
end
التابع readonly

عند استخدام التابع readonly، ستكون الكائنات المرتبطة قابلة للقراءة فقط عند تحميلها من الارتباط.

التابع select

يمكّنك التابع select من تجاوز الاستعلام SELECT الافتراضي المستخدم لإعادة البيانات من أجل الكائنات المرتبطة. افتراضيًا، يعيد ريلز جميع حقول الكائنات المرتبطة.

التابع distinct

استخدم هذا التابع لجعل المجموعة المعادة خالية من التكرارات (فريدة).

متى يتم حفظ الكائنات؟

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

في حال فشل أحد عمليات الحفظ هذه بسبب أخطاء التحقق من الصحة، سيقوم الإسناد بإعادة القيمة false وستلغى عملية الإسناد.

في حال عدم حفظ الكائن الأب (المعرّف لارتباط الانتماء والتعددية)، أي أعاد التابع ?new_record القيمة true، لن يتم حفظ الكائنات الأبناء، إذ يتم حفظها تلقائيًا عند حفظ الكائن الأب.

في حال أردت إسناد كائن ما إلى ارتباط انتماء وتعددية دون حفظه، استخدم التابع collection.build.

توابع رد النداء للارتباطات

إن توابع رد النداء ترتبط مباشرةً بدورة حياة كائنات Active Record، مما يمكّنك من العمل مع هذه الكائنات ضمن نقاط متعددة. مثلًا، يمكنك استخدام تابع رد النداء before_save: لإضافة سلوك معين قبل حفظ الكائن.

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

  • before_add
  • after_add
  • before_remove
  • after_remove
يمكنك تعريف توابع رد النداء للارتباطات عن طريق إضافة الخيارات لتعريف الارتباطات. مثلًا:
class Author < ApplicationRecord
  has_many :books, before_add: :check_credit_limit
 
  def check_credit_limit(book)
    ...
  end
end
يمرر ريلز الكائن المضاف أو المحذوف من المجموعة إلى تابع رد النداء. يمكنك تكديس ردود النداء على حدث وحيد عن طريق تمريرها كمصفوفة:
class Author < ApplicationRecord
  has_many :books,
    before_add: [:check_credit_limit, :calculate_shipping_charges]
 
  def check_credit_limit(book)
    ...
  end
 
  def calculate_shipping_charges(book)
    ...
  end
end
في حال رمى التابع before_add استثناءً، لن يضاف الكائن إلى المجموعة. بالمثل، في حال رمى التابع before_remove استثناءً، لن يحذف الكائن من المجموعة.

امتدادات الارتباطات

في ريلز، أنت لست مقيدًا بالخيارات الوظيفية المزوّدة تلقائيًا على كائنات الارتباط الوسيطية. يمكنك إنشاء توسيع هذه الكائنات عن طريق الوحدات المجهولة (anonymous modules)، أو إضافة باحيثين جددًا (new finders)، أو منشئات، أو توابع أخرى. مثلًا:
class Author < ApplicationRecord
  has_many :books do
    def find_by_book_prefix(book_number)
      find_by(category_id: book_number[0..2])
    end
  end
end
في حال كان لديك ملحقة (extension) يجب مشاركتها مع أكثر من ارتباط، فيمكنك استخدام وحدة ملحقة مسمّاة. مثلًا:
module FindRecentExtension
  def find_recent
    where("created_at > ?", 5.days.ago)
  end
end
 
class Author < ApplicationRecord
  has_many :books, -> { extending FindRecentExtension }
end
 
class Supplier < ApplicationRecord
  has_many :deliveries, -> { extending FindRecentExtension }
end
يمكن للملحقات أن تشير لداخل وسيط الارتباط عن طريق ثلاث خاصيات خاصة بالكائن الملحق proxy_association:
  • يعيد proxy_association.owner الكائن الذي يعد الارتباط جزءًا منه.
  • يعيد proxy_association.reflection كائن الانعكاس (reflection object) الذي يعبر عن الارتباط.
  • يعيد proxy_association.target الكائن المرتبط (associated object) من أجل ارتباط الانتماء أو الفردية، أو مجموعة الكائنات المرتبطة في ارتباط التعددية أو الانتماء والتعددية.

وراثة الجدول الوحيد

في بعض الأحيان، ستحتاج إلى مشاركة حقولٍ وسلوك بين مجموعة من النماذج المختلفة. لنفرض أن لدينا النماذج Car، و Motorcycle، و Bicycle، سنحتاج إلى مشاركة الحقلين color و price وبعض التوابع في هذه النماذج، لكن يجب أن يملك كل نموذج سلوكًا مختلفًا عن الآخر، ومتحكمات مختلفة أيضًا.

يجعل ريلز هذا الأمر سهلًا. أولًا، لنوّلد النموذج Vehicle الأساسي:
$ rails generate model vehicle type:string color:string price:decimal{10.2}
هل لاحظت أننا نضيف الحقل "type"؟ لما كانت كل النماذج ستُحفَظ في جدول وحيد، سيحفظ ريلز اسم النموذج المستخدم في هذا الحقل. في مثالنا، يمكن أن يكون هذا إمّا Car، أو Motorcycle، أو Bicycle. لن تعمل STI (اختصار للعبارة Single Table Inheritance) دون الحقل type في الجدول.

نقوم بعد ذلك بتوليد النماذج الثلاثة التي ترث من النموذج Vehicle. من أجل هذا التخصيص، يجب استخدام الخيار parent=PARENT--، الذي يولّد نموذجًا يرث من نموذج آخر دون الحاجة إلى تهجير موافق (لأن الجدول موجود مسبقًا).

مثلًا، لتوليد النموذج Car:
$ rails generate model car --parent=Vehicle
سيبدو النموذج المولّد كالتالي:
class Car < Vehicle
end
هذا يعني أنّ كل السلوك المستخدم في النموذج Vehicle موجودٌ في النموذج Car أيضًا، كالارتباطات والتوابع العامة، ...إلخ. إن إنشاء سيارة سيؤدي إلى حفظها في جدول العربات vehicles مع تعيين قيمة الحقل type للقيمة Car:
Car.create(color: 'Red', price: 10000)
ستولّد استعلام SQL التالي:
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
بالمثل، الاستعلام عن السيارات سيبحث في جدول العربات عن السيارات فقط:
Car.all
سيولّد استعلام SQL التالي:
SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')

مصادر