تهجيرات Active Record في ريلز

من موسوعة حسوب

التهجيرات (Migrations) هي ميزة من Active Record التي تمكّنك من تحديث مخطط قاعدة البيانات على مر الوقت. بدلًا من كتابة تعديلات قاعدة البيانات باستخدام SQL، تمكّنك التهجيرات من كتابة تعليمات DSL باستخدام روبي لتحديث جداولك. بعد قراءة هذا الدليل، ستتعرَّف على:

  • المولِّدات التي تستطيع استعمالها لإنشاء التهجيرات.
  • التوابع التي يوفرها Active Record لتعديل قاعدة البيانات.
  • المهام bin/rails التي تعدِّل وتتحكم بالتهجيرات والمخطط (schema) الخاص بك.
  • العلاقة بين التهجيرات والملف schema.rb.

نظرة عامة على التهجيرات

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

يمكنك اعتبار التهجيرات كإصدارات مختلفة من قاعدة البيانات؛ إذ يبدأ المخطط بلا شيء، وكل تهجير يضيف أو يحذف الجداول أو الأعمدة أو السجلات. يعرف Active Record متى وكيف يتوجب تحديث قاعدة البيانات في هذا المخطط الزمني، مما يمكّن نقل حالة قاعدة البيانات من أي نقطة لآخر إصدار ممكن. يحدِّث Active Record أيضًا الملف db/schema.rb لمطابقة البنية الأخيرة لقاعدة البيانات الخاصة بك.

إليك مثال بسيط عن التهجيرات:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps
    end
  end
end

يضيف هذا التهجير جدولًا يسمى products، يحتوي عمودًا من النوع string يسمى name، وعمودًا آخر من نوع النصي (text) يسمى description. يتم أيضًا إضافة مفتاح رئيسي يسمى id بشكل ضمني، لكونه المفتاح الرئيسي الافتراضي لكل نماذج Active Record. يضيف الماكرو timestamps عمودين هما: created_at و updated_at. يتم إدارة هذين العمودين تلقائيًا من قبل Active Record في حال وجودهما.

من الجدير بالذكر أننا قمنا بالتعريف عن التغيير الذي نريد وجوده بعد وقت. قبل تنفيذ هذا التهجير، لن يكون هناك جدول. وبعد تنفيذه، سيوجد الجدول. يعرف Active Record كيف تتم إعادة التهجير أيضًا: إذا أعدنا هذا التهجير، سيتم إزالة الجدول.

في قواعد البيانات التي تدعم عمليات النقل (transactions) مع التعليمات التي تحدث تغييرًا في مخطط قاعدة البيانات، يتم تغليف التهجيرات في عملية نقل. في حال كانت قاعدة البيانات لا تدعم عمليات النقل هذه، وعند فشل تنفيذ تهجير ما، لن يتم إعادة الأجزاء التي نجحت منه. إذ من الضروري إعادتها يدويًا.

ملاحظة: هناك بعض الاستعلامات (queries) التي لا يمكن تنفيذها ضمن عملية نقل. في حال كان محول قاعدة البيانات يدعم عمليات نقل DDL، يمكنك استخدام disable_ddl_transaction!‎ لإلغاء تفعيلها من أجل تهجير وحيد.

في حال أردت إحداث تغيير في التهجير لا يمكن لـ Active Record معرفة كيفية إعادته، يمكنك استخدام reversible:

class ChangeProductsPrice < ActiveRecord::Migration[5.0]
  def change
    reversible do |dir|
      change_table :products do |t|
        dir.up   { t.change :price, :string }
        dir.down { t.change :price, :integer }
      end
    end
  end
end

وبشكل بديل، يمكنك استخدام التوابع up و down بدلًا من change:

class ChangeProductsPrice < ActiveRecord::Migration[5.0]
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end
 
  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end

إنشاء تهجير

إنشاء تهجير وحيد (Standalone)

