Rails/association basics

من موسوعة حسوب
< Rails
مراجعة 09:10، 19 يناير 2019 بواسطة جميل-بيلوني (نقاش | مساهمات) (إنشاء الصفحة، هذه الصفحة من مساهمات "صفوان الحاجي")
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى التنقل اذهب إلى البحث

الارتباطات في السجل الفعال

يغطي هذا الدليل مزايا الارتباطات للسجل الفعَّال. بعد قراءة هذا الدليل، ستتعلم:

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

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

في ريلز، يعدّ الارتباط اتصالًا بين نموذجين من السجل الفعال. لمَ نحتاج إلى الارتباطات بين النماذج؟ لأنها تجعل العمليات الشائعة سهلة وبسيطة في تطبيقك. مثلًا، لنفرض أن لدينا تطبيق ريلز بسيط يحوي نموذجًا للكتّاب ونموذجًا للكتب؛ كل كاتب يملك العديد من الكتب. بدون الارتباطات، سيبدو تعريف النموذج بالشكل التالي:

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

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

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

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

أنواع السجل الفعال

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

  • 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

[[ملف:Rails_belongs_to.png|بديل=إنشاء ارتباط انتماء (belongs_to) بين نموذج الكتب ونموذج الكتَّاب.|بدون|تصغير|500بك|إنشاء ارتباط انتماء (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

[[ملف:Rails_has_one.png|بديل=إنشاء ارتباط الملكية بين نموذج الحسابات ونموذج المزودين.|بدون|تصغير|500بك|إنشاء ارتباط الملكية بين نموذج الحسابات ونموذج المزودين.]] قد يبدو التهجير الموافق كالتالي:

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

ملاحظة: إن اسم النموذج الآخر يكون بنمط الجمع عند تعريف ارتباط التعددية.

[[ملف:Rails_has_many.png|بديل=إنشاء ارتباط تعددية (has_many) بين نموذج الكتب ونموذج الكتَّاب.|بدون|تصغير|500بك|إنشاء ارتباط تعددية (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

[[ملف:Rails_has_many_through.png|بديل=إنشاء ارتباط التعددية الضمني بين ثلاثة نماذج في تطبيق طبي لحجز المواعيد بين المريض والطبيب.|بدون|تصغير|500بك|إنشاء ارتباط التعددية الضمني بين ثلاثة نماذج في تطبيق طبي لحجز المواعيد بين المريض والطبيب.]] قد يبدو التهجير الموافق كالتالي:

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

[[ملف:Rails_has_one_through.png|بديل=إنشاء ارتباط الفردية الضمني بين نموذج المزودين ونموذج تاريخ الحساب عبر نموذج الحسابات.|بدون|تصغير|500بك|إنشاء ارتباط الفردية الضمني بين نموذج المزودين ونموذج تاريخ الحساب عبر نموذج الحسابات.]] قد يبدو التهجير الموافق كالتالي:

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

[[ملف:Rails_habtm.png|بديل=إنشاء ارتباط الانتماء والتعددية بين ثلاثة نماذج.|بدون|تصغير|500بك|إنشاء ارتباط الانتماء والتعددية بين ثلاثة نماذج.]] قد يبدو التهجير الموافق كالتالي:

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

[[ملف:Rials_polymorphic.png|بديل=مثال عن ارتباط متعدد الأشكال.|بدون|تصغير|500بك|مثال عن ارتباط متعدد الأشكال.]]

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

عند تصميم نماذج البيانات، قد تحتاج أحيانًا إلى نموذج يملك ارتباطًا بنفسه. مثلًا، قد تحتاج إلى تخزين كل الموظفين في نموذج بيانات وحيد، لكن تحتاج أيضًا إلى إقامة علاقة بين المدير وموظّفيه التابعين. يمكن نمذجة هذه الحالة باستخدام الارتباطات الذاتية (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

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

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

  • التحكم بالذاكرة المؤقتة (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:، يقوم السجل الفعال بإنشاء اسم الجدول الوسيطي عن طريق الترتيب الأبجدي لأسماء الأصناف. لذا، من أجل ارتباط بين الكتب والكتّاب، سيكون اسم الجدول الوسيطي "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) الحالية. يكون هذا مهمًا عند تعريف نماذج السجل الفعال في وحدة. مثلًا:

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

يحاول السجل الفعال أن يتعرف تلقائيًا على أن هذين النموذجين يشاركان ارتباطًا ثنائي الاتجاه (bi-directional association) بناءً على اسم الارتباط. في هذه الطريقة، يحمّل السجل الفعال فقط نسخة واحدة من الكائن 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

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

  • ‎:through
  • ‎:foreign_key

مثلًا، لنفرض تعريف النموذج التالي:

class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end

لن يتعرف السجل الفعال تلقائيًا على الارتباطات ثنائية الاتجاه:

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

يزوّد السجل الفعال بالخيار 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)، يتعرّف السجل الفعال الآن على الارتباط ثنائي الاتجاه:

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

ملاحظة: في أي حالة، لن ينشئ ريلز المفاتيح الأجنبية تلقائيًا. يجب عليك تعريفها كجزء من تهجيراتك.

الخيار primary_key:

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

الخيار source:

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

الخيار source_type:

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

الخيار through:

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

الخيار validate:

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

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

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

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

إذا كنت تستخدم ارتباط الممثّلين (representative) بشكل متكرر مباشرةً من المزوّدين (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 من تجاوز استعلام الـ SQL 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 بالنمط الإفرادي من هذا الرمز. مثلًا، بفرض التعريف التالي:

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 عنصر أو مجموعة من العناصر من النوع المرتبط. ستتم تهيئة الكائنات من الحقول الممررة، وإنشاء الرابط عن طريق تعيين قيم حقول المفاتيح الأجنبية، لكن لن يتم حفظ الكائنات في قاعدة البيانات.

@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 بحقل مسمى guid كفتاح رئيسي، إذا أردنا فصل جدول todos بحيث يحوي على حقل مفتاح أجنبي مسمى user_id يأخذ قيمه من الحقل guid، يمكننا تحقيق ذلك كالتالي:

class User < ApplicationRecord

 self.primary_key = 'guid' # المفتاح الرئيسي مسمى guid وليس id

end

class Todo < ApplicationRecord

 belongs_to :user, 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 Book < ApplicationRecord

 belongs_to :author, -> { where active: true },

                       dependent: :destroy

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، سيتم تحويل نمط حفظ السجلات عن طريق هذا الارتباط إلى النمط المجالي باستخدام الـ 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 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 بشكل متكرر مباشرةً من نموذج الكتّاب 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

يمكّنك هذا التابع من تحديد ترتير قراءة الكائنات المرتبطة (عن طريق استخدام التعليمة SQL ORDER BY).

class Author < ApplicationRecord

 has_many :books, -> { order "date_confirmed DESC" }

end

التابع readonly

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

التابع select

يمكّنك التابع select من تجاوز استعلام الـ SQL 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 بالنمط الإفرادي من هذا الرمز. مثلًا، بفرض التعريف التالي:

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 عن كائنات ضمن المجموعة بناءً على الشروط الممررة، لكن الكائنات محملة بشكل خامل، مما يعني أنه لن يتم تنفيذ استعلام قاعدة البيانات إلّا عند الوصول إلى الكائنات.

@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: هي مفيدة أثناء تهيئة علاقة كثير إلى كثير ذاتية، مثلًا:

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: لتحديد اسم النموذج الآخر. مثلًا، في حال امتلك الجزء عدة مجمّعات، لكن الاسم الحقيقي للنموذج الممثل للمجمعات هو 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، مما يعني أنه سيتم تأكيد الكائنات المرتبطة عند تحديث الكائن الأب.

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

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

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 إلى true.

التابع 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

يمكّنك هذا التابع من تحديد ترتير قراءة الكائنات المرتبطة (عن طريق استخدام التعليمة SQL ORDER BY).

class Parts < ApplicationRecord

 has_and_belongs_to_many :assemblies,

   -> { order "assembly_name ASC" }

end

التابع readonly

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

التابع select

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

التابع distinct

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

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

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

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

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

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

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

إن توابع رد النداء ترتبط مباشرةً بدورة حياة كائنات السجل الفعال، مما يمكّنك من العمل مع هذه الكائنات ضمن نقاط متعددة. مثلًا، يمكنك استخدام تابع رد النداء 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 استثناءً، لن يحذف الكائن من المجموعة.

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

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

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

في حال كان لديك امتداد مشترك في أكثر من ارتباط، يمكنك استخدام وحدة امتداد مسمّاة. مثلًا:

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 كائن الانعكاس الذي يعبر عن الارتباط.
  • يعيد proxy_association.target الكائن المرتبط في ارتباطي الانتماء والفردية، ومجموعة الكائنات المرتبطة في ارتباطي التعددية والانتماء والتعددية.

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

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

يجعل ريلز هذا الأمر سهلًا. أولًا، لنوّلد نموذج العربة Vehicle الأساسي:

$ rails generate model vehicle type:string color:string price:decimal{10.2}

هل لاحظت أننا نضيف حقل type؟ هذا لأن كل النماذج ستحفظ في جدول وحيد، سيحفظ ريلز اسم النموذج المستخدم في هذا الحقل. في مثالنا، يمكن أن يكون هذا إمّا Car، أو Motorcycle، أو Bicycle. لن تعمل وراثة الجدول الوحيد دون الحقل type في الجدول الأساسي.

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

مثلًا، لتوليد نموذج السيارة Car:

$ rails generate model car --parent=Vehicle

سيبدو النموذج المولّد كالتالي:

class Car < Vehicle

end

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

إن إنشاء سيارة سيؤدي إلى حفظها في جدول العربات 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')