Migrating to UUIDs as Primary Keys

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!

Discuss this post
Mustache
Hi, I'm Loren. When I'm not building Penflip.com, I'm trying to grow a pretty sweet mustache and beard. Say hi on twitter!

Read more:

Kanye West, Startup Idol

End the week with something

Too many features

Failure

I get it. This is why people have cofounders.

From Idea to Validation to 3,500 Users

Github for Writers

Build A Life

Interesting Experience With the TSA...

Every First Draft

I Quit My Job

Do things, tell people.

I can't spell 'Textmate'

Elon Musk's Hyperloop

Rocketr, don’t tell a story. Answer questions.

Why Instagram Is Worth $1 Billion (Or More)

Startup Founders: It's Okay To Take A Break

Knowledge empowers people

Sean Parker - Solve Problems

Every app should be this easy.

Dane Maxwell - Do More Stuff

The 4-Hour Startup: Marketing It

Developers: Free Project Idea. Please Make This.

The 4-Hour Startup: Building It

RSS is live!

The 4-Hour Startup: How I Did It