تخزن التهجيرات في ملفات ضمن المجلد db/migrate، واحد لكل صنف تهجير. يسمى ملف التهجير وفق النمط YYYYMMDDHHMMSS_create_products.rb، الذي يوافق توقيت غرينتش الذي عُرِّف في أثنائه التجيره يليه اسم التهجير مع الفصل بينهما (وبين الكلمات) بشرطة سفلية. يجب على اسم صنف التهجير (المسمى بطريقة سنام الجمل [CamelCase]) أن يوافق اسم ملف التهجير. مثلًا، الملف ‎20080906120000_create_products.rb يجب أن يعرِّف الصنف CreateProducts، والملف ‎20080906120001_add_details_to_products.rb يجب أن يعرِّف الصنف AddDetailsToProducts. يستخدم ريلز هذه التوقيتات الزمنية لمعرفة التهجيرات التي يجب تنفيذها والترتيب المراد تنفيذها به؛ لذا، في حال نسخ تهجير من تطبيق آخر، أو عند توليد تهجيرات جديدة، يجب الانتباه إلى ترتيب هذه التهجيرات.

بالطبع، ليس من السهل توليد وحساب التوقيتات الزمنية، لذلك يزودنا Active Record بمولد ينشئها لنا بسهولة:

$ bin/rails generate migration AddPartNumberToProducts

سينشئ هذا تهجيرًا فارغًا:

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
  end
end

في حال كان اسم التهجير بالشكل "AddXXXToYYY" أو "RemoveXXXFromYYY" واتبِع بقائمة بالأعمدة وأنواعها، فسيتم إنشاء تهجير يحتوي على التعابير add_column و remove_column بطريقة مناسبة. أي:

$ bin/rails generate migration AddPartNumberToProducts part_number:string

سيولّد:

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
  end
end

وفي حال أردت إضافة فهرس للعمود الجديد، يمكنك ذلك من خلال:

$ bin/rails generate migration AddPartNumberToProducts part_number:string:index

سيولّد:

class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
    add_index :products, :part_number
  end
end

وبطريقة مماثلة، يمكنك توليد تهجير يؤدي إلى حذف عمود وذلك كالتالي:

$ bin/rails generate migration RemovePartNumberFromProducts part_number:string

يولّد:

class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
  def change
    remove_column :products, :part_number, :string
  end
end

لست مجبرًا على عمود وحيد فقط. إليك مثلًا:

$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal

يولّد:

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :part_number, :string
    add_column :products, :price, :decimal
  end
end

في حال كان اسم التهجير بالشكل "CreateXXX"، وتبعه قائمةٌ بالأعمدة وأنواعها، فسيتم إنشاء تهجير ينشئ جدولًا يسمى XXX يحتوي على الأعمدة المحددة. مثلًا:

$ bin/rails generate migration CreateProducts name:string part_number:string

يولّد:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.string :part_number
    end
  end
end

وكما جرت العادة، ما تم توليده هو نقطة البداية فقط. يمكنك الإضافة أو الحذف من الملف المولد كما تريد عن طريق تعديل الملف db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb. أيضاً، يقبل المولّد أنواع الأعمدة كموارد مثل references (أو belongs_to متاح أيضًا). مثلًا:

$ bin/rails generate migration AddUserRefToProducts user:references

يولّد:

class AddUserRefToProducts < ActiveRecord::Migration[5.0]
  def change
    add_reference :products, :user, foreign_key: true
  end
end

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

$ bin/rails g migration CreateJoinTableCustomerProduct customer product

يولّد:

class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
  def change
    create_join_table :customers, :products do |t|
      # t.index [:customer_id, :product_id]
      # t.index [:product_id, :customer_id]
    end
  end
end

مولدات النماذج

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

$ bin/rails generate model Product name:string description:text

سيؤدي إلى إنشاء التهجير التالي:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps
    end
  end
end

يمكنك أيضًا إضافة المزيد من الأعمدة وأنواعها في حال أردت ذلك.

تمرير معدلات الأنواع

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

$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

إلى إنشاء التهجير التالي:

class AddDetailsToProducts < ActiveRecord::Migration[5.0]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true
  end
end

كتابة التهجيرات

بعد إنشاء تهجيراتك باستخدام أحد المولّدات، فقد حان الوقت للبدء بالعمل به واستخدامه!

إنشاء جدول

إن التابع create_table هي أحد أهم التوابع لكن في معظم الأوقات، سيتم توليد الجدول تلقائيًا باستخدام أحد المولدات. الاستخدام التقليدي سيكون كالتالي:

create_table :products do |t|
  t.string :name
end

