أولاً ، المقالة ليست حول كيف أحب القضبان ، وثانيًا ، لا تتعلق المقالة بكيفية كرهها. يمكن معالجتها بطرق مختلفة تمامًا وسوف تصبح أفضل فقط إذا قمت بتغييرها. ويمكن أن تصبح أسوأ فقط إذا بدأوا في التغيير. حسنًا ، بشكل عام ، لقد حذرتك ، لكنك فهمتني.
أحد المفاهيم الرئيسية لـ ActiveRecord هو أن قاعدة البيانات مفيدة جدًا ويمكن تغييرها. حسنًا ، أنت تجلس هناك وتكتب نماذجك باستخدام MySQL ، وفجأة تقرأ في مكان ما يمكنك أن تأخذ MySQL بدلاً منه وتستبدله بـ MongoDB. حسنًا ، ليس جذريًا جدًا ، ولكن ، على سبيل المثال ، في PostgreSQL ، قد يكون لديك أسباب لاستبدال MySQL. أو العكس ، ليس لدي شيء ضد MySQL. هنا يدعي ActiveRecord أنه يسهل عليك ، من المفترض أن تكون النطاقات ، قبل / بعد عوامل التصفية والارتباطات مجردة بما يكفي بحيث لا تقلق بشأن إنشاء استعلامات قاعدة البيانات والعناية بمنطق التطبيق. هذا بدلاً من WHERE is_archived = TRUE
الكتابة where(is_archived: true)
و ActiveRecord سيفعلون كل شيء من أجلك. سيتم ترجمة جميع الأمثلة لـ PostgreSQL ، وليس لـ MySQL ، لذا سيتعين على مستخدمي MySQL اختراع دراجتهم الخاصة.

ولكن مهما! من الناحية العملية ، اتضح أن هذه الطبقة من التجريد مليئة بالكامل بالثقوب ، مثل حوض من حكاية السمكة الذهبية. ولا يمكن استخدام العديد من الميزات الأساسية ، مثل مقارنة التواريخ أو العمل مع المصفوفات. وستحصل على نطاقات تحتوي على where("#{quoted_table_name}.finished_at >= ?", Date.current)
قسري where("#{quoted_table_name}.finished_at >= ?", Date.current)
أو where("#{quoted_table_name}.other_ids <@ ARRAY[?]", ids)
. الذي يعطي ActiveRecord إجابة منطقية تمامًا: لا تستخدمه. بدلاً من المصفوفات ، استخدم اتصال habtm ، وإذا كنت بحاجة إلى مقارنة التواريخ ، فعيش معها. نعم ، والله quoted_table_name
أن تفوت quoted_table_name
في مثل هذا النطاق - الأول includes
أو joins
سيضع كل شيء في مكانه. من الأسهل في كل مكان ودائماً الكتابة ، حتى لا تضرب يدك.
حسنًا ، بالطبع ، بمجرد أن تقرر التدخل في ActiveRecord هنا ، فلن يكون هناك عودة إلى الوراء. لا يعني ذلك أنه لن تكون هناك فرصة ، حتى أمل شبحي لانتقال غير مؤلم إلى قاعدة بيانات أخرى. سيكون من الأسهل بكثير طباعة وحرق كود المصدر هذا. وبالطبع ، لا يوجد سبب آخر لعدم استخدام ميزات قاعدة البيانات الإضافية في تطبيقك. استخدم على الصحة وإجبار الآخرين!
وعندما يتبين أن نطاقاتك في مجلد النماذج تتكون من أكثر من نصف هذه الميزات الإضافية ، فمن الواضح تمامًا أن ActiveRecord هو مجرد غلاف مناسب لدمج جزء واحد من التعليمات البرمجية مع التسميات مع جزء آخر من التعليمات البرمجية. والنطاقات ، مثل where(is_archived: true).joins(:sprint).merge(Sprint.archived)
، ستعمل بشكل جيد والجمع بينها لن يكون أكثر صعوبة من صنع بيض مخفوق ، أليس كذلك؟

