I'm working on a project and at the point where I want to add UUIDs. Typically I maintain IDs for internal use and UUIDs for anything that may be exposed to the end user. For this particular project, I was curious about using UUIDs as primary keys (instead of IDs), thus dropping IDs altogether.
There's plenty of information out there for using primary key UUIDs with new projects, but not so much for migrating existing data.
UUIDs as Primary keys in Rails 5 with PostgreSQL
First, enable UUIDs for our database:
rails g migration enable_uuid_extension
class EnableUuidExtension < ActiveRecord::Migration
def change
enable_extension 'uuid-ossp'
end
end
And for new tables (going forward):
create_table :users, id: :uuid do |t|
t.string :name
end
Rails 5 tip - application.rb config to generate this tag by default:
config.generators do |generator|
generator.orm :active_record, primary_key_type: :uuid
end
Add UUIDs to existing tables
We want to keep id as our primary key, but with UUIDs instead of sequential integers. For now we'll create temporary uuid columns:
class AddUuidToEveryTable < ActiveRecord::Migration
def up
tables = [
"categories",
"products",
"users"
]
tables.each do |table|
add_column table, :uuid, :uuid, default: "uuid_generate_v4()", null: false
end
end
end
Migrate Foreign Keys to UUIDs
Associations between records have to be accounted for as well. This can be a bit tricky, particularly with non-standard association names names. There are probably gems to help with this, but I just hacked together a quick and dirty helper method.
The strategy here has three steps. For each association: 1) write the association's uuid to a temporary foreign key _uuid column, 2) remove the _id column and 3) rename the _uuid column, effectively migrating our foreign keys to UUIDs while sticking with the _id convention.
class ChangeForeignKeysToUuid < ActiveRecord::Migration
def up
id_to_uuid("products", "primary_category", "category")
id_to_uuid("products", "secondary_category", "category")
id_to_uuid("products", "user", "user")
end
def id_to_uuid(table_name, relation_name, relation_class)
table_name = table_name.to_sym
klass = table_name.to_s.classify.constantize
relation_klass = relation_class.to_s.classify.constantize
foreign_key = "#{relation_name}_id".to_sym
new_foreign_key = "#{relation_name}_uuid".to_sym
add_column table_name, new_foreign_key, :uuid
klass.where.not(foreign_key => nil).each do |record|
if associated_record = relation_klass.find_by(id: record.send(foreign_key))
record.update_column(new_foreign_key, associated_record.uuid)
end
end
remove_column table_name, foreign_key
rename_column table_name, new_foreign_key, foreign_key
end
end
Migrate Primary Keys to UUIDs
Now that we have UUIDs and foreign key references to UUIDs, we can safely transition our primary keys to UUIDs:
class ChangePrimaryKeysToUuid < ActiveRecord::Migration
def up
tables = [
"categories",
"products",
"users"
]
tables.each do |table|
remove_column table, :id
rename_column table, :uuid, :id
execute "ALTER TABLE #{table} ADD PRIMARY KEY (id);"
end
end
end
And lets not forget about our association indexes:
class AddAssociationIndexes < ActiveRecord::Migration
def up
add_index :products, :user_id
end
end
Sorting
One thing to note is that sequential IDs are used for default sorting, but since we no longer have sequential IDs, we need to fix that. A good solution is to use created_at:
default_scope -> { order("created_at ASC") }
And of course, indexes on created_at as well:
class AddCreatedAtIndexes < ActiveRecord::Migration
def up
add_index :categories, :created_at
add_index :products, :created_at
add_index :users, :created_at
end
end
Now User.first and User.last query on created_at instead of id.
Done!
And there we go! We've completed our migration to UUIDs as primary keys and foreign keys, with no more sequential IDs. Happy coding!