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