الذي ينشئ الجدول products الذي يحوي عمودًا باسم name (وكما تم مناقشته أدناه، العمود id منشأ بشكل ضمني). افتراضيًا، سينشئ التابع create_table مفتاحًا رئيسيًا يدعى id. يمكنك تغيير اسم المفتاح الرئيسي باستخدام الخيار ‎:primary_key (لا تنسَ تعديل النموذج الموافق)، أو في حال لم ترد إنشاء مفتاح رئيسي إطلاقًا، يمكنك تمرير الخيار id: false. في حال أردت تمرير خيارات خاصة بقاعدة البيانات، يمكنك وضع قطع SQL في الخيار ‎:option. مثلًا:

create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

سيؤدي إلى إضافة ENGINE=BLACKHOLE إلى تعليمة SQL المستخدمة لإنشاء الجدول.

أيضًا، يمكنك تمرير الخيار ‎:comment مع أي شرح أو تعليق حول الجدول الذي سيتم تخزينه في قاعدة البيانات والذي يمكن عرضه في أدوات إدارة قواعد البيانات، مثل MySQL Workbench و PgAdmin III. من المستحسن تعريف التعليقات في التهجيرات للتطبيقات التي تملك قواعد بيانات كبيرة، الأمر الذي يساعد المطوّرين من فهم تحليل النظام وتوليد توثيقات. في الوقت الحالي، فقط محوّلات قواعد البيانات MySQL و PostgreSQL تدعم التعليقات.

إنشاء جدول رابط

التابع التهجيري create_join_table ينشئ الجدول HABTM (اختصار للعبارة has and belongs to many]) الرابط. يكون استخدامه كالتالي:

create_join_table :products, :categories

ينشئ هذا جدولًا يدعى categories_products بحقلين category_id و product_id. في هذه الحقول، يضبط الخيار ‎:null إلى القيمة false افتراضيًّا. يمكن تجاوز هذا عن طريق تحديد الخيار ‎:column_options.

create_join_table :products, :categories, column_options: { null: true }

بشكل افتراضي، يأتي اسم الجدول الرابط من أول وسيطين ممررين للتابع create_join_table وذلك بترتيب أبجدي. لتعديل اسم الجدول، قم بتعديل الخيار ‎:table_name:

create_join_table :products, :categories, table_name: :categorization

يقبل التابع create_join_table أيضًا كتلةً، والتي يمكنك استخدامها لإضافة الفهارس (التي لا يتم إنشاؤها بشكل افتراضي)، أو الحقول الإضافية:

create_join_table :products, :categories do |t|
  t.index :product_id
  t.index :category_id
end

تغيير الجداول

بشكل مقارب للتابع create_table، يمكن استخدام change_table من أجل تغيير الجداول الموجودة مسبقًا. يمكن استخدامه بشكل مشابه للتابع create_table، لكن الكائن المزود للكتلة يملك بعض المزايا الإضافية. مثلاً:

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

يزيل هذا التابع الحقول name و description، وينشئ حقلاً يدعى part_number ويضيف فهرسًا عليه. نهايةً، يعيد تسمية الحقل upccode.

تغيير الحقول

بشكل مشابه للتابع remove_column و add_column، يمكنك استخدام التابع change_column الذي توفره ريلز لتغيير حقل محدَّد:

change_column :products, :part_number, :text

يغيّر هذا التابع الحقل part_number من الجدول products ليصبح حقلًا من النوع النصي (text). لاحظ أن التغييرات التي تجرى عبر التابع change_column لا رجعة فيها (irreversible). إلى جانب change_column، يمكن استخدام التابعين change_column_null و change_column_default لتغيير القيد "not null" أو القيم الافتراضية من الحقول وذلك بشكل واضح.

change_column_null :products, :name, false
change_column_default :products, :approved, from: true, to: false

يغيّر هذا الحقل name من الجدول products ليصبح NOT NULL، والقيمة الافتراضية له تصبح false. ملاحظة: يمكنك أيضًا كتابة التغيير نفسه بالشكل:

change _column_default :products, :approved, false

لكن على عكس المثال السابق، سيجعل هذا التغيير من تهجيرك غير قابل للاستعادة.

معدّلات الحقول

