اذهب إلى المحتوى

Active Record Associations: نصائح، خدع وتحذيرات


هشام رزق الله

تعرفنا في الدرس السابق على الارتباطات وأنواعها وسنتابع في هذا الدرس أهم النصائح والخدع والتحذيرات.

3 نصائح، خدع وتحذيرات

يجب عليك معرفة هذه الأشياء لتستخدم ارتباطات Active Record بشكل أفضل وأكفأ في تطبيقات Rails:

  • التحكم في التخزين المؤقت.
  • تجنب تضارب الأسماء.
  • تحديث المخطط.
  • التحكم في نطاق (scope) الارتباط.
  • الارتباطات ثنائية الاتجاه.

3.1 التحكم في التخزين المؤقت

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

author.books                 # retrieves books from the database
author.books.size            # uses the cached copy of books
author.books.empty?          # uses the cached copy of books

لكن ماذا لو أردنا إعادة تحميل التخزين المؤقت، لأن البيانات قد تتغير عن طريق أجزاء اخرى من التطبيق؟ فقط أدعو إعادة التحميل في الارتباط:

author.books                 # retrieves books from the database
author.books.size            # uses the cached copy of books
author.books.reload.empty?   # discards the cached copy of books
                             # and goes back to the database

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

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

3.3 تحديث المخطط

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

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

عندما تعلن عن ارتباط belongs_to، ستحتاج إلى إنشاء مفاتيح خارجية حسب الحاجة، فعلى سبيل المثال، فكر في هذا النموذج:

class Book < ApplicationRecord
  belongs_to :author
end

يحتاج هذا الإعلان إلى أن يُدعم عن طريق مفتاح خارجي مناسب مُعلن في جدول books:

class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.datetime :published_at
      t.string   :book_number
      t.integer  :author_id
    end
  end
end

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

class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.datetime :published_at
      t.string   :book_number
      t.integer  :author_id
    end
 
    add_index :books, :author_id
    add_foreign_key :books, :authors
  end
end

3.3.2 إنشاء جداول الضم لارتباطات has_and_belongs_to_many

إذا أنشئت ارتباط has_and_belongs_to_many، فستحتاج إلى إنشاء جدول الضم (joining table)، إذا لم يحدد اسم جدول الضم بشكل صريح عن طريق خيار join_table:، فسينشئ Active Record الاسم عن طريق استخدام الكتاب المعجمي لأسماء الصنف، فالضم بين نماذج المؤلف والكتاب سيُكوّن اسم جدول الضم بشكل افتراضي هو “authors_books”، لأن A تأتي قبل B في الترتيب المعجمي.

Quote

تنبيه:
تُحسب الأسبقية بين أسماء النماذج باستخدام العامل <=> للسلاسل النصية، وهذا يعني إذا كانت السلاسل النصية ذات أطوال مختلفة، وكانت كلا السلسلتين متساويتين مقارنة بالسلسلة الأقصر، فإن السلسلة النصية الأطول ذا أسبقية أعلى من السلسلة الأقصر، فعلى سبيل المثال، قد تملك جدول “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

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

3.4 التحكم في نطاق scope الارتباط

تبحث الارتباطات افتراضيا عن الكائنات ضمن نطاق الوحدة (module) الحالية، وهذا جيد عندما تعلن عن نماذج Active Record ضمن الوحدة، فعلى سبيل المثال:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
       has_one :account
    end
 
    class Account < ApplicationRecord
       belongs_to :supplier
    end
  end
end

وهذا سيعمل بشكل صحيح، لأن كل من صنف المُورّد والحساب تم تعريفهما داخل نفس النطاق، لكن الشيفرة البرمجية التالية لن تعمل، لأنه عُرّف المُورّد والحساب في نطاقات مختلفة:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
       has_one :account
    end
  end
 
  module Billing
    class Account < ApplicationRecord
       belongs_to :supplier
    end
  end
end

لربط نموذج مع نموذج في مساحة اسم (namespace) مختلفة يجب عليك تحديد اسم الصنف كاملا في إعلان الارتباط:

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

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

من الطبيعي أن تعمل الارتباطات في كلا الاتجاهين، وهذا الأمر يتطلب إعلان في نموذجين مختلفين:

class Author < ApplicationRecord
  has_many :books
end
 
class Book < ApplicationRecord
  belongs_to :author
end

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

  • conditions:
  • through:
  • polymorphic:
  • class_name:
  • 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

من خلال تضمين خيار :inverse_of في إعلان ارتباط 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

توجد بعض القيود على دعم inverse_of: :

  • لا تعمل مع ارتباطات through:
  • لا تعمل مع ارتباطات polymorphic:
  • لا تعمل مع ارتباطات as:

وسنتابع في الدرس القادم التعرّف على مرجع الارتباط المفصّل.

المصدر:
توثيقات Ruby on Rails


تفاعل الأعضاء

أفضل التعليقات

بتاريخ On 2/27/2022 at 10:30 قال Smaily:

في الارتباطات ثنائية 
 

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

لماذا النتيجة الأخيرة كانت false

في هذا المثال لم يُعرّف Active Record الارتباطات ثنائية الاتجاه تلقائيا لأنها احتوت على: class_name و foreign_key و بالتالي في هذه الحالة لن يُحمل Active Record نُسخة واحدة من الكائن Author أي أن:

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

a و b.writer هنا ليسا نفس الكائن. حيث أن Active Record قام بتحميل نُسختين في البداية كانت قيمة الخاصية first_name مُتساوية في كلتا النُسختين، أما بعد تغيير القيمة للكائن a أصبح هناك إختلاف لذلك المُقارنة أعطت 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

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

هنا أعطت المُقارنة true حتى بعد تغيير القيمة first_name لـِ a لأن Active Record تعرفت على الإرتباط ثنائي الإتجاه و أن a و b.writer هما نفس الكائن.

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...