Rails 4. Migrate table id to UUID

女生的网名这么多〃 提交于 2019-11-28 09:04:44

I had the same problem as yours. To migrate from default id to use uuid, I think you could something similar to what I had:

class ChangeVocBrandsPrimaryKey < ActiveRecord::Migration
  def change
    add_column :voc_brands, :uuid, :uuid, default: "uuid_generate_v4()", null: false

    change_table :voc_brands do |t|
      t.remove :id
      t.rename :uuid, :id
    end
    execute "ALTER TABLE voc_brands ADD PRIMARY KEY (id);"
  end
end

I know migrations are preferred way to made any db change but below approach is awesome. It is possible to use direct queries to PostgreSQL to convert table with existing data.

For primary key:

    ALTER TABLE students
        ALTER COLUMN id DROP DEFAULT,
        ALTER COLUMN id SET DATA TYPE UUID USING (uuid(lpad(replace(text(id),'-',''), 32, '0'))),
        ALTER COLUMN id SET DEFAULT uuid_generate_v4()

For other references:

    ALTER TABLE students
        ALTER COLUMN city_id SET DATA TYPE UUID USING (uuid(lpad(replace(text(city_id),'-',''), 32, '0')))

The above left pads the integer value with zeros and converts to a UUID. This approach does not require id mapping and if needed old id could be retrieved.

As there is no data copying, this approach works quite fast.

To handle these and more complicated case of polymorphic associations please use https://github.com/kreatio-sw/webdack-uuid_migration. This gem adds additional helpers to ActiveRecord::Migration to ease these migrations.

I know this doesn't directly answer the question, but I created a rake task that can help convert any project from id to uuid https://gist.github.com/kuczmama/152d762177968f7192df1dea184e3370

task id_to_uuid: :environment do
  puts "[START] Convert id to uuid"
  ActiveRecord::Base.connection.enable_extension 'uuid-ossp' unless ActiveRecord::Base.connection.extensions.include? 'uuid-ossp'
  ActiveRecord::Base.connection.enable_extension 'pgcrypto' unless ActiveRecord::Base.connection.extensions.include? 'pgcrypto'

  table_names = ActiveRecord::Base.connection.tables - ["schema_migrations", "ar_internal_metadata", "migration_validators"]
  table_names.each do |table_name|
    puts "[CREATE] uuid column for #{table_name}"

    #Make sure the column is a uuid if not delete it and then create it
    if ActiveRecord::Migration.column_exists? table_name, :uuid
      column_type = ActiveRecord::Migration.columns(table_name).select{|c| c.name == "uuid"}.try(:first).try(:sql_type_metadata).try(:type)
      if column_type && column_type != :uuid
        ActiveRecord::Migration.remove_column(table_name, :uuid)
      end
    end

    # Create it if it doesn't exist
    if !ActiveRecord::Migration.column_exists? table_name, :uuid
      ActiveRecord::Migration.add_column table_name, :uuid, :uuid, default: "uuid_generate_v4()", null: false
    end

  end

  # The strategy here has three steps.
  # For each association:
  # 1) write the association's uuid to a temporary foreign key _uuid column,
  # 2) For each association set the value of the _uuid column
  # 3) remove the _id column and
  # 4) rename the _uuid column to _id, effectively migrating our foreign keys to UUIDs while sticking with the _id convention.
  table_names.each do |table_name|
    puts "[UPDATE] change id to uuid #{table_name}"
    model = table_name.singularize.camelize.constantize
    id_columns = model.column_names.select{|c| c.end_with?("_id")}


    # write the association's uuid to a temporary foreign key _uuid column
    # eg. Message.room_id => Message.room_uuid
    model.reflections.each do|k, v|
      begin
        association_id_col = v.foreign_key
        # Error checking
        # Make sure the relationship actually currently exists
        next unless id_columns.include?(association_id_col)
        # Check that there is at

        # 1) Create temporary _uuid column set to nulll,
        tmp_uuid_column_name = column_name_to_uuid(association_id_col)
        unless ActiveRecord::Migration.column_exists?(table_name, tmp_uuid_column_name)
          puts "[CREATE] #{table_name}.#{tmp_uuid_column_name}"
          ActiveRecord::Migration.add_column(table_name, tmp_uuid_column_name, :uuid)
        end

        # 2) For each association set the value of the _uuid column
        #
        # For example.  Assume the following example
        #
        # message.room_id = 1
        # room = Room.find(1)
        # room.uuid = 0x123
        # message.room_uuid = 0x123
        #
        association_klass = v.klass

        model.unscoped.all.each do |inst|
          next unless inst.present?
          association = association_klass.find_by(id: inst.try(association_id_col.try(:to_sym)))
          next unless association.present?
          inst.update_column(tmp_uuid_column_name, association.try(:uuid))
        end

        # 3) Remove id column
        ActiveRecord::Migration.remove_column table_name, association_id_col if ActiveRecord::Migration.column_exists?(table_name, association_id_col)

        # 4) Rename uuid_col_name to id
        ActiveRecord::Migration.rename_column table_name, tmp_uuid_column_name, association_id_col
      rescue => e
        puts "Error: #{e} continuing"
        next
      end
    end

    # Make each temp _uuid column linked up
    # eg. Message.find(1).room_uuid = Message.find(1).room.uuid
    puts "[UPDATE] #{model}.uuid to association uuid"
  end

  ## Migrate primary keys to uuids
  table_names.each do |table_name|
    if ActiveRecord::Migration.column_exists?(table_name, :id) && ActiveRecord::Migration.column_exists?(table_name, :uuid)
      ActiveRecord::Base.connection.execute %Q{ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey CASCADE} rescue nil
      ActiveRecord::Migration.remove_column(table_name, :id)
      ActiveRecord::Migration.rename_column( table_name, :uuid, :id) if ActiveRecord::Migration.column_exists?(table_name, :uuid)
      ActiveRecord::Base.connection.execute "ALTER TABLE #{table_name} ADD PRIMARY KEY (id)"
      ActiveRecord::Base.connection.execute %Q{DROP SEQUENCE IF EXISTS #{table_name}_id_seq CASCADE} rescue nil
    end
  end
end

# Add uuid to the id
# EG. column_name_to_uuid("room_id") => "room_uuid"
# EG. column_name_to_uuid("room_ids") => "room_uuids"
def column_name_to_uuid(column_name)
  *a, b = column_name.split("_id", -1)
  a.join("_id") + "_uuid" + b
end
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!