استراتژی های انتقال داده در Ruby on Rails: راه درست برای مدیریت داده های از دست رفته

بررسی اجمالی
در این مقاله، ما قصد داریم در مورد استراتژیهای ممکن برای مهاجرت، تولید و پر کردن دادهها در برنامه Rails بحث کنیم. ما آنها را پیاده سازی می کنیم، آنها را بهبود می بخشیم، مزایا و معایب آنها را در نظر می گیریم و بحث می کنیم که کدام یک بهتر است در سناریوهای مختلف استفاده شود. تا پایان مقاله، تصویر کاملی از روش های مختلف حل مشکلات انتقال داده خواهیم داشت.
معرفی
به عبارت ساده تر، انتقال داده فرآیند افزودن، به روز رسانی یا انتقال برخی از داده ها در داخل برنامه شما است. محبوب ترین موارد برای انتقال داده ها به شرح زیر است:
- پر کردن داده های ستون
- انتقال داده های ستون از یک جدول به جدول دیگر
- ایجاد رکوردهای جدید پایگاه داده
- به روز رسانی داده های خراب یا نامعتبر با مقادیر صحیح
- حذف داده های استفاده نشده
ما 3 روش مختلف را برای انجام آن در نظر خواهیم گرفت:
- دستکاری مستقیم داده ها
- Rake Task
- جواهر مهاجرت داده
دستکاری مستقیم داده ها
اولین گزینه ساده ترین گزینه است: ما فقط داده های از دست رفته را از طریق اضافه می کنیم rails c
یا از طریق اتصال مستقیم پایگاه داده در حال تولید.
مزایای:
- آسان
- نیازی به پیاده سازی چیز جدیدی نیست
- سریع است زیرا انتقال داده ها را می توان در چند دقیقه انجام داد.
چالش ها و مسائل:
- خیلی خطرناک تغییرات ممکن است آنطور که در نظر گرفته شده به پایان نرسد
- مشکلات دسترسی و امنیتی احتمالی
- هیچ تست و بررسی کدی وجود ندارد، بنابراین نمی توانیم از کیفیت آن مطمئن باشیم
- عدم کنترل؛ شما نمی دانید چه کسی مهاجرت را اجرا کرد یا چرا آنها آن را اجرا کردند.
Rake Task
گزینه دوم وظیفه رنک است. در این فصل، ما سعی خواهیم کرد نحوه اضافه کردن صحیح وظایف Rake را درک کنیم، اطمینان حاصل کنیم که آنها به درستی کار می کنند، مزایا و معایب آنها را یاد می گیریم و چگونگی استفاده از آنها برای انتقال داده را بررسی خواهیم کرد. ما با اضافه کردن سادهترین کار رنک شروع میکنیم و سپس ساختار آن را بهبود میدهیم، منطق را با تستها پوشش میدهیم و استفاده از بهترین شیوهها را برای نوشتن انتقال دادهها با استفاده از وظایف Rake در نظر میگیریم.
بیایید تصور کنیم که یک مدل Animal با فیلدهای زیر داریم:
id
kind
status
created_at
updated_at
و باید مقدار وضعیت را از آن تغییر دهیم nil
به reserved
برای همه حیواناتی که قبلاً امروز خلق کردیم. چگونه میتوانیم آنرا انجام دهیم؟ بیایید با اضافه کردن یک الگوی کار ساده Rake شروع کنیم.
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
puts 'Updating animal status...'
end
end
و بررسی کنید که کار می کند:
rake animals:backfill_statuses
# => Updating animal status...
وظیفه اجرا شده است و همانطور که انتظار می رود کار می کند. حالا بیایید کد واقعی را با به روز رسانی پایگاه داده اضافه کنیم. به شکل زیر خواهد بود:
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
Animal.where(status: nil).where('created_at < ?', Time.zone.today).update_all(status: 'reserved')
end
end
حالا بیایید بررسی کنیم که آیا کار می کند یا خیر:
rake animals:backfill_statuses
وظیفه Rake اجرا شده است و مقادیر پایگاه داده بر این اساس به روز می شوند. خودشه. سناریوی اصلی ما همانطور که انتظار میرود کار میکند، اما هنوز جا برای بهبود وجود دارد. بیایید نگاهی بیندازیم به آنچه که می توانیم انجام دهیم تا کارمان قابل اعتمادتر شود.
بهبودها
5 زمینه وجود دارد که به طور بالقوه می توانیم آنها را بهبود بخشیم:
- نمایش نتایج در کنسول برای مشاهده
- از سازگاری داده ها با تراکنش ها اطمینان حاصل کنید
- بهینه سازی درخواست های DB
- کد وظیفه Rake را جدا کنید
- اضافه کردن تست ها
نمایش نتایج در کنسول برای مشاهده
همانطور که ممکن است متوجه شده باشید، وظیفه Rake در بالا هیچ خروجی را نشان نداده است. این می تواند یک مشکل واقعی باشد زیرا شما نمی دانید که آیا با موفقیت اجرا شده است یا نه، و زمان زیادی را صرف تلاش برای بررسی آن توسط خودتان روی داده های تولید خواهید کرد. بیایید این مشکل را برطرف کنیم:
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
puts "Before running the rake task, there were #{Animal.where(status: 'reserved').count} animals in the 'reserved' state."
Animal.where(status: nil).where('created_at < ?', Time.zone.today).update_all(status: 'reserved')
puts "After running the rake task, there are now #{Animal.where(status: 'reserved').count} animals in the 'reserved' state."
end
end
حال، بیایید وظیفه رنک را اجرا کنیم:
rake animals:backfill_statuses
# => Before running the rake task, there were 0 animals in the 'reserved' state.
# => After running the rake task, there are now 101 animals in the 'reserved' state.
با کد به روز شده، ما تعداد حیوانات را در حالت «رزرو شده» هم قبل و هم بعد از اجرای کار رنک نمایش می دهیم و دید بهتری را ارائه می دهیم و از اجرای موفقیت آمیز کار اطمینان می دهیم.
از سازگاری داده ها با تراکنش ها اطمینان حاصل کنید
اگر برخی از خطاهای غیرمنتظره در وسط انتقال داده ظاهر شوند، چه اتفاقی میافتد؟ در حال حاضر، ما آن را اداره نمی کنیم. حتی اگر برای مثالی که ارائه کردهایم حیاتی نباشد، به طور کلی، نباید فراموش کنیم که چنین دستکاری دادهها را در یک تراکنش بپیچانیم تا وضعیت داده ثابتی را حفظ کنیم.
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
ActiveRecord::Base.transaction do
Animal.where(status: nil).where('created_at < ?', Time.zone.today).update_all(status: 'reserved')
end
end
end
با اضافه کردن ActiveRecord::Base.transaction
ما اطمینان حاصل می کنیم که تمام به روز رسانی ها به صورت یک عملیات اتمی اجرا می شوند. اگر در حین انتقال داده ها خطایی رخ دهد، تراکنش برگردانده می شود و داده ها بدون تغییر باقی می مانند و یکپارچگی و یکپارچگی داده ها را حفظ می کنند.
بهینه سازی درخواست های DB
ما قبلاً این مشکل را در وظیفه رنک خود حل کرده ایم، اما ذکر این نکته مهم است که در صورت امکان باید از راه حل بهینه پایگاه داده استفاده کنیم. به عنوان مثال، شخصی می تواند وظیفه ما را اینگونه بنویسد:
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
Animal.where(status: nil).where('created_at < ?', Time.zone.today).each do |animal|
animal.update(status: 'reserved')
end
end
end
کد زیر یک درخواست بهروزرسانی SQL را برای هر حیوانی از لیست آغاز میکند و آن را غیربهینه میکند:
D, [2023-07-21T09:50:58.346040 #67787] DEBUG -- : Animal Load (1.6ms) SELECT `animals`.* FROM `animals` WHERE `animals`.`status` IS NULL AND (created_at < '2023-07-21')
D, [2023-07-21T09:50:58.346735 #67787] DEBUG -- : ↳ lib/tasks/animals/backfill_statuses.rake:10:in `block (2 levels) in <main>'
D, [2023-07-21T09:50:58.371908 #67787] DEBUG -- : TRANSACTION (2.2ms) BEGIN
D, [2023-07-21T09:50:58.372737 #67787] DEBUG -- : ↳ lib/tasks/animals/backfill_statuses.rake:11:in `block (3 levels) in <main>'
D, [2023-07-21T09:50:58.375091 #67787] DEBUG -- : Animal Update (2.2ms) UPDATE `animals` SET `animals`.`status` = 'reserved', `animals`.`updated_at` = '2023-07-21 07:50:58.368697' WHERE `animals`.`id` = 1
D, [2023-07-21T09:50:58.375713 #67787] DEBUG -- : ↳ lib/tasks/animals/backfill_statuses.rake:11:in `block (3 levels) in <main>'
D, [2023-07-21T09:50:58.381169 #67787] DEBUG -- : TRANSACTION (5.0ms) COMMIT
D, [2023-07-21T09:50:58.381524 #67787] DEBUG -- : ↳ lib/tasks/animals/backfill_statuses.rake:11:in `block (3 levels) in <main>'
D, [2023-07-21T09:50:58.383624 #67787] DEBUG -- : TRANSACTION (1.3ms) BEGIN
D, [2023-07-21T09:50:58.384250 #67787] DEBUG -- : ↳ lib/tasks/animals/backfill_statuses.rake:11:in `block (3 levels) in <main>'
D, [2023-07-21T09:50:58.385792 #67787] DEBUG -- : Animal Update (1.4ms) UPDATE `animals` SET `animals`.`status` = 'reserved', `animals`.`updated_at` = '2023-07-21 07:50:58.381901' WHERE `animals`.`id` = 2
...
به همین دلیل همیشه منطقی است که سعی کنیم راهی برای انجام آن در یک عملیات DB پیدا کنیم، همانطور که با کد زیر انجام دادیم:
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
Animal.where(status: nil).where('created_at < ?', Time.zone.today).update_all(status: 'reserved')
end
end
این کد فقط یک درخواست DB را راه اندازی می کند:
Animal Update All (6.8ms) UPDATE `animals` SET `animals`.`status` = 'reserved' WHERE `animals`.`status` IS NULL AND (created_at < '2023-07-21')
اگر راهی برای به روز رسانی چیزی در یک درخواست DB وجود ندارد، حداقل باید استفاده از دسته ها را به عنوان یک تمرین خوب در نظر بگیرید:
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
Animal.where(status: nil).where('created_at < ?', Time.zone.today).find_each do |animal|
animal.update(status: 'reserved')
end
end
end
PS برای دیدن گزارشهای SQL از وظیفه Rake، میتوانید کد زیر را داخل آن اضافه کنید:
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
ActiveRecord::Base.logger = Logger.new(STDOUT)
# ...
end
end
کد وظیفه Rake را جدا کنید
یکی از مشکلات نه چندان واضحی که ممکن است در هنگام انجام وظایف بسیار زیاد رخ دهد، عدم وجود کپسوله است. بیایید نگاهی به دو کار رنک زیر بیندازیم و سعی کنیم حدس بزنیم چه چیزی می تواند در اینجا اشتباه باشد:
# frozen_string_literal: true
# lib/tasks/animals/task1.rake
namespace :animals do
task task1: [:environment] do
puts message
end
def message
'Hello world from Task 1!'
end
end
# frozen_string_literal: true
# lib/tasks/animals/task2.rake
namespace :animals do
task task2: [:environment] do
puts message
end
def message
'Hello world from Task 2!'
end
end
حالا بیایید هر دوی آنها را اجرا کنیم:
rake animals:task1
# => Hello world from Task 2!
rake animals:task2
# => Hello world from Task 2!
آیا توجه کرده اید؟ این چیزی نیست که ما انتظار داشتیم! وظیفه رنک دوم، مقدار متد را از اولی حذف کرد! و این بسیار خطرناک و غیرمنتظره است اگر بخواهید از چیزی مانند این استفاده کنید:
# frozen_string_literal: true
# lib/tasks/animals/task1.rake
namespace :animals do
task task1: [:environment] do
query.destroy_all
end
def query
Animal.where(status: nil)
end
end
-
lib/tasks/animals/task2.rake
# frozen_string_literal: true
# lib/tasks/animals/task2.rake
namespace :animals do
task task2: [:environment] do
query.destroy_all
end
def query
Animal.all
end
end
و اجرا:
rake animals:task1
شما به جای زیرمجموعه مورد نظر، تمام رکوردهای خود را حذف می کنید!
چگونه می توانیم آن را تعمیر کنیم؟
ما باید وظایف رنک خود را در آن قرار دهیم Rake::DSL
کلاس مثل این:
# frozen_string_literal: true
# lib/tasks/animals/task1.rake
module Tasks
module Animals
class Task1
include Rake::DSL
def initialize
namespace :animals do
task task1: [:environment] do
puts message
end
end
end
private
def message
'Hello world from Task 1!'
end
end
end
end
Tasks::Animals::Task1.new
# frozen_string_literal: true
# lib/tasks/animals/task2.rake
module Tasks
module Animals
class Task2
include Rake::DSL
def initialize
namespace :animals do
task task2: [:environment] do
puts message
end
end
end
private
def message
'Hello world from Task 2!'
end
end
end
end
Tasks::Animals::Task2.new
و بیایید اجرا کنیم:
rake animals:task1
# => Hello world from Task 1!
rake animals:task2
# => Hello world from Task 2!
اکنون همه چیز همانطور که انتظار می رود کار می کند. بیایید همان انزوا را برای خودمان اعمال کنیم backfill_statuses
وظیفه چنگک زدن
# frozen_string_literal: true
# lib/tasks/animals/backfill_statuses.rake
module Tasks
module Animals
class BackfillStatuses
include Rake::DSL
def initialize
namespace :animals do
desc "Update animal status to 'reserved' for animals created before today"
task backfill_statuses: [:environment] do
Animal.where(status: nil).where('created_at < ?', Time.zone.today).update_all(status: 'reserved')
end
end
end
end
end
end
Tasks::Animals::BackfillStatuses.new
خودشه.
اضافه کردن تست ها
آخرین کاری که برای اطمینان از کیفیت انجام می دهیم اضافه کردن آزمایشات است. بیایید ببینیم چگونه می توانیم وظایف رنک را آزمایش کنیم.
اول از همه، باید کدی را برای بارگذاری وظایف خود تعریف کنیم:
# spec/support/tasks.rb
# frozen_string_literal: true
RSpec.configure do |_config|
Rails.application.load_tasks
end
و آن را در spec/rails_helper.rb
:
# spec/rails_helper.rb
require 'support/tasks'
سپس تست خود را اضافه می کنیم:
# spec/tasks/animals/backfill_statuses_spec.rb
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'rake animals:backfill_statuses', type: :task do
subject { Rake::Task['animals:backfill_statuses'].execute }
let(:expected_output) do
<<~TEXT
Before running the rake task, there were 1 animals in the 'reserved' state.
After running the rake task, there are now 2 animals in the 'reserved' state.
TEXT
end
let(:animal_1) { create(:animal, created_at: 10.days.ago, status: nil) }
let(:animal_2) { create(:animal, created_at: 10.days.ago, status: 'reserved') }
let(:animal_3) { create(:animal, created_at: 10.days.ago, status: 'another_status') }
let(:animal_4) { create(:animal, created_at: 10.days.from_now, status: nil) }
before do
animal_1
animal_2
animal_3
animal_4
end
it "update animal status to 'reserved' for animals created before today" do
expect { subject }.to change { animal_1.reload.status }.from(nil).to('reserved')
.and output(expected_output).to_stdout
expect(animal_2.reload.status).to eq('reserved')
expect(animal_3.reload.status).to eq('another_status')
expect(animal_4.reload.status).to be_nil
end
end
خودشه.
جواهر مهاجرت داده
گزینه سوم استفاده از data-migrate gem است.
بیایید این گوهر را به پروژه خود اضافه کنیم:
# Gemfile
gem 'data_migrate'
و اجرا کنید:
bundle install
اکنون می توانید یک انتقال داده ایجاد کنید، همانطور که یک مهاجرت طرحواره ایجاد می کنید:
rails g data_migration backfill_animal_statuses
# => db/data/20230721111716_backfill_animal_statuses.rb
بیایید مقداری کد به فایل تولید شده اضافه کنیم تا بررسی کنیم که آیا واقعاً کار می کند یا خیر:
# frozen_string_literal: true
class BackfillAnimalStatuses < ActiveRecord::Migration[7.0]
def up
puts 'Test Data Migration'
end
def down
# do nothing
end
end
برای اجرای migration باید از دستور زیر استفاده کنیم:
rake data:migrate
# or
rake db:migrate:with_data
و خروجی زیر را دریافت می کنیم:
== 20230721111716 BackfillAnimalStatuses: migrating ===========================
Test Data Migration
== 20230721111716 BackfillAnimalStatuses: migrated (0.0000s) ==================
این مهاجرت فقط یک بار قابل اجرا است. پس بیایید آن را حذف کنیم و یک کد دیگر ایجاد کنیم و کد واقعی را داخل آن اضافه کنیم:
rails g data_migration backfill_animal_statuses
# => db/data/20230721112534_backfill_animal_statuses.rbb
این چیزی است که پس از افزودن کد منطق کسب و کار خود به دست می آوریم:
# db/data/20230721112534_backfill_animal_statuses.rb
# frozen_string_literal: true
class BackfillAnimalStatuses < ActiveRecord::Migration[7.0]
def up
Animal.where(status: nil).where('created_at < ?', Time.zone.today).update_all(status: 'reserved')
end
def down
# do nothing
end
end
و بیایید اجرا کنیم:
rake data:migrate
در اینجا چیزی است که ما دریافت می کنیم:
== Data =======================================================================
== 20230721112534 BackfillAnimalStatuses: migrating ===========================
== 20230721112534 BackfillAnimalStatuses: migrated (0.0221s) ==================
اساساً، انتقال داده با همان منطق مهاجرت طرح کار می کند، اما به جای ذخیره آخرین نسخه مهاجرت در حال اجرا در schema_migrations
جدول، انتقال داده ها نسخه را در جدول دیگری به نام ذخیره می کند data_migrations
.
ذکر این نکته مهم است که انتقال دادهها در بیشتر موارد باید برگشتناپذیر باشد، اما ما نمیخواهیم یک خطای صریح ایجاد کنیم، زیرا از بازگشت به عقب برای تغییرات ساختار طرح جلوگیری میکند. در عوض، ما فقط آن را ترک می کنیم down
روش خالی به همین دلیل، بهتر است مهاجرت را به صورت غیر توانمند طراحی کنیم تا در صورت امکان چندین بار بتوان آن را اجرا کرد.
انتقال داده هیچ مزیت دیگری به جز مواردی که در بالا به آن اشاره کردیم، ارائه نمی دهد. بنابراین، ما هنوز باید در مورد مشکلاتی که برای وظیفه Rake حل کرده ایم فکر کنیم، مانند نمایش خروجی، اضافه کردن تراکنش ها، بهینه سازی درخواست های DB و غیره.
مقایسه Rake Task و Data Migration Gem
بیایید این دو راه حل را با هم مقایسه کنیم و تصمیم بگیریم که از کدام یک و تحت چه شرایطی استفاده کنیم:
چه زمانی یک کار شن کش بهتر می شود؟
- زمانی که می خواهید این قابلیت را داشته باشید که زمان و روز دقیقی را که می خواهید اجرا کنید را انتخاب کنید.
- زمانی که می خواهید این قابلیت را داشته باشید که پلتفرمی را که می خواهید اجرا کنید (مثلاً صحنه سازی یا تولید) را انتخاب کنید.
- زمانی که می خواهید یک وظیفه Rake را چندین بار اجرا کنید.
چه زمانی گوهر انتقال داده بهتر جا می شود؟
- وقتی می خواهید مطمئن شوید که داده ها به طور خودکار اضافه می شوند و هیچ کس فراموش نمی کند چیزی را اجرا کند.
- وقتی ترتیب انتقال طرحواره برای شما مهم است (به عنوان مثال، مهاجرت طرحواره یک ستون جدید اضافه می کند و انتقال داده ها این ستون را پر می کند).
- زمانی که می خواهید آن را در همه محیط ها بدون تلاش اضافی اجرا کنید.
- زمانی که باید مهاجرت را فقط یک بار اجرا کنید.
بنابراین، به طور کلی، وظیفه Rake بسیار انعطافپذیرتر و قابل آزمایشتر است و میتواند وظایف مشابه انتقال داده را پوشش دهد، اما ممکن است به تلاش بیشتری نیاز داشته باشد. انتقال دادهها بسیار سختگیرانهتر هستند، اما برخی از اتوماسیونها و ترتیب اجرای دقیق را ارائه میدهند که با تغییرات طرحواره مرتبط هستند.
به عنوان مثال، اگر شما نیاز به پشتیبانی از ساختار مجدد طرحواره پایگاه داده دارید و نمی خواهید داده ها را در طول این تغییرات از دست بدهید، gem انتقال داده بسیار مناسب است. به عنوان مثال، اگر شما نیاز به تغییر نام یک ستون دارید و می خواهید مقادیر را از ستون قدیمی به ستون جدید کپی کنید، سپس تنظیم کنید NULL=false
محدود کردن به جدید، و سپس حذف کامل قدیمی، جواهر مهاجرت داده، این فرآیند را در مقایسه با استفاده از وظیفه رنک بسیار آسانتر میکند.
از سوی دیگر، اگر کار با تغییرات طرح پایگاه داده ارتباطی نداشته باشد، یک کار رنک ممکن است انتخاب مناسب تری باشد. انعطاف پذیری و آزمایش پذیری بیشتری را ارائه می دهد و مدیریت وظایفی را که مستقیماً با تغییرات طرحواره مرتبط نیستند آسان تر می کند.
نتیجه
در طول این مقاله، ما استراتژیهای مختلف برای انتقال داده، تولید و پر کردن دادهها در یک برنامه Rails را بررسی کردهایم. ما با در نظر گرفتن مزایا و معایب آنها، این استراتژی ها را اجرا و بهبود بخشیده ایم. علاوه بر این، ما دو راهحل اصلی، وظیفه rake و gem مهاجرت داده را مقایسه کردهایم و مناسب بودن آنها را در سناریوهای مختلف بررسی کردهایم.