【问题标题】:Rails 4. Migrate table id to UUIDRails 4. 将表 id 迁移到 UUID
【发布时间】:2014-10-30 03:42:43
【问题描述】:

我有一张桌子: db/migrate/20140731201801_create_voc_brands.rb:

class CreateVocBrands < ActiveRecord::Migration
  def change
    create_table :voc_brands do |t|
      t.string :name

      t.timestamps
    end
  end
end

但我需要将表更改为这个(如果我要从零开始创建它):

class CreateVocBrands < ActiveRecord::Migration
  def change
    create_table :voc_brands, :id => false do |t|
      t.uuid :id, :primary_key => true
      t.string :name

      t.timestamps
    end
    add_index :voc_brands, :id
  end
end

如何使用迁移来改变这一点?

【问题讨论】:

  • 你能解释一下你想做什么吗?您是否已迁移此文件并想要进行更改,或者您想在新的迁移文件中进行更改?
  • 不,我没有做任何更改。我有 1 个迁移 CreateVocBrands(第一个代码列表)。如果我要从零创建表 VocBrands,我需要运行 ChangeVocBrandsmigration (问题应该是迁移)以将表更改为二维列表
  • 我能问一下你为什么想要UUID吗?当然,您可以将其作为单独的列应用,而留下 id 主键?如果你有正当理由,我会帮忙的!

标签: ruby-on-rails migration rails-migrations


【解决方案1】:

我遇到了和你一样的问题。要从默认 id 迁移到使用 uuid,我认为您可以使用类似于我所拥有的东西:

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

【讨论】:

  • 该解决方案在这种情况下有效,因为 voc_brands 表没有数据,否则可能会损坏数据,特别是如果 voc_brands 有依赖关系。
  • 我知道两次生成相同 UUID 的机会是 slim 但您的示例是否保证 id 是唯一的?
  • 这可行,但我必须将add_column 默认声明的格式更新为default: -&gt; { "gen_random_uuid()" }
【解决方案2】:

我知道迁移是进行任何数据库更改的首选方法,但以下方法很棒。可以使用对 PostgreSQL 的直接查询来使用现有数据转换表。

对于主键:

    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()

其他参考:

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

左上角用零填充整数值并转换为 UUID。这种方法不需要 id 映射,如果需要,可以检索旧 id。

由于没有数据复制,这种方法运行速度很快。

要处理这些和更复杂的多态关联情况,请使用https://github.com/kreatio-sw/webdack-uuid_migration。这个 gem 为 ActiveRecord::Migration 添加了额外的帮助器来简化这些迁移。

【讨论】:

  • 我知道这是一年中最好的部分,但我回来的唯一目的是支持您在github.com/kreatio-sw/webdack-uuid_migration 引用宝石。我今天发现了外键引用的问题并为此提交了 PR,并且可以说即使在 5.2.0.alpha 版本的 rails 上它也很好用!几个月前我们试图用手做同样的事情,这非常痛苦。感谢分享! +1
  • 是的,webdack-uuid_migration 同上!回来也赞成这个。伟大的包,它的维护者努力支持。我认为它的保质期会很长,因为它不会疯狂​​地尝试使用 ActiveRecord 内部组件。绝对推荐!
【解决方案3】:

我知道这并不能直接回答问题,但我创建了一个 rake 任务,可以帮助将任何项目从 id 转换为 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

【讨论】:

  • 为什么同时启用uuid-ossppgcrypto 扩展?从 PostgreSQL 9.5 开始,如果您只使用 uuid 生成功能,建议使用 pgcrypto。然后,使用 pgcrypto,您可以将 uuid_generate_v4() 替换为 gen_random_uuid()。可能我错过了您的代码中的某些内容,而您确实需要两者。
  • 脚本可以同时适用于新旧迁移。我想如果您的项目相当新,您可以删除 uuid-ossp 扩展名。但这更安全。
【解决方案4】:

如果有人来这里寻找如何从 UUID 转换为整数 ID,您可以使用以下迁移:

