تهجير الداتا من MongoDB الى PostgreSQL
هذه المهم من صعب نسيناها، ما زالت اذكر تفاصليها :اني عملت عليها الامس. لا اعلم السبب بصراحة ربما لانها مهمة نادرة الحصول مع المبرمجيم بشكل عام ام لانها كانت تمثل تحديا كبيرا لي. على اي حال احببت ان ادون تفاصيل هذه التجربة لكي لا انساها في المستقبل ولكي يتفيد منها اي مبرمج قد يعمل على مهمة مماثلة لها.
الاسباب التي ادت الى اتخاذ قرار التجهير
السبب الرئيسي والوحيد لهذا القرار هو ان اختيار قاعدة البيانات MongoDB من البداية كان قرار غير موفق تم استخدمها فقط للاستفادة من خاصية الحقل Hash الذي يمكنك من حفظ json داتا
التحديات التي واجهني
1- اتمام المهمة بدون اي انقطاع بالخدمة 2- تحويل Mongo ObjectID الى UUID
خطوات التنفيذ
1- لتجاوز مشكلة انقطاع الخدمة كان يتوجب علي تقسيم المهمة الى ثلات مراحل (اصدارات)
1-1 الاصدار الاول
1- تنصيب gems المطلوبة
#Gemfile
gem 'sinatra-activerecord'
gem 'bson-objectid-to-uuid'
gem 'pg', '~> 0.20.0'
التطبيق الذي كنت اعمل عليه كان مبيني على sinatra و ليس rails
لذا محتاج لاضافة gem 'sinatra-activerecord'
ايضا
2- انشاء postgresql schema
انشاء migration لكل Mongoid::Document ضمن postgresql واستخدام نوع الداتا لكل عامود
مثلا لدي هذا Document
class BannedCustomer
include Mongoid::Document
include Mongoid::Timestamps
field :account_id, type: Integer
field :notes, type: String
field :system_banned, type: Boolean, default: false
field :deleted, type: Boolean, default: false
end
سوف تكون migration كالتالي
class CreateBannedCustomers < ActiveRecord::Migration
def change
create_table :banned_customers, id: :uuid, default: 'gen_random_uuid()' do |t|
t.integer :account_id
t.text :notes
t.boolean :deleted, :default => false
t.boolean :system_banned, :default => false
t.timestamps null: false
end
end
end
3- توليد ActiveRecord models
كل models الخاصة ب Mongo موجودة ضمن المسار التالي
root
├── models
│ └── banned_customers.rb
│ └── another_model.rb
لذا نحتاح لاستخدام namespace لكي نميز بين models الخاصة بكل نوع من الداتا بيز
root
├── models
│ └── banned_customers.rb
│ └── another_model.rb
│ └── pg (folder)
│ └── banned_customers.rb
│ └── another_model.rb
module PG
class BannedCustomer < ActiveRecord::Base
end
end
هكذا يكون لدينا اثنان من models ل BannedCustomer
BannedCustomer => MongoDB
PG::BannedCustomer => PostreSQL
4- كتابة مهمة rake task
وظيفتها نقل الدتا من ومزامنتها
# tasks/db.rake
namespace :db do
namespace :sync_postgresql do
sync_desc = <<-DESC.gsub(/ /, '')
Sync data between Mongoid and postgresql for specifce Mongoid's model
$ rake db:sync_postgresql:model MODEL='MyModel'
DESC
desc sync_desc
task :model do
batch_size = 100
models = ["BannedCustomer", ....]
not_synced_ids = []
last_record_processed = nil
updated_records_ids = []
created_records_ids = []
if ENV['MODEL'].to_s == ''
puts '='*90, 'USAGE', '='*90, sync_desc, ""
exit(1)
else
if !models.include?(ENV['MODEL'].to_s)
puts '='*90, 'USAGE', '='*90, sync_desc, ""
exit(1)
end
mongoid_klass = eval(ENV['MODEL'])
pg_klass = eval("PG::#{ENV['MODEL'].to_s}")
after_id = ENV['AFTER_ID'].present? ? ENV['AFTER_ID'].to_s : nil
if after_id.eql?("0")
last_record_synced_id = nil
else
last_record_synced_id = after_id || PG::MigrationState.find_by(key: "last_#{ENV['MODEL'].to_s.underscore}_id").try(:value)
end
puts "last_record_synced_id: #{last_record_synced_id}"
if last_record_synced_id.present?
last_record_synced = mongoid_klass.unscoped.find(last_record_synced_id) rescue nil
end
last_record_stored = mongoid_klass.unscoped.asc(:created_at).last
if last_record_synced && (last_record_synced.id == last_record_stored.id)
puts "All #{ENV['MODEL']} Records Have Been Synced"
else
if last_record_synced
count = mongoid_klass.unscoped.between(created_at: (last_record_synced.created_at..last_record_stored.created_at)).count.to_f
else
count = mongoid_klass.unscoped.all.count.to_f
end
puts "count: #{count}"
1.upto((count / batch_size).ceil) do |page|
if last_record_synced
@mongodb_records = mongoid_klass.unscoped.between(created_at: (last_record_synced.created_at..last_record_stored.created_at)).page(page).per(batch_size)
else
@mongodb_records = mongoid_klass.unscoped.all.page(page).per(batch_size)
end
@mongodb_records.each do |record|
uuid = record._id.to_uuid
mongo_json = record.to_json
mongo_attributes = JSON.parse(mongo_json)
mongo_attributes = mongo_attributes.merge({"id" => uuid})
mongo_attributes.delete("_id")
begin
pg_record = pg_klass.unscoped.find_by(id: uuid)
if pg_record
pg_attributes = pg_record.attributes
if pg_attributes != mongo_attributes
pg_record.update!(mongo_attributes)
updated_records_ids << record._id
end
else
pg_klass.create!(mongo_attributes)
created_records_ids << record._id
end
last_record_processed = record._id
rescue => e
Raven.capture_exception(e)
puts e
not_synced_ids << record._id
end
end
unless @mongodb_records.empty?
sync_state = PG::MigrationState.find_or_create_by!(key: "last_#{ENV['MODEL'].to_s.underscore}_id")
sync_state.update!(value: last_record_processed )
end
puts "#{batch_size} #{mongoid_klass} are processed."
end
not_synced_state = PG::MigrationState.find_or_create_by!(key: "not_synced_#{ENV['MODEL'].to_s.underscore}_ids")
not_synced_state_value = not_synced_ids.empty? ? "" : not_synced_ids.join(',')
not_synced_state.update(value: not_synced_state_value)
end
end
end
all_sync_desc = <<-DESC.gsub(/ /, '')
Sync data between Mongoid and postgresql for all Mongoid's models
$ rake db:sync_postgresql:model:all
DESC
desc all_sync_desc
task :all do
all_models = ["BannedCustomer", "BannedPaypal", "CreditCardCheck", "PaypalCheck"]
all_models.each do |klass|
puts "#" * 40
puts "Processing model: #{klass}..."
ENV['MODEL'] = klass.to_s
Rake::Task["db:sync_postgresql:model"].invoke
Rake::Task["db:sync_postgresql:model"].reenable
end
end
end
end
PG::MigrationState
هو جدول استخدمته لحفظ حالة التجهير
بالتأكيد يمكن استبداله باي حل آخر متل in-memory database ولكني بالنسبة الي لم يكن هناك اي خيار آخر متوفر لذا كنت مضطر لحفظها داخل الداتا بيز
5- ابقاء الكتابة والقراءة من MongoDB
بعد تنفيذ الخطوات السابقة تم رفع هذا الاصدار وباليدأ بتنفيذ تاسك المزامنة ووضعها ضمن cron ليتم تنفيذها كل 10د
2-1 الاصدار الثاني
في هذا الاصدار تم جعل القراءة والكتابة من PostgreSQL بدلا MongoDb وتنفيذ مهمة المزامنة للتأكد من مزامنة الداتا
3-1 الاصدار الاخير
تم حذف MongoDb نهائيا بعد التأكد من عمل PostgreSQL وان جميع الداتا تم نقلها من خلال التاسك السابقة
الخلاصة
الخطوات السابقة بالرغم من انها قد تبدو بسيطة ولكنها استغرقت وقتًا طويلاً ولكن المهم انها تمت بدون ان يلاحظ المستخدمين اي تغير يذكر. وهذا المطلوب بالتأكيد
أراهن أن هناك العديد من المطورين الذين اضطروا بالفعل للتعامل مع مثل هذه المشكلة، اذا واحد منهم لا تترد بإخباري بتجربتك من خلال التعليقات :)