既存のカラムへの外部キー制約の追加

外部キーは、関連するデータベーステーブル間の一貫性を保証します。現在のデータベースレビュープロセスでは、他のテーブルのレコードを参照するテーブルを作成する際には、常に 外部キーを追加することを推奨しています。

Railsバージョン4から、Railsにはデータベーステーブルに外部キー制約を追加するマイグレーションヘルパーが含まれています。Rails 4以前では、ある程度の一貫性を確保する唯一の方法は、関連付け定義のdependent オプションでした。アプリケーションレベルでデータの一貫性を確保すると、不運なケースで失敗することがあり、テーブルに一貫性のないデータが残ってしまうことがありました。これは主に、データベースレベルで一貫性を保証するフレームワークのサポートがなかった古いテーブルに影響します。このようなデータの不整合は、予期しないアプリケーションの動作やバグを引き起こす可能性があります。

既存のデータベースカラムに外部キーを追加するには、データベース構造の変更と潜在的なデータの変更が必要です。テーブルが使用中である場合、一貫性のないデータが存在することを常に想定しなければなりません。

既存のカラムに外部キー制約を追加するには:

  1. GitLab versionN.M: GitLabが一貫性のないレコードを作成しないように、カラムにNOT VALID 外部キー制約を追加します。
  2. GitLab versionN.M: 既存のレコードを修正またはクリーンアップするために、データマイグレーションを追加しました。
  3. GitLab versionN.M+1: 外部キーをVALID にしてテーブル全体を検証。

物件例

以下のテーブル構造を考えてみましょう:

users テーブルを指定します:

  • id (整数、主キー)。
  • name (文字列)

emails テーブルを指定します:

  • id (整数、主キー)。
  • user_id 整数
  • email (文字列)

ActiveRecord で関係を表します:

class User < ActiveRecord::Base
  has_many :emails
end

class Email < ActiveRecord::Base
  belongs_to :user
end

問題:ユーザーが削除されると、削除されたユーザーに関連する電子メールレコードがemails テーブルに残ります:

user = User.find(1)
user.destroy

emails = Email.where(user_id: 1) # returns emails for the deleted user

無効なレコードの防止

テーブルにNOT VALID 外部キー制約を追加し、レコード変更の一貫性を強制します。

上記の例では、emails テーブルのレコードを更新することはできます。しかし、存在しない値でuser_id を更新しようとすると、制約によってデータベースエラーが発生します。

NOT VALID 外部キーを追加するためのマイグレーションファイル:

class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.1]
  def up
    add_concurrent_foreign_key :emails, :users, column: :user_id, on_delete: :cascade, validate: false
  end

  def down
    remove_foreign_key_if_exists :emails, column: :user_id
  end
end

バリデーションを行わずに外部キーを追加するオペレーションは高速です。新しいデータに制約を適用する前に、テーブルを短時間ロックするだけです。トラフィックが多いテーブルや大きなテーブルでは、ロックの再試行を有効にしたいものです。add_concurrent_foreign_key 、この処理を行います。また、外部キーがすでに存在するかどうかもチェックします。

caution
ソーステーブルとターゲットテーブルが同一でない限り、add_foreign_key またはadd_concurrent_foreign_key 制約をマイグレーションファイルごとに複数回使用することは避けてください。

既存レコードを修正するデータマイグレーション

ここでのアプローチはデータ量とクリーンアップ戦略に依存します。データベースクエリを実行して「無効な」レコードを見つけることができ、レコード数が多くなければ、Railsマイグレーションでデータマイグレーションを実行できます。

データ量が多い場合(>1000レコード)、バックグラウンドマイグレーションを作成したほうがよいでしょう。不明な場合はデータベースチームに問い合わせてください。

データベースマイグレーションでemails テーブルのレコードをクリーンアップする例:

class RemoveRecordsWithoutUserFromEmailsTable < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  class Email < ActiveRecord::Base
    include EachBatch
  end

  def up
    Email.where('user_id NOT IN (SELECT id FROM users)').each_batch do |relation|
      relation.delete_all
    end
  end

  def down
    # Can be a no-op when data inconsistency is not affecting the pre and post deployment version of the application.
    # In this case we might have records in the `emails` table where the associated record in the `users` table is not there anymore.
  end
end

外部キーの検証

外部キーの検証はテーブル全体をスキャンし、各リレーションが正しいことを確認します。幸いなことに、これは実行中にソーステーブル(users)をロックすることはありません。

note
バッチバックグラウンドマイグレーションを使う場合、外部キーの検証は次のGitLabリリースで行われるはずです。

外部キーを検証するためのマイグレーションファイル:

# frozen_string_literal: true

class ValidateForeignKeyOnEmailUsers < Gitlab::Database::Migration[2.1]
  def up
    validate_foreign_key :emails, :user_id
  end

  def down
    # Can be safely a no-op if we don't roll back the inconsistent data.
  end
end

外部キーの非同期検証

非常に大きなテーブルの場合、外部キーのバリデーションが何時間も実行されると管理が大変になります。autovacuum のような必要なデータベースオペレーションは実行できず、GitLab.com ではデプロイプロセスはマイグレーションが終わるのを待ってブロックされます。

GitLab.comへの影響を抑えるために、週末の時間帯に非同期で検証するプロセスが存在します。一般的にトラフィックが少なく、デプロイも少ないため、FKの検証はより低いリスクレベルで進めることができます。

影響の少ない時間帯に外部キーの検証をスケジュールします。

  1. 検証する外部キーをスケジュールします。
  2. MR がデプロイされ、FK が本番環境で有効であることを確認します。
  3. 同期的に FK を検証するマイグレーションを追加します。

検証されるFKのスケジュール

  1. デプロイ後のマイグレーションを含むマージリクエストを作成し、外部キーを非同期検証用に準備します。
  2. 後続のイシューを作成し、外部キーを同期的に検証するマイグレーションを追加します。
  3. 非同期外部キーを準備するマージリクエストに、フォローアップ課題についてのコメントを追加します。

非同期ヘルパーを使用して外部キーを検証する例を以下のブロックに示します。このマイグレーションでは、postgres_async_foreign_key_validations テーブルに外部キー名を入力します。週末に実行されるプロセスはこのテーブルから外部キーを取り出し、検証を試みます。

# in db/post_migrate/

FK_NAME = :fk_be5624bf37

# TODO: FK to be validated synchronously in issue or merge request
def up
  # `some_column` can be an array of columns, and is not mandatory if `name` is supplied.
  # `name` takes precedence over other arguments.
  prepare_async_foreign_key_validation :ci_builds, :some_column, name: FK_NAME

  # Or in case of partitioned tables, use:
  prepare_partitioned_async_foreign_key_validation :p_ci_builds, :some_column, name: FK_NAME
end

def down
  unprepare_async_foreign_key_validation :ci_builds, :some_column, name: FK_NAME

  # Or in case of partitioned tables, use:
  unprepare_partitioned_async_foreign_key_validation :p_ci_builds, :some_column, name: FK_NAME
end

MR がデプロイされ、外部キーが本番環境で有効であることを確認します。

  1. ChatOpsを使用して、デプロイ後のマイグレーションがGitLab.comで実行されたことを/chatops run auto_deploy status <merge_sha> 。出力がdb/gprd を返す場合、デプロイ後のマイグレーションは本番データベースで実行されています。詳細については、デプロイ後のマイグレーションがGitLab.comで実行されたかどうかを判断する方法を参照してください。
  2. 週末にFKを検証できるように、次の週まで待ちましょう。
  3. Database Labを使用して、検証が成功したかどうかを確認します。外部キーがNOT VALID であることを示す出力がないことを確認してください。

FKを同期的に検証するマイグレーションを追加します。

外部キーが本番データベースで有効になった後、外部キーを同期的に検証する 2 番目のマージ・リクエストを作成します。この二番目のマージリクエストでは、スキーマの変更を更新してstructure.sql にコミットする必要があります。同期マイグレーションはGitLab.com上では失敗となりますが、他のインストールでは期待通りにマイグレーションを追加する必要があります。以下のブロックは、先ほどの非同期の例で二回目のマイグレーションを作成する方法を示しています。

caution
validate_foreign_key 。バリデーションが実行される前に2つ目のマイグレーションがデプロイされた場合、2つ目のマイグレーションが実行されるときに外部キーが同期的にバリデーションされます。
# in db/post_migrate/

  FK_NAME = :fk_be5624bf37

  def up
    validate_foreign_key :ci_builds, :some_column, name: FK_NAME
  end

  def down
    # Can be safely a no-op if we don't roll back the inconsistent data.
  end
end

データベースの FK の変更をローカルでテスト

マージリクエストを作成する前に、データベース外部キーの変更をローカルでテストする必要があります。

非同期で検証された外部キーの検証

ローカル環境で非同期ヘルパーを使用して、外部キーを検証するための変更をテストします:

  1. RailsコンソールでFeature.enable(:database_async_foreign_key_validation) 、機能フラグを有効にします。
  2. bundle exec rails db:migrate を実行して、非同期検証テーブルにエントリを作成します。
  3. bundle exec rails gitlab:db:validate_async_constraints:all を実行し、すべてのデータベースで FK が非同期に検証されるようにします。
  4. 外部キーを検証するには、GDKコマンドgdk psql を使用してPostgreSQLコンソールを開き、コマンド\d+ table_name を実行して外部キーが有効であることを確認してください。検証に成功すると、外部キー定義からNOT VALID が削除されます。