يمكن تطبيق معدلات الحقول (Column modifiers) عند إنشاء أو تغيير حقل ما:

  • limit: يحدد الحجم الأعظمي للحقول التي من الأنواع string، أو text، أو binary، أو integer.
  • precision: يحدد دقة حقول الأعداد العشرية، إذ يمثل عدد الخانات الرقمية الكلية في العدد.
  • scale: يحدد دقة المنازل العشرية، إذ يمثل عدد الخانات العشرية بعد الفاصلة العشرية.
  • polymorphic: يضيف الحقل type إلى العلاقات من النوع belongs_to.
  • null: يسمح/لا يسمح بإضافة القيم NULL في الحقل.
  • default: يسمح بضبط القيمة الافتراضية في الحقل. تجب الملاحظة أنه عند استخدام قيمة ديناميكية (مثل قيمة تاريخ)، سيتم حساب القيمة الافتراضية في المرة الأولى فقط (عند تطبيق التهجير).
  • index: يضيف فهرسًا للحقل.
  • comment: يضيف تعليقًا للحقل.

قد تدعم بعض المحولات (adapters) المزيد من الخيارات؛ اطّلع على توثيق الواجهة البرمجية الخاص بالمحول من أجل المزيد من المعلومات.

ملاحظة: لا يمكن تحديد المعدلات null و default من سطر الأوامر.

المفاتيح الأجنبية

رغم أنَّ المفاتيح الأجنبية غير مطلوبة، يمكنك إضافتها لضمان السلامة المرجعية (referential integrity).

add_foreign_key :articles, :authors

يضيف هذا الأمر مفتاحًا أجنبيًا جديدًا للحقل author_id من الجدول articles؛ إذ يكون الحقل مرجعًا للحقل id من الجدول authors. في حال عدم قابلية اشتقاق أسماء الحقول من أسماء الجداول، يمكنك استخدام الخيارين ‎:column و ‎:primary_key.

يقوم ريلز بتوليد اسم من أجل كل مفتاح أجنبي يبدأ بالسابقة _fk_rails، ويليها 10 محارف يتم توليدها من اسم الجدول from_table والحقل column. يمكن تحديد اسم المفتاح الأجنبي من الخيار ‎:name.

ملاحظة: يدعم Active Record المفاتيح الأجنبية الوحيدة فقط. لاستخدام المفاتيح الأجنبية المركبة، يجب استخدام execute و structure.sql. اقرأ قسم تعديل المخططات في الأسفل.

يمكن أيضًا إزالة مفتاح أجنبي بسهولة:

# دع السجل الفعال يكتشف اسم الحقل
remove_foreign_key :accounts, :branches

# إزالة المفتاح الأجنبي لحقل معين
remove_foreign_key :accounts, column: :owner_id

# إزالة المفتاح الأجنبي باسم معين
remove_foreign_key :accounts, name: :special_fk_name

عند عدم كفاية التوابع المساعدة

في حال كانت التوابع المساعدة التي يزودها Active Record غير كافية، يمكنك استخدام التابع execute لتنفيذ تعليمات SQL:

Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")

للمزيد من المعلومات والأمثلة حول التوابع المفردة، يمكنك الاطلاع على توثيق الواجهة البرمجية. بالتحديد، توثيق ActiveRecord::ConnectionAdapters::SchemaStatements (الذي يزود التوابع المتاحة في التوابع change و up و down)، وتوثيق ActiveRecord::ConnectionAdapters::TableDefinition (الذي يزود التوابع المتاحة في الكائن المعاد بواسطة create_table)، وتوثيق ActiveRecord::ConnectionAdapters::Table (الذي يزود التوابع المتاحة في الكائن المعاد بواسطة change_table).

استخدام التابع change

إن التابع change هو الطريقة الأساسية لكتابة التهجيرات؛ إذ يعمل في معظم الحالات، التي يعلم فيها Active Record كيف يستعيد التهجيرات بشكل تلقائي. في الوقت الحالي، يدعم التابع change فقط تعريفات التهجيرات التالية:

  • add_column
  • add_foreign_key
  • add_index
  • add_reference
  • add_timestamps
  • change_column_default (يجب أن يُزوَّد بالخيارين ‎:from و ‎:to)
  • change_column_null
  • create_join_table
  • create_table
  • disable_extension
  • drop_join_table
  • drop_table (يجب أن يُزوَّد بكتلة)
  • enable_extension
  • remove_column (يجب أن يُزوَّد بنوع)
  • remove_foreign_key (يجب أن يُزوَّد بجدولٍ ثانٍ)
  • remove_index
  • remove_reference
  • remove_timestamps
  • rename_column
  • rename_index
  • rename_table

