在 Rails 中的每个模型上实现多个 UUID 和 ID 的最佳方法

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

将大型遗留代码库从 UUID 转换为 ID。这需要分阶段完成,以保持许多设备之间的向后兼容性。

当前的解决方案是同时维护 UUID 和 ID 字段,直到我们可以完全过渡。

执行此操作的最佳方法是什么,以便所有 belongs_to 模型都更新每个 create/update 上的 ID 和 UUID?

示例:评论模型属于 BlogPost,需要在 create/update 上同时设置 blogpost_idblogpost_uuid

例如,在您的 Comment 模型上,您可以添加一个 before_save 回调,它会在模型​​创建和更新时被调用。在回调方法中,您可以引用关联并确保在评论中更新必要的字段。

# app/models/comment.rb

belongs_to :blogpost

# Add callback, gets called before create and update
before_save :save_blogpost_id_and_uuid

# At the bottom of your model
private

def save_blogpost_id_and_uuid
  # You usually don't have to explicitly set the blogpost_id
  # because Rails usually handles it. But you might have to 
  # depending on your app's implementation of UUIDs. Although it's
  # probably safer to explicitly set them just in case.

  self.blogpost_uuid = blogpost.uuid
  self.blogpost_id = blogpost.id
end

然后对其他模型及其关联重复上述方法。

如果需要,您可以添加一些条件逻辑,仅在博文 ID 或 UUID 更改时更新 blogpost_idblogpost_uuid

首先:您问题的答案可能在很大程度上取决于您使用的 DBMS,因为某些 DBMS 比其他 DBMS 具有更好的处理这类问题的能力。对于我的回答,我假设您使用的是 Postgres。

让我们开始吧。

从概念上讲,您在这里处理的是外键。 Postgres(与许多其他 DBMS 一样)提供内置外键,它允许您做几乎所有事情——包括在相同表之间建立多个外键关系。因此,第 1 步(如果您尚未完成)是为您的整数列和 UUID 列在受影响的表之间设置外键关系。结果应该是 comments.blogpost_idblogposts.id 以及 comments.blogpost_uuidblogposts.uuid 之间有外键。一旦您删除整数列,此设置将确保您的数据库内容保持一致。

第 2 步确保在设置一个值时始终写入两个值。您可以在 Rails 中以类似于 bwalshy 的评论的方式稍作调整来执行此操作:

self.blogpost_id ||= BlogPost.find_by(uuid: blogpost_uuid)&.id if blogpost_uuid.present?
self.blogpost_uuid ||= BlogPost.find_by(id: blogpost_id)&.uuid if blogpost_id.present?

或者您可以再次让您的 DBMS 完成其工作并设置触发器来处理 INSERT/UPDATE 上的这些事情。这是我会采用的方式,因为它再次提高了一致性并避免了应用程序代码中附带的临时复杂性(如果需要,您仍然可以为此编写单元测试)。

第 3 步是回填任何现有数据,并对所有涉及外键关系的列设置 NOT NULL 约束以确保完全一致。

我希望这是有道理的。如果您有任何后续问题,请告诉我。

您可以使用此 gem 为主键定义多个键:https://github.com/composite-primary-keys/composite_primary_keys

class Blogpost
  self.primary_keys = :uuid, :id

  has_many :comments, foreign_key: [:uuid, :id]
end

class Comment
  belongs_to :blogpost, foreign_key: [:blogpost_uuid, :blogpost_id]
end

如果您已经为 BlogPost 生成 UUID 和 ID 并与 Comment 的 blogpost_uuidblogpost_id

同步,那么它会起作用

如果您还没有同步 blogpost_uuidblogpost_id,我建议您执行以下迁移操作:

  • 将您的系统置于维护模式
  • uuid 从博文复制到评论的 blogpost_uuid,您可以:
Comment.preload(:blogpost).find_each do |comment|
  comment.update_column(blogpost_uuid: blogpost.uuid)
end
  • 使用复合主键 gem 和代码更改发布新更新
  • 关闭维护模式

希望它能帮助您顺利过渡。 如果有什么不清楚的地方,请告诉我。

直接通过数据库来做:

假设您有这样的旧表

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

使用上面的代码你有:

Legacy.create.id        # => "fb360410-0403-4388-9eac-c35f676f8368"
AnotherLegacy.create.id # => "dd45b2db-13c2-4ff1-bcad-3718cd119440"

现在添加新的 id 列

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

默认值是在您创建新列后添加的,因为这会阻止数据库尝试更新所有记录。 => 默认值将是新记录的默认值。

旧的可以随意回填

例如一个接一个

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

如果您愿意,可以先回填,然后添加默认值,然后再次回填。

如果您对这些值感到满意,只需更改主键即可:

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"

最后,您还需要进行一次迁移,将主键更改为新 ID。

最重要的是避免停机:

  • 创建一列
  • 确保新记录以某种方式默认填充(默认或触发)
  • 回填旧记录
  • 添加约束条件
  • 切换到新栏目
  • 那么你可以放弃旧的(如果你确定它没有被使用)

ps。不确定你为什么要完全从 uuid 切换,如果你想从外部应用程序引用记录,它们会更好

ps.2.0。如果你需要能够做到 Legacy.find("fb360410-0403-4388-9eac-c35f676f8368")Legacy.find(123) 也许试试 https://github.com/norman/friendly_id

friendly_id :uuid, use: [:slugged, :finders]