ارتباطات 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
ملاحظة: يجب على ارتباطات الانتماء أن يستخدم نمط الكلمات الفردية. في حال استخدمت نمط الجمع في الكلمات في المثال السابق، فسيتم إخبارك أنَّ هناك ثابت غير معرّف مسمّى 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
ملاحظة: إن اسم النموذج الآخر يكون بنمط الجمع عند تعريف ارتباط التعددية.
قد يبدو التهجير الموافق كالتالي:
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')