إن التابع change_table أيضًا قابل للاستعادة (reversible)، طالما لا يستدعي الهيكل أي من التوابع change و change_default و remove.

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

remove_column :posts, :slug, :string, null: false, default: '', index: true

في حال أردت استعمال أي من التوابع الأخرى، يجب عليه استخدام reversible لكتابة التوابع up و down، بدلًا من استخدام التابع change.

استخدام reversible

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

class ExampleMigration < ActiveRecord::Migration[5.0]
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end
 
    reversible do |dir|
      dir.up do
        # add a CHECK constraint
        execute <<-SQL
          ALTER TABLE distributors
            ADD CONSTRAINT zipchk
              CHECK (char_length(zipcode) = 5) NO INHERIT;
        SQL
      end
      dir.down do
        execute <<-SQL
          ALTER TABLE distributors
            DROP CONSTRAINT zipchk
        SQL
      end
    end
 
    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end
end

باستخدام reversible، يمكنك ضمان تشغيل التعليمات بالترتيب الصحيح أيضًا. في حال تمّت استعادة التهجير في المثال السابق، يتم تنفيذ الكتلة down بعد إزالة الحقل home_page_url وقبل حذف الجدول distributors.

في بعض الأوقات، تفعل التهجيرات أمورًا لا يمكن استعادتها؛ مثلًا، قد تحذف بعض البيانات. في هذه الحالات، يمكنك اطلاق الاستثناء ActiveRecord::IrreversibleMigration في الكتلة down. في حال أراد شخص ما استعادة تهجيرك، ستظهر رسالة خطأ تبلغه بعدم إمكانية ذلك.

استخدام التابعان up و down

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

class ExampleMigration < ActiveRecord::Migration[5.0]
  def up
    create_table :distributors do |t|
      t.string :zipcode
    end
 
    # add a CHECK constraint
    execute <<-SQL
      ALTER TABLE distributors
        ADD CONSTRAINT zipchk
        CHECK (char_length(zipcode) = 5);
    SQL
 
    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end
 
  def down
    rename_column :users, :email_address, :email
    remove_column :users, :home_page_url
 
    execute <<-SQL
      ALTER TABLE distributors
        DROP CONSTRAINT zipchk
    SQL
 
    drop_table :distributors
  end
end

في حال كان تهجيرك غير قابل للاستعادة، يجب عليك اطلاق الاستثناء ActiveRecord::IrreversibleMigration في التابع down؛ إذ في حال أراد شخص ما استعادة تهجيرك، ستظهر رسالة خطأ تبلغه بعدم إمكانية ذلك.

استعادة التهجيرات القديمة

يمكنك استخدام قدرات Active Record لاستعادة التهجيرات باستخدام التابع revert:

require_relative '20121212123456_example_migration'
 
class FixupExampleMigration < ActiveRecord::Migration[5.0]
  def change
    revert ExampleMigration
 
    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

يقبل التابع revert أيضًا هيكلًا من التعليمات لاستعادتها. يفيد ذلك في حال أردت استعادة أجزاء من تهجيرات سابقة. مثلًا، لنفترض أنّه تم تنفيذ التهجير ExampleMigration لكن اكتشف لاحقًا أنّه من المفضّل استخدام عمليات التحقق من Active Record بدلًا من القيد CHECK لتأكيد الرمز البريدي (zipcode):

class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
  def change
    revert do
      # copy-pasted code from ExampleMigration
      reversible do |dir|
        dir.up do
          # add a CHECK constraint
          execute <<-SQL
            ALTER TABLE distributors
              ADD CONSTRAINT zipchk
                CHECK (char_length(zipcode) = 5);
          SQL
        end
        dir.down do
          execute <<-SQL
            ALTER TABLE distributors
              DROP CONSTRAINT zipchk
          SQL
        end
      end
 
      # بقية التجهير لا غبار عليه
    end
  end
end

