在Rails中,在每个模型上实现多个UUID和ID的最佳方法是什么?

8

将一个大型遗留代码库从UUID转换为ID。这需要分阶段进行,以保持对许多设备的向后兼容性。

目前的解决方案是保留UUID和ID字段,直到我们可以完全过渡为止。

最佳方法是什么,以便所有的belongs_to模型在每次创建/更新时都更新ID和UUID?

例如:评论模型属于BlogPost并且需要在创建/更新时设置blogpost_idblogpost_uuid


1
您对ID是否需要连续或从0开始有任何要求吗?如果没有,您可以直接使用已经存在的UUID字段,创建新列,将数据复制过去并将其标记为主键。 - Mark
UUID列仍需要更新(因为它仍在内部使用),这就是为什么我不认为我们可以简单地切换主键定义的原因。 - Matt Privman
你看过这个吗?https://dev59.com/YmAf5IYBdhLWcg3wcyar#24643105 看起来可能有一些线索。 - Dan
4个回答

7

只需通过数据库完成:

假设您有这样的遗留表格

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。

最重要的是避免停机时间:

  • 创建列
  • 确保新记录以某种方式填充默认值(默认或触发器)
  • 回溯旧记录
  • 添加约束
  • 切换到新列
  • 然后可以删除旧列(如果确定它没有在使用)

注:不确定为什么您要完全从UUID切换,如果您想要从外部应用程序引用记录,则UUID更好。

注2.0:如果您需要能够执行Legacy.find("fb360410-0403-4388-9eac-c35f676f8368")Legacy.find(123),也许可以尝试https://github.com/norman/friendly_id

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

我不能使用这个,因为我们使用的是MySQL 5.7,但这个答案非常好。真的让我希望我们使用的是Postgres。 - Matt Privman
mysql中不见问题,有UUID和auto_increment的唯一性。https://paiza.io/projects/driuwZ567qVl8SsAqQ107A?language=mysql您可以手动填充id列,然后执行ALTER TABLE Test MODIFY COLUMN id INT UNIQUE NOT NULL AUTO_INCREMENT;,也可以使用CREATE TRIGGER test_uuid_generator BEFORE INSERT ON Test FOR EACH ROW SET new.uuid = IF(new.uuid IS NOT NULL, new.uuid, UUID());为uuid创建触发器。 - bliof

1
你可以使用这个 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,我建议您执行以下操作进行迁移:

  • 将系统置于维护模式
  • 将Blogpost中的uuid复制到Comment的blogpost_uuid,您可以执行以下操作:
Comment.preload(:blogpost).find_each do |comment|
  comment.update_column(blogpost_uuid: blogpost.uuid)
end
  • 发布新的更新,包括复合主键宝石和代码更改
  • 关闭维护模式

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


1
例如,在您的评论模型上,您可以添加一个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


0
首先,你的问题的答案可能严重依赖于你使用的数据库管理系统,因为一些数据库管理系统比其他系统更适合处理这种情况。对于我的回答,我将假设你正在使用Postgres。
那么让我们开始吧。
从概念上讲,你正在处理外键。Postgres(像许多其他数据库管理系统一样)提供了内置的外键,并允许你做几乎所有的事情,包括在同一张表之间建立多个外键关系。因此,如果你还没有这样做,第一步就是为受影响的表之间的整数和UUID列设置外键关系。结果应该是,在comments.blogpost_idblogposts.id以及comments.blogpost_uuidblogposts.uuid之间建立外键。这个设置将确保在删除整数列后,你的数据库内容保持一致。
第二步是确保在设置一个值时,两个值都被写入。你可以在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。这是我会采取的方式,因为它可以增加一致性并将偶然的临时复杂性排除在应用程序代码之外(如果您想要,仍然可以编写单元测试)。

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

希望这有意义。如果您有任何后续问题,请告诉我。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接