在 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_id
和 blogpost_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_id
和 blogpost_uuid
。
首先:您问题的答案可能在很大程度上取决于您使用的 DBMS,因为某些 DBMS 比其他 DBMS 具有更好的处理这类问题的能力。对于我的回答,我假设您使用的是 Postgres。
让我们开始吧。
从概念上讲,您在这里处理的是外键。 Postgres(与许多其他 DBMS 一样)提供内置外键,它允许您做几乎所有事情——包括在相同表之间建立多个外键关系。因此,第 1 步(如果您尚未完成)是为您的整数列和 UUID 列在受影响的表之间设置外键关系。结果应该是 comments.blogpost_id
和 blogposts.id
以及 comments.blogpost_uuid
和 blogposts.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_uuid
、blogpost_id
同步,那么它会起作用
如果您还没有同步 blogpost_uuid
和 blogpost_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]
将大型遗留代码库从 UUID 转换为 ID。这需要分阶段完成,以保持许多设备之间的向后兼容性。
当前的解决方案是同时维护 UUID 和 ID 字段,直到我们可以完全过渡。
执行此操作的最佳方法是什么,以便所有 belongs_to
模型都更新每个 create/update 上的 ID 和 UUID?
示例:评论模型属于 BlogPost,需要在 create/update 上同时设置 blogpost_id
和 blogpost_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_id
和 blogpost_uuid
。
首先:您问题的答案可能在很大程度上取决于您使用的 DBMS,因为某些 DBMS 比其他 DBMS 具有更好的处理这类问题的能力。对于我的回答,我假设您使用的是 Postgres。
让我们开始吧。
从概念上讲,您在这里处理的是外键。 Postgres(与许多其他 DBMS 一样)提供内置外键,它允许您做几乎所有事情——包括在相同表之间建立多个外键关系。因此,第 1 步(如果您尚未完成)是为您的整数列和 UUID 列在受影响的表之间设置外键关系。结果应该是 comments.blogpost_id
和 blogposts.id
以及 comments.blogpost_uuid
和 blogposts.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_uuid
、blogpost_id
如果您还没有同步 blogpost_uuid
和 blogpost_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]