كان من الممكن كتابة التهجير نفسه دون استخدام التابع revert، لكن كان ليتضمّن ذلك بعض الخطوات الإضافية: عكس ترتيب استدعاء create_table و reversible، وتبديل create_table بـ drop_table، وأخيرًا تبديل up بـ down والعكس صحيح. هذا الأمر مُعالج بأكمله في التابع revert.

ملاحظة: في حال أردت إضافة القيد CHECK كما في المثال السابق، يجب عليك استخدام الملف structure.sql مثل التابع dump. اقرأ قسم تعديل المخططات في الأسفل.

تنفيذ التهجيرات

يزوّد ريلز بمجموعة من مهام bin/rails لتنفيذ مجموعة محددة من التهجيرات.

من المرجَّح أن تكون المهمة الأولى المتعلقة بالتهجيرات والتي ستستخدمها هي rails db:migrate. في شكلها الأكثر بساطة، تشغّل هذه المهمة التوابع change أو up للتهجيرات التي لم يتم تشغيلها بعد. في حال لم يكن هناك تهجيرات لم تُشغّل بعد، تقوم هذه المهمة بالإنهاء. تنفّذ هذه المهمة التهجيرات بالترتيب حسب تاريخ التهجير.

من الجدير بالذكر أن تنفيذ المهمة db:migrate ينفّذ بدوره المهمة db:schema:dump، التي تحدّث الملف db/schema.rb ليناسب المخطط الحالي لقاعدة البيانات.

في حال حدّدت إصدارًا معيّنًا، يقوم Active Record بتنفيذ التهجيرات المطلوبة (change و up و down) حتّى يصل للإصدار المعطى. يُحدد الإصدار بالسابقة الرقمية في اسم ملف التهجير. مثلًا، للتهجير حتّى الإصدار 20080906120000، نفذ الأمر التالي:

$ bin/rails db:migrate VERSION=20080906120000

في حال كان الإصدار 20080906120000 أكبر من الإصدار الحالي (أي يتم التهجير للأعلى)، تُنفّذ التوابع change (أو up) على كل التهجيرات وصولًا إلى وبما فيها التهجير 20080906120000 ولن تنفذ بعد ذلك أية تهجيرات لاحقة. في حال كان التهجير للأسفل، يتم تنفيذ التوابع down على كل التهجيرات تنازليًّا حتى الوصول إلى ودون تضمين التهجير 20080906120000.

الاستعادة

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

$ bin/rails db:rollback

يستعيد هذا الأمر آخر تهجير تمّ تنفيذه، إمّا بعكس التابع change، أو بتنفيذ التابع down. في حال أردت استعادة مجموعة من التهجيرات، يمكنك تمرير المعامل STEP:

$ bin/rails db:rollback STEP=3

يستعيد ذلك آخر ثلاثة تهجيرات. المهمة db:migrate:redo هو اختصار للقيام باستعادة التهجيرات وتنفيذها مجددًا. وكما هو الحال في المهمة db:rollback، يمكنك استخدام المعامل STEP لتنفيذه على أكثر من تهجير، مثلًا:

$ bin/rails db:migrate:redo STEP=3

لا تقوم أي من المهمّات bin/rails هذه بأي شيء لا يمكنك فعله مع المهمة db:migrate. ببساطة، هي أكثر أريحية، بما أنّك لست بحاجة لتحديد إصدار التهجير الذي تريد العودة له.

تهيئة قاعدة البيانات

تقوم المهمة rails db:setup بإنشاء قاعدة البيانات، وتحميل المخطط وتهيئته بالبيانات اللازمة.

إعادة تهيئة قاعدة البيانات

تقوم المهمة rails db:reset بحذف قاعدة البيانات وتهيئتها من جديد. تكافئ هذه المهمّة الأمر rails db:drop db:setup.

ملاحظة: لا يكافئ ذلك تنفيذ كل التهجيرات؛ إذ يستخدم فقط الملف db/schema.rb أو الملف db/structure.sql. في حال عدم إمكانية استعادة تهجير ما، قد لا تساعدك المهمة db:reset. لقراءة المزيد حول حذف المخطط، اقرأ قسم تحديث المخططات في الأسفل.

تنفيذ تهجيرات محددة

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

$ bin/rails db:migrate:up VERSION=20080906120000

