Best way to implement multiple both UUID and ID on each model in Rails

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:

  • create a column
  • ensure new records fill by default somehow (default or trigger)
  • backfill old record
  • add constraints
  • switch to the new column
  • then you can drop the old one (if you are sure it is not in use)

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]