1. Don't
2. See rule 1!
3. See rule 1!
4. See rule 1!
5. See rule 1!
class Something < ApplicationRecord
alias_attribute :new_column_name, :existing_column_name
end
Create the new column
class Car < ApplicationRecord
# Has a column named :kar_blueray_thor
end
class AddCarburatorToCars < ActiveRecord::Migration[7.1]
def change
add_column :cars, :carburator, :string
end
end
bundle exec rails db:migrate
1. Make sure you don't have a default value for the new column.
2. Add indexes concurrently
class AddIndexToCarburator < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :cars, :carburator, algorithm: :concurrently
end
end
Read from first, write in both
class Car < ApplicationRecord
# Read from first
def carburator
kar_blueray_thor
end
# Write in both
def carburator=(value)
self.update(carburator: value)
self.update(kar_blueray_thor: value)
end
alias_method :kar_blueray_thor=, :carburator=
end
1. Use your new methods everywhere in the codebase.
2. Make sure all tests pass.
3. Deploy your code.
4. Monitor for errors.
Migrate your data
This is where the fun begins!
Have the follwoing 3 arguments:
1. Batch Size
2. Batch Delay
3. Cursor
Make the task idempotent! Don't trigger callbacks!
namespace :data do
task :carburator do
size = ENV.fetch('BATCH_SIZE').to_i
delay = ENV.fetch('BATCH_DELAY').to_i
cursor = ENV.fetch('CURSOR').to_i
CarburatorDataFill.new(size, delay, cursor).call
end
end
class CarburatorDataFill
# initializer...
def call
Car.where(id: @cursor..).in_batches(of: @size) do |cars|
cars.each do |car|
car.carburator = car.kar_blueray_thor
car.save!
end
sleep @delay
end
end
end
cars.each do |car|
ApplicationRecord.transaction do
car.carburator = car.kar_blueray_thor
car.save!
end
end
Ideally there are no such callbacks!
1. Deploy your code.
2. Start a shell and execute the task with LOG_LEVEL=debug!
3. Monitor for errors and performance degradation.
Stop execution if you notice issues, and update the 3 parameters for your next run!
You could deploy and execute your task in a background job if you can't have shell access.
Only if you have quick and easy access to stop and tweak the parameteres!
And always monitor the performance metrics, errors, and logs!
Read from second, write in both
class Car < ApplicationRecord
# Read from second
def kar_blueray_thor
carburator
end
# Write in both
def kar_blueray_thor=(value)
self.update(carburator: value)
self.update(kar_blueray_thor: value)
end
alias_method :carburator=, :kar_blueray_thor=
end
1. Make sure all tests still pass.
2. Deploy your code.
3. Monitor for errors.
Cleanup
class Car < ApplicationRecord
# Old code removed!
end
-- Make sure the column is REALLY no longer used!
REVOKE ALL PRIVILEGES
ON cars(kar_blueray_thor)
FROM app_database_user;
class RemoveKarThingyFromCars < ActiveRecord::Migration[7.1]
# This is IRREVERSIBLE!
def up
remove_column :cars, :kar_blueray_thor
end
end
class RemoveKarThingyFromCars < ActiveRecord::Migration[7.1]
def up
remove_column :cars, :kar_blueray_thor
end
def down
add_column :cars, :kar_blueray_thor, :string
end
end
And you can restore the data from a backup...
Advantages:
1. Zero Downtime!
2. Reversible every step of the way
3. Slow, you have time to catch bugs
Disadvantages:
1. Slow
2. Works best with continuous deployments
class Something < ApplicationRecord
alias_attribute :new_column_name, :existing_column_name
end
But it's good to know the entire process, maybe you will need parts of it for other Zero Downtime workloads.