يُنفّذ التهجير 20080906120000 عن طريق تنفيذ التابع change (أو التابع up). تقوم هذه المهمة أولًا بفحص فيما إذا كان التهجير هذا مطبّق مسبقًا على قاعدة البيانات، ولن تقوم بأي شيء في حال معرفة Active Record أنّ التهجير قد تم تطبيقه مسبقًا.

تنفيذ التهجيرات في بيئات مختلفة

افتراضيًا، تنفّذ المهمة bin/rails db:migrate بالبيئة development. لتنفيذ التهجيرات على بيئة مختلفة، يمكنك تحديدها في متغير البيئة RAILS_ENV عند تشغيل الأمر. مثلًا، لتنفيذ تهجير ما على بيئة الاختبار (test)، يمكنك تشغيل:

$ bin/rails db:migrate RAILS_ENV=test

تغيير خرج تنفيذ التهجيرات

افتراضيًا، تخبرك التهجيرات بما تفعله حاليًا، وكم من الوقت استغرق تنفيذها؛ ففي حال كان لدينا تهجير ينشئ جدولًا ويضيف فهرسًا عليه، يبدو خرج تنفيذه كالتالي:

==  CreateProducts: migrating =================================================

-- create_table(:products)

  -> 0.0028s

==  CreateProducts: migrated (0.0028s) ========================================

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

التابع الغرض
suppress_messages يأخذ كتلةً كوسيط له، ويزيل أي خرج يتم توليده من الكتلة.
say يأخذ رسالة كوسيط له، ويظهرها كما هي. يمكن تمرير وسيط منطقيٍّ (boolean) ثانٍ لتحديد ضرورة وضع الفراغات قبل الكلام.
say_with_time يظهر نصًا، وكم من الوقت استغرق تنفيذ الكتلة. في حال أعادت الكتلة عددًا صحيحًا، يفترض التابع أنّه يمثّل عدد الأسطر المتأثرة.

مثلًا، ألقِ نظرة فاحصة على هذا التهجير:

class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    suppress_messages do
      create_table :products do |t|
        t.string :name
        t.text :description
        t.timestamps
      end
    end
 
    say "Created a table"
 
    suppress_messages {add_index :products, :name}
    say "and an index!", true
 
    say_with_time 'Waiting for a while' do
      sleep 10
      250
    end
  end
end

يولّد الخرج التالي:

==  CreateProducts: migrating =================================================
-- Created a table
   -> and an index!
-- Waiting for a while
   -> 10.0013s
   -> 250 rows
==  CreateProducts: migrated (10.0054s) =======================================

في حال أردت من Active Record ألّا يطبع شيئًا، يمكنك استخدام rails db:migrate VERBOSE=false لإزالة أي مخرجات.

تغيير التهجيرات الموجودة

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

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

يمكن للتابع revert أن يفيدك في هذه الحالة، عند كتابة تهجير جديد يلغي تهجيرًا قديمًا ككل أو كجزء.

تعديل المخططات

لمَ نستخدم ملفات المخططات؟

لا يمكن اعتبار التهجيرات - رغم متانتها - مصدرًا موثوقًا لمخطط (schema) قاعدة البيانات، إذ يقع هذا الدور على الملف db/schema.rb أو ملف SQL الذي يولده Active Record عن طريق تحليل قاعدة البيانات. لم تصمّم هذه الملفات ليتم تعديلها، إذ تمثّل حالة قاعدة البيانات الحالية.

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

مثلًأ، هكذا يتم إنشاء قاعدة بيانات الاختبار: يتم تنزيل قاعدة بيانات التطوير (إمّا إلى db/schema.rb أو db/strucutre.sql)، ومن ثم تحميلها على قاعدة بيانات الاختبار.

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

أنواع عمليات تنزيل المخططات

هناك طريقتين لتنزيل المخطط. يحدّد ذلك في الملف config/application.rb عن طريق الإعداد config.active_record_schema_format، إذ يكون إمّا sql: أو ruby:.

في حال تمّ اختيار ruby:، ستجد المخطط في db/schema.rb. إذا اطلعت على الملف، ستجده مماثلًا لتهجير كبير جدًا:

ActiveRecord::Schema.define(version: 20080906171750) do
  create_table "authors", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end
 
  create_table "products", force: true do |t|
    t.string   "name"
    t.text     "description"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "part_number"
  end
end