المرحلة التالية ستكون عدم التطبيع. لا ، لم يكن الاختفاء ، كما كان ، دائمًا يختفي ، ولكن الاهتمام به كان على أكتاف Rails و ActiveRecord القوية ، وأنت تعلم أن هذين الرجلين لم يختلفا في السرعة والزهد في متطلبات الموارد. دعنا نقول counter_cache: true
هو الخطوة الأولى نحو COUNT(*) AS sprints_count
، لأن COUNT(*) AS sprints_count
لن تسمح لك COUNT(*) AS sprints_count
ActiveRecord (لا تريد تغيير select()
، أليس كذلك؟). counter_cache
التخزين المؤقت counter_cache
ليست بعيدة عن الكمال ، وفي بعض الحالات قد يكون هناك عدم تزامن للمبلغ الحقيقي من التخزين المؤقت. غير ناقد ، بالطبع ، لكنه غير سار. وهذا هو أول مرشح يستقر في قاعدة البيانات وليس تحميل الرأس المحمّل بالفعل لآلة الياقوت. فقط بضعة محفزات وانتهيت! أولاً ، عند حذف سجل جديد وإضافته إلى اللوحة A ، تحتاج إلى حساب عدد السجلات في اللوحة B وهذا كل شيء ، أليس كذلك؟ حسنًا ، عند التحرير ، بالطبع ، إذا foreign_key
تغيير foreign_key
، لأن طلب UPDATE B SET a_id = $1 WHERE id = $2
سيكسر UPDATE B SET a_id = $1 WHERE id = $2
لكل من A القديم والجديد.
CREATE OR REPLACE FUNCTION update_#{parent_table}_#{child_table}_counter_on_insert() RETURNS TRIGGER AS $$ BEGIN UPDATE #{parent_table} SET #{counter_column} = COALESCE((SELECT COUNT(id) FROM #{child_table} GROUP BY #{foreign_column} HAVING #{foreign_column} = NEW.#{foreign_column}), 0) WHERE (#{parent_table}.id = NEW.#{foreign_column}); RETURN NULL; END; $$ LANGUAGE plpgsql;
سيرتبط مسار قاعدة البيانات التالي بوقت التاريخ. وللمبتدئين ، دعنا فقط نخدم حقلي updated_at
في قاعدة البيانات ، لحسن الحظ ، هذا أسهل بكثير. افتراضيات المجموعة الأولى:
change_column_default :table_name, :created_at, -> { 'CURRENT_TIMESTAMP' } change_column_default :table_name, :updated_at, -> { 'CURRENT_TIMESTAMP' }
ومن أجل القيام بذلك على الفور في كل مكان ، يمكنك تنظيم دورة لجميع الأطباق حيث توجد هذه الحقول. بالإضافة إلى schema_migrations
ar_internal_metadata
، بالطبع:
(tables -
هذا كل شيء ، الآن القيمة الافتراضية لهذه الجداول ستكون بالضبط ما نحتاجه. والآن حان الوقت للتأكد من أن القضبان لا تلمس هذه الحقول. يتم ذلك بمسمارين في المكان الصحيح. ونعم ، هناك خيار في إنشاء الإطار المسؤول عن ذلك:
Rails.application.config.active_record.record_timestamps = false
لذا ، فإن الخطوة التالية هي تحديث حقل updated_at
في وقت تحديث السجل. إنها بسيطة:
CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_update() RETURNS TRIGGER AS $$ BEGIN SELECT CURRENT_TIMESTAMP INTO NEW.updated_at; RETURN NEW; END; $$ LANGUAGE plpgsql;
الآن أنت بحاجة للتخلص تمامًا من touch: true
في النماذج. هذا الشيء يشبه إلى حد كبير الهدف في اندفاعة - أيضًا هولي تمامًا. ولن أشرح لماذا ، لأنك تعرف بالفعل كل هذه الحالات. هذا ليس أكثر تعقيدًا ، كل ما تحتاجه هو تحديث updated_at ليس فقط لنفسك:
CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_update() RETURNS TRIGGER AS $$ BEGIN UPDATE foreign_table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.foreign_column_name; SELECT CURRENT_TIMESTAMP INTO NEW.updated_at; RETURN NEW; END; $$ LANGUAGE plpgsql;
بالطبع ، ستقوم سلسلة مكالمات هذه المحفزات بعمل غير ضروري ، ولكن في postgres من آلية عاقل ، يتم استدعاء المشغلات دون تغيير السجل نفسه. يمكنك محاولة القيام بـ SET title = title
، ولكنها لا تأتي أفضل من SET updated_at = CURRENT_TIMESTAMP
.
سيتم تشغيل المشغل نفسه بالضبط على الإدراج ، فقط تحديث updated_at
ليس ضروريًا:
CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_insert() RETURNS TRIGGER AS $$ BEGIN UPDATE foreign_table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.foreign_column_name; RETURN NEW; END; $$ LANGUAGE plpgsql;
بالطبع ، يمكنك محاولة كتابة هذا بوظيفة واحدة ، وإضافة شيك للحدث الحالي مباشرة داخل المشغل ، على غرار IF TG_OP = 'UPDATE' THEN
، ولكن من الأفضل جعل جميع المشغلات بسيطة بقدر الإمكان لتقليل احتمالية حدوث خطأ.
قد ترغب في أتمتة إنشاء مثل هذه المحفزات بطريقة أو بأخرى ، ومن ثم قد تحتاج إلى العثور على جميع العلاقات الخارجية بين الجدول الحالي والباقي. يمكنك القيام بذلك بسهولة باستخدام هذا الاستعلام:
SELECT ccu.table_name AS foreign_table_name, kcu.column_name AS column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '#{table_name}' ORDER BY ccu.table_name;
نصيحة أخرى مفيدة للغاية. مشغلات الاتصال بنفس الطريقة لتتمكن من التحقق من وجود أو عدم وجود ما يحتاجه طلب واحد ، على سبيل المثال ، سيجد هذا الطلب جميع مشغلات إدراج اللمس:
SELECT routine_name AS name FROM information_schema.routines WHERE routine_name LIKE 'touch_for_%_on_insert' AND routine_type ='FUNCTION' AND specific_schema='public';
وأخيرا ، تبقى الأسوأ. والحقيقة هي أن القضبان ليست مصممة لنوع ما على الأقل من قاعدة البيانات الذكية ، ولم يهتموا حقًا بأن شيئًا آخر على الأقل غير حقل id
يمكن أن يتغير في قاعدة البيانات ، وذلك فقط عند إدراجه. لذلك ، لا توجد آلية سليمة لإضافة RETURNING updated_at
لتحديث الطلبات ، وليس هناك أي ، ستحتاج إلى الغوص داخل Rails إلى الأذنين.

تبين أن Mankipatch لم يكن أنيقًا جدًا ، ولكن الهدف أولاً كان إتلاف العمل الحالي للإطار بأقل قدر ممكن.
سأحضره بالكامل module ActiveRecord module Persistence # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L729-L741 def _create_record(attribute_names = self.attribute_names) attribute_names &= self.class.column_names attributes_values = attributes_with_values_for_create(attribute_names) an_id, *affected_rows = self.class._insert_record(attributes_values).dup self.id ||= an_id if self.class.primary_key Hash[ApplicationRecord.custom_returning_columns(self.class.quoted_table_name, :create).take(affected_rows.size).zip(affected_rows)].each do |column_name, value| public_send("#{column_name}=", self.class.attribute_types[column_name.to_s].deserialize(value)) if value end @new_record = false yield(self) if block_given? id end private :_create_record # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L710-L725 def _update_record(attribute_names = self.attribute_names) attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? affected_rows = [] @_trigger_update_callback = true else affected_rows = _update_row(attribute_names) @_trigger_update_callback = affected_rows.any? end Hash[ApplicationRecord.custom_returning_columns(self.class.quoted_table_name, :update).take(affected_rows.size).zip(affected_rows)].each do |column_name, value| public_send("#{column_name}=", self.class.attribute_types[column_name.to_s].deserialize(value)) end yield(self) if block_given? affected_rows.none? ? 0 : 1 end private :_update_record end module ConnectionAdapters module PostgreSQL module DatabaseStatements # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L93-L96 def exec_update(sql, name = nil, binds = []) execute_and_clear(sql_with_returning(sql), name, binds) { |result| Array.wrap(result.values.first) } end # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L147-L152 def insert(arel, name = nil, pk = nil, _id_value = nil, sequence_name = nil, binds = []) sql, binds = to_sql_and_binds(arel, binds) exec_insert(sql, name, binds, pk, sequence_name).rows.first end alias create insert # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L98-L111 def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: table_ref = extract_table_ref_from_insert_sql(sql) if pk.nil? # Extract the table from the insert sql. Yuck. pk = primary_key(table_ref) if table_ref end returning_columns = quote_returning_column_names(table_ref, pk, :create) if returning_columns.any? sql = "#{sql} RETURNING #{returning_columns.join(', ')}" end super end # No source in original repo def quote_returning_column_names(table_ref, pk, action) returning_columns = [] returning_columns << pk if suppress_composite_primary_key(pk) returning_columns += ApplicationRecord.custom_returning_columns(table_ref, action) returning_columns.map { |column| quote_column_name(column) } end # No source in original repo def sql_with_returning(sql) table_ref = extract_table_ref_from_update_sql(sql) returning_columns = quote_returning_column_names(table_ref, nil, :update) return sql if returning_columns.blank? "#{sql} RETURNING #{returning_columns.join(', ')}" end # No source in original repo def extract_table_ref_from_update_sql(sql) sql[/update\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*set/im] Regexp.last_match(1)&.strip end end end end end
الشيء الأكثر أهمية هو أننا ننتقل هنا إلى ApplicationRecord.custom_returning_columns
لمعرفة الأعمدة بالإضافة إلى المعرّف التي تهمنا. وهذه الطريقة تبدو مثل هذا:
class << self def custom_returning_columns(table_ref, action) return [] if ['"schema_migrations"', '"ar_internal_metadata"'].include?(table_ref) res = [] res << :created_at if action == :create res << :updated_at res += case table_ref when '"user_applications"' [:api_token] when '"users"' [:session_salt, :password_changed_at] # ... else [] end res end end
بدلا من الاستنتاجات ، يمكننا أن نقول أن رأس التهاب القضبان أصبح أقل قرحة. العمليات counter_cache
مثل counter_cache
و touch
ستغرق في النسيان ، وفي المقالة التالية يمكننا التفكير في شيء أكثر عالمية ، مثل إزالة المساحات المعلقة ، التحقق من صحة البيانات ، حذف البيانات المتتالية ، أو حذف بجنون العظمة. إذا أعجبك هذا المقال بالطبع.