class ChangeUuidToInteger < ActiveRecord::Migration::Current
  def change
    ### LOAD ALL MODELS for `.subclasses` method
    Dir.glob(Rails.root.join("app/models/*.rb")).each{|f| require(f) }

    id_map = {}

    ApplicationRecord.subclasses.each do |outer_klass|
      outer_klass.reset_column_information

      if outer_klass.column_for_attribute(outer_klass.primary_key).type == :uuid
        case outer_klass.connection.adapter_name
        when "Mysql2"
          execute "ALTER TABLE #{outer_klass.table_name} DROP PRIMARY KEY;"
        else
          result = outer_klass.connection.execute("
            SELECT ('ALTER TABLE ' || table_schema || '.' || table_name || ' DROP CONSTRAINT ' || constraint_name) as my_query
            FROM information_schema.table_constraints
            WHERE table_name = '#{outer_klass.table_name}' AND constraint_type = 'PRIMARY KEY';")
              
          sql_drop_constraint_command = result.values[0].first

          execute(sql_drop_constraint_command)
        end

        rename_column outer_klass.table_name, outer_klass.primary_key, "tmp_old_#{outer_klass.primary_key}"

        add_column outer_klass.table_name, outer_klass.primary_key, outer_klass.connection.native_database_types[:primary_key]

        outer_klass.reset_column_information

        records = outer_klass.all

        if outer_klass.column_names.include?("created_at")
          records = records.reorder(created_at: :asc)
        end

        id_map[outer_klass] = {}

        records.each_with_index do |record, i|
          old_id = record.send("tmp_old_#{outer_klass.primary_key}")

          if record.send(outer_klass.primary_key).nil?
            new_id = i+1
            record.update_columns(outer_klass.primary_key => new_id)
          else
            new_id = record.send(outer_klass.primary_key)
          end

          id_map[outer_klass][old_id] = new_id
        end

        remove_column outer_klass.table_name, "tmp_old_#{outer_klass.primary_key}"

        outer_klass.reset_column_information
      end
    end

    ApplicationRecord.subclasses.each do |inner_klass|
      inner_klass.reflect_on_all_associations(:belongs_to).each do |reflection|
        if inner_klass.column_for_attribute(reflection.foreign_key).type == :uuid
          if reflection.polymorphic?
            ### POLYMORPHIC BELONGS TO
            
            #null_constraint = inner_klass.columns.find{|x| x.name == reflection.foreign_key }.null
            if inner_klass.connection.index_exists?(inner_klass.table_name, reflection.foreign_key)
              remove_index inner_klass.table_name, reflection.foreign_key
            end
            rename_column inner_klass.table_name, reflection.foreign_key, "tmp_old_#{reflection.foreign_key}"
            add_column inner_klass.table_name, reflection.foreign_key, :bigint#, null: null_constraint
            add_index inner_klass.table_name, reflection.foreign_key

            inner_klass.reset_column_information
            
            id_map.each do |outer_klass, inner_id_map|
              records = inner_klass
                .where("#{inner_klass.table_name}.tmp_old_#{reflection.foreign_key} IS NOT NULL")
                .where("#{reflection.foreign_type}" => outer_klass.name)

              records.each do |record|
                old_id = record.send("tmp_old_#{reflection.foreign_key}")

                if old_id
                  new_id = inner_id_map[old_id]

                  if new_id
                    ### First Update Column ID Value
                    record.update_columns(reflection.foreign_key => new_id)
                  else
                    # Orphan record, we just clear the value
                    record.update_columns(reflection.foreign_key => nil)
                  end
                end
              end
            end

            ### Then Change Column Type
            remove_column inner_klass.table_name, "tmp_old_#{reflection.foreign_key}"

            inner_klass.reset_column_information

          elsif id_map[reflection.klass]
            ### DIRECT BELONGS TO
            
            inner_id_map = id_map[reflection.klass]

            #null_constraint = inner_klass.columns.find{|x| x.name == reflection.foreign_key }.null
            if inner_klass.connection.index_exists?(inner_klass.table_name, reflection.foreign_key)
              remove_index inner_klass.table_name, reflection.foreign_key
            end
            rename_column inner_klass.table_name, reflection.foreign_key, "tmp_old_#{reflection.foreign_key}"
            add_column inner_klass.table_name, reflection.foreign_key, :bigint#, null: null_constraint
            add_index inner_klass.table_name, reflection.foreign_key

            inner_klass.reset_column_information

            records = inner_klass.where("#{inner_klass.table_name}.tmp_old_#{reflection.foreign_key} IS NOT NULL")

            records.each do |record|
              old_id = record.send("tmp_old_#{reflection.foreign_key}")

              if old_id
                new_id = inner_id_map[old_id]

                if new_id
                  ### First Update Column ID Value
                  record.update_columns(reflection.foreign_key => new_id)
                else
                  # Orphan record, we just clear the value
                  record.update_columns(reflection.foreign_key => nil)
                end
              end
            end

            ### Then Change Column Type
            remove_column inner_klass.table_name, "tmp_old_#{reflection.foreign_key}"

            inner_klass.reset_column_information
          end
        end
      end

      inner_klass.reflect_on_all_associations(:has_and_belongs_to_many).each do |reflection|
        if id_map[reflection.klass]
          inner_id_map = id_map[reflection.klass]

          #null_constraint = join_klass.columns.find{|x| x.name == reflection.foreign_key }.null
          if inner_klass.connection.index_exists?(reflection.join_table, reflection.association_foreign_key)
            remove_index reflect.join_table, reflection.association_foreign_key
          end
          rename_column reflect.join_table, reflection.association_foreign_key, "tmp_old_#{reflection.association_foreign_key}"
          add_column reflect.join_table, reflection.association_foreign_key, :bigint
          add_index reflect.join_table, reflection.association_foreign_key

          inner_id_map.each do |old_id, new_id|
            if new_id
              ### First Update Column ID Value
              execute "UPDATE #{reflection.join_table} SET #{reflection.association_foreign_key} = '#{new_id}' WHERE tmp_old_#{reflection.association_foreign_key} = '#{old_id}'"
            end
          end

          execute "DELETE FROM #{reflection.join_table} WHERE tmp_old_#{reflection.association_foreign_key} NOT IN ('#{inner_id_map.values.join("','")}')"

          remove_column reflection.join_table, "tmp_old_#{reflection.association_foreign_key}"

          #join_klass.reset_column_information
        end
      end
    end
  end

end

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-05-19
    • 2014-06-09
    • 2017-03-27
    • 2014-02-28
    • 1970-01-01
    • 1970-01-01
    • 2018-02-02
    • 1970-01-01
    相关资源
    最近更新 更多