يتم إنشاء هذا الملف عن طريق تحليل قاعدة البيانات والتعبير عن بنيتها باستخدام create_table و add_index والمزيد. باعتبار هذا الملف مستقل عن قاعدة البيانات، يمكن تنفيذه على أي قاعدة بيانات يدعمها Active Record. من الضروري القيام بذلك عند العمل مع تطبيق يقوم بتشغيل مجموعة من قواعد البيانات.

ملاحظة: لا يمكن للملف db/schema.rb التعبير عن الأمور المتعلقة بقاعدة البيانات مثل القوادح (triggers)، والسلاسل (sequences)، والإجراءات المخزنة (stored procedures) أو القيود (check)، ...إلخ. تجدر الملاحظة أنّه في حين يمكن تنفيذ تعليمات SQL في التهجيرات، إلّا أنّه لا يمكن إعادة كتابة ذلك عند تنزيل المخطط. في حال استخدمت إحدى هذه الميّزات، يجب عليك تعيين نمط المخطط الحالي إلى sql:.

بدلًا من استخدام تنزيل المخططات الخاص بـ Active Record، يمكنك تنزيل مخطط قاعدة البيانات باستخدام أداة مناسبة لقاعدة البيانات الحالية (عن طريق المهمة db:structure:dump) إلى الملف db/structure.sql. مثلًا، من أجل قاعدة بيانات من النوع PostgreSQL، يمكنك استخدام الأداة pg_dump. ومن أجل MySQL و MariaDB، سيحتوي هذا الملف على خرج SHOW CREATE TABLE من أجل مجموعة من الجداول.

إن تحميل هذه المخططات يعني ببساطة تنفيذ تعليمات SQL التي تحويها. باستخدام النمط sql:، لن تتمكّن من تحميل المخطط على قاعدة بيانات من نوع مختلف عن النوع المستخدم لتنزيل ملف المخطط.

ملفات المخططات والتحكم بالمصدر

بما أنّ ملفات المخططات هي المصدر الأساسي لمخطط قاعدة البيانات، من المستحسن جدًا تسليمها لنظام التحكم بالمصدر الخاص بك.

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

Active Record والسلامة المرجعية

يدّعي Active Record أنّ العمليّات المعقّدة والمنطقية يجب أن تكون جزءًا من طبقة النماذج، وليس من قاعدة البيانات. لذلك، فإنّ الميزات مثل القوادح (triggers) والقيود (constraints) والتي تضفي المنطقية إلى قاعدة بياناتك ليست مستخدمة بكثرة.

التحققات من الصحة مثل validates :foreign_key, uniqueness: true هي طريقة من الطرق المستخدمة لتضمن نماذجك تكامل البيانات (data integrity). يسمح الخيار dependent: للارتباطات من حذف الكائنات الأبناء لنموذج ما عند حذف الكائن الأب لها. كما هو الحال مع أي شيء يُشغّل على مستوى التطبيق، لا يمكن لهذه الأوامر أن تضمن السلامة المرجعية (referential integrity)، لذلك يقوم بعض المبرمجون بإضافة قيود المفاتيح الأجنبية لقاعدة البيانات.

رغم أن Active Record لا يوفر هذه الإمكانية، إلّا أن التابع execute يمكن استخدامه لتنفيذ تعليمات SQL وتحقيق المطلوب.

التهجيرات وبيانات البذور

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

class AddInitialProducts < ActiveRecord::Migration[5.0]
  def up
    5.times do |i|
      Product.create(name: "Product ##{i}", description: "A product.")
    end
  end
 
  def down
    Product.delete_all
  end
end

لإضافة بيانات ابتدائية بعد إنشاء قاعدة البيانات، يملك ريلز ميزة "البذور" (seeds) التي تجعل الأمر سهلًا وسريعًا. يفيدك ذلك بشكل خاص عند إعادة تحميل قاعدة البيانات بشكل متكرر في بيئات التطوير والاختبار. من السهل البدء بهذه الميزة: فقط املأ الملف db/seeds.rb بشيفرة روبي، ثم شغّل rails db:seed.

5.times do |i|
  Product.create(name: "Product ##{i}", description: "A product.")
end

هذه الطريقة أفضل بكثير لتهيئة قاعدة بيانات تطبيق جديد.

مصادر