Transitioning a large legacy codebase from UUIDs to IDs. This needs to be done in stages to maintain backwards compatibility among many devices.
Current solution is to maintain both a UUID and ID field until we can transition over completely.
What's the best way to do this so that all belongs_to
models update both the ID and UUID on each create/update?
Example: Comment model belongs to BlogPost and needs to set both blogpost_id
& blogpost_uuid
on create/update.
Just do it through the database:
Let's say you have such legacy tables
class CreateLegacy < ActiveRecord::Migration
def change
enable_extension 'uuid-ossp'
create_table :legacies, id: :uuid do |t|
t.timestamps
end
create_table :another_legacies, id: false do |t|
t.uuid :uuid, default: 'uuid_generate_v4()', primary_key: true
t.timestamps
end
end
end
class Legacy < ActiveRecord::Base
end
class AnotherLegacy < ActiveRecord::Base
self.primary_key = 'uuid'
end
With the above code you have:
Legacy.create.id # => "fb360410-0403-4388-9eac-c35f676f8368"
AnotherLegacy.create.id # => "dd45b2db-13c2-4ff1-bcad-3718cd119440"
Now to add the new id column
class AddIds < ActiveRecord::Migration
def up
add_column :legacies, :new_id, :bigint
add_index :legacies, :new_id, unique: true
add_column :another_legacies, :id, :bigint
add_index :another_legacies, :id, unique: true
execute <<-SQL
CREATE SEQUENCE legacies_new_id_seq;
ALTER SEQUENCE legacies_new_id_seq OWNED BY legacies.new_id;
ALTER TABLE legacies ALTER new_id SET DEFAULT nextval('legacies_new_id_seq');
CREATE SEQUENCE another_legacies_id_seq;
ALTER SEQUENCE another_legacies_id_seq OWNED BY another_legacies.id;
ALTER TABLE another_legacies ALTER id SET DEFAULT nextval('another_legacies_id_seq');
SQL
end
def down
remove_column :legacies, :new_id
remove_column :another_legacies, :id
end
end
The default value is added after you create the new column as this prevents the db to try to update all the records. => the default will be default just for new records.
The old one you can backfill as you wish.
e.g. One by one
Legacy.where(new_id: nil).find_each { |l| l.update_column(:new_id, ActiveRecord::Base.connection.execute("SELECT nextval('legacies_new_id_seq')")[0]['nextval'].to_i) }
AnotherLegacy.where(id: nil).find_each { |l| l.update_column(:id, ActiveRecord::Base.connection.execute("SELECT nextval('another_legacies_id_seq')")[0]['nextval'].to_i) }
If you want you can first backfill and then add the defaults and then backfill again.
When you are happy with the values just change the primary key:
class Legacy < ActiveRecord::Base
self.primary_key = 'new_id'
def uuid
attributes['id']
end
end
class AnotherLegacy < ActiveRecord::Base
self.primary_key = 'id' # needed as we have not switched the PK in the db
end
Legacy.first.id # => 1
Legacy.first.uuid # => "fb360410-0403-4388-9eac-c35f676f8368"
AnotherLegacy.first.id # => 1
AnotherLegacy.first.uuid # => "dd45b2db-13c2-4ff1-bcad-3718cd119440"
Finally you need one more migration to change the primary key to the new id.
Most importantly to avoid downtime:
ps. not sure why you want to switch completely from the uuids, they are better if you want to reference the records from external applications
ps.2.0. if you need to be able to do Legacy.find("fb360410-0403-4388-9eac-c35f676f8368")
and Legacy.find(123)
maybe try https://github.com/norman/friendly_id
friendly_id :uuid, use: [:slugged, :finders]
On your Comment model, for example, you can add a before_save
callback, which gets called on model creation and update. In the callback method, you can reference the association and make sure the necessary fields are updated on the Comment.
# app/models/comment.rb
belongs_to :blogpost
# Add callback, gets called before create and update
before_save :save_blogpost_id_and_uuid
# At the bottom of your model
private
def save_blogpost_id_and_uuid
# You usually don't have to explicitly set the blogpost_id
# because Rails usually handles it. But you might have to
# depending on your app's implementation of UUIDs. Although it's
# probably safer to explicitly set them just in case.
self.blogpost_uuid = blogpost.uuid
self.blogpost_id = blogpost.id
end
And then repeat the above method for other models and their associations.
If desired, you can add some conditional logic that only updates the blogpost_id
and blogpost_uuid
if the blogpost ID or UUID changed.
You can define multiple keys for primary key using this gem: https://github.com/composite-primary-keys/composite_primary_keys
class Blogpost
self.primary_keys = :uuid, :id
has_many :comments, foreign_key: [:uuid, :id]
end
class Comment
belongs_to :blogpost, foreign_key: [:blogpost_uuid, :blogpost_id]
end
It would work if you already generated UUID and ID for BlogPost and synchronized with Comment's blogpost_uuid
, blogpost_id
In case you haven't synchronized blogpost_uuid
and blogpost_id
, I recommend you do the following to migrate:
uuid
from Blogpost to Comment's blogpost_uuid
, you could do:Comment.preload(:blogpost).find_each do |comment|
comment.update_column(blogpost_uuid: blogpost.uuid)
end
Hope it help you have a smooth transition. Let me know if something is not clear.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With