Лучший способ реализовать несколько UUID и ID для каждой модели в Rails - PullRequest
5 голосов
/ 25 февраля 2020

Переход большой устаревшей кодовой базы с UUID на идентификаторы. Это необходимо сделать поэтапно для обеспечения обратной совместимости между многими устройствами.

Текущее решение состоит в том, чтобы поддерживать поля UUID и ID до тех пор, пока мы не сможем перейти полностью.

Какой лучший способ сделать так, чтобы все belongs_to модели обновляли и идентификатор, и UUID при каждом создании / обновлении?

Пример: модель комментариев принадлежит BlogPost и для нее необходимо установить оба значения blogpost_id & blogpost_uuid при создании / обновлении .

Ответы [ 4 ]

3 голосов
/ 07 марта 2020

Просто сделайте это через базу данных:

Допустим, у вас есть такие устаревшие таблицы

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"

Теперь добавить новый столбец идентификатора

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

Значение по умолчанию добавляется после создания нового столбца, поскольку это не позволяет БД попытаться обновить все записи. => по умолчанию будет по умолчанию только для новых записей.

Старый, который вы можете засыпать, когда будете sh.

Например, один за другим

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"

Наконец вы нужна еще одна миграция, чтобы заменить первичный ключ на новый идентификатор.

Самое главное, чтобы избежать простоев:

  • создать столбец
  • обеспечить заполнение новых записей по умолчанию каким-то образом (по умолчанию или триггером)
  • засыпать старую запись
  • добавить ограничения
  • переключиться на новый столбец
  • , тогда вы можете удалить старый (если вы уверены, что он не используется)

пс. Не уверен, почему вы хотите полностью переключиться с uuids, они лучше, если вы хотите ссылаться на записи из внешних приложений

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]
1 голос
/ 06 марта 2020

Прежде всего: ответ на ваш вопрос может сильно зависеть от СУБД, которую вы используете, потому что некоторые СУБД имеют лучшие возможности для такого рода вещей, чем другие. Для моего ответа, я предполагаю, что вы используете Postgres.

Итак, начнем.

С концептуальной точки зрения вы имеете дело с внешними ключами здесь. Postgres (как и многие другие СУБД) предлагает встроенные внешние ключи, и это позволяет вам делать практически все, в том числе устанавливать множественные отношения внешних ключей между одними и теми же таблицами. Таким образом, шаг 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?

Или вы можете, опять же, позволить вашей СУБД выполнить свою работу и настроить триггеры, которые обрабатывают эти вещи на INSERT / UPDATE. Я бы выбрал этот способ, потому что это снова повышает согласованность и исключает случайную временную сложность из кода приложения (вы можете написать для этого модульные тесты, если хотите).

Шаг 3 - это засыпьте все существующие данные и установите ограничения NOT NULL для всех столбцов, связанных с отношениями внешних ключей, чтобы обеспечить полную согласованность.

Надеюсь, это имеет смысл. Дайте мне знать, если у вас есть дополнительные вопросы.

1 голос
/ 06 марта 2020

Вы можете определить несколько ключей для первичного ключа, используя этот гем: 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

Это будет работать, если вы уже сгенерировали UUID и ID для BlogPost и синхронизировали с комментариями blogpost_uuid, blogpost_id

Если вы не синхронизировали blogpost_uuid и blogpost_id, я рекомендую вам выполнить следующие действия для миграции:

  • Переведите систему в режим обслуживания В режиме
  • Скопируйте uuid из Blogpost в blogpost_uuid комментария, вы можете сделать:
Comment.preload(:blogpost).find_each do |comment|
  comment.update_column(blogpost_uuid: blogpost.uuid)
end
  • Выпустить новое обновление с составным гемом первичного ключа и изменением кода
  • Отключить режим обслуживания

Надеюсь, это поможет вам сделать плавный переход. Дайте мне знать, если что-то не понятно.

1 голос
/ 25 февраля 2020

Например, в вашей модели комментариев вы можете добавить обратный вызов 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

А затем повторите описанный выше метод для других моделей и их ассоциаций.

При желании вы можете добавить некоторые условные логи c, которые обновляют blogpost_id и blogpost_uuid только в случае изменения идентификатора блога или UUID.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...