既存のカラムへの外部キー制約の追加
外部キーは、関連するデータベーステーブル間の一貫性を保証します。現在のデータベースレビュープロセスでは、他のテーブルのレコードを参照するテーブルを作成する際には、常に 外部キーを追加することを推奨しています。
Railsバージョン4から、Railsにはデータベーステーブルに外部キー制約を追加するマイグレーションヘルパーが含まれています。Rails 4以前では、ある程度の一貫性を確保する唯一の方法は、関連付け定義のdependent
オプションでした。アプリケーションレベルでデータの一貫性を確保すると、不運なケースで失敗することがあり、テーブルに一貫性のないデータが残ってしまうことがありました。これは主に、データベースレベルで一貫性を保証するフレームワークのサポートがなかった古いテーブルに影響します。このようなデータの不整合は、予期しないアプリケーションの動作やバグを引き起こす可能性があります。
既存のデータベースカラムに外部キーを追加するには、データベース構造の変更と潜在的なデータの変更が必要です。テーブルが使用中である場合、一貫性のないデータが存在することを常に想定しなければなりません。
既存のカラムに外部キー制約を追加するには:
- GitLab version
N.M
: GitLabが一貫性のないレコードを作成しないように、カラムにNOT VALID
外部キー制約を追加します。 - GitLab version
N.M
: 既存のレコードを修正またはクリーンアップするために、データマイグレーションを追加しました。 - GitLab version
N.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
、この処理を行います。また、外部キーがすでに存在するかどうかもチェックします。
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
)をロックすることはありません。
外部キーを検証するためのマイグレーションファイル:
# 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の検証はより低いリスクレベルで進めることができます。
影響の少ない時間帯に外部キーの検証をスケジュールします。
- 検証する外部キーをスケジュールします。
- MR がデプロイされ、FK が本番環境で有効であることを確認します。
- 同期的に FK を検証するマイグレーションを追加します。
検証されるFKのスケジュール
- デプロイ後のマイグレーションを含むマージリクエストを作成し、外部キーを非同期検証用に準備します。
- 後続のイシューを作成し、外部キーを同期的に検証するマイグレーションを追加します。
- 非同期外部キーを準備するマージリクエストに、フォローアップ課題についてのコメントを追加します。
非同期ヘルパーを使用して外部キーを検証する例を以下のブロックに示します。このマイグレーションでは、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 がデプロイされ、外部キーが本番環境で有効であることを確認します。
- ChatOpsを使用して、デプロイ後のマイグレーションがGitLab.comで実行されたことを
/chatops run auto_deploy status <merge_sha>
。出力がdb/gprd
を返す場合、デプロイ後のマイグレーションは本番データベースで実行されています。詳細については、デプロイ後のマイグレーションがGitLab.comで実行されたかどうかを判断する方法を参照してください。 - 週末にFKを検証できるように、次の週まで待ちましょう。
-
Database Labを使用して、検証が成功したかどうかを確認します。外部キーが
NOT VALID
であることを示す出力がないことを確認してください。
FKを同期的に検証するマイグレーションを追加します。
外部キーが本番データベースで有効になった後、外部キーを同期的に検証する 2 番目のマージ・リクエストを作成します。この二番目のマージリクエストでは、スキーマの変更を更新してstructure.sql
にコミットする必要があります。同期マイグレーションはGitLab.com上では失敗となりますが、他のインストールでは期待通りにマイグレーションを追加する必要があります。以下のブロックは、先ほどの非同期の例で二回目のマイグレーションを作成する方法を示しています。
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 の変更をローカルでテスト
マージリクエストを作成する前に、データベース外部キーの変更をローカルでテストする必要があります。
非同期で検証された外部キーの検証
ローカル環境で非同期ヘルパーを使用して、外部キーを検証するための変更をテストします:
- Railsコンソールで
Feature.enable(:database_async_foreign_key_validation)
、機能フラグを有効にします。 -
bundle exec rails db:migrate
を実行して、非同期検証テーブルにエントリを作成します。 -
bundle exec rails gitlab:db:validate_async_constraints:all
を実行し、すべてのデータベースで FK が非同期に検証されるようにします。 - 外部キーを検証するには、GDKコマンド
gdk psql
を使用してPostgreSQLコンソールを開き、コマンド\d+ table_name
を実行して外部キーが有効であることを確認してください。検証に成功すると、外部キー定義からNOT VALID
が削除されます。