- 適切なマイグレーションタイプを選択してください。
- 対象とするデータベースの決定
- 通常のスキーママイグレーションの作成
- スキーマの変更
- ダウンタイムの回避
- 可逆性
- 原子性とトランザクション
- 命名規則
- マイグレーション・ヘルパーとバージョニング
- データベースロック取得時のリトライメカニズム
- インデックスの削除
- インデックスの追加
- インデックスの存在のテスト
- 外部キー制約の追加
NOT NULL
制約- デフォルト値を持つカラムの追加
- NULLにできない列のデフォルトの削除
- カラムのデフォルトの変更
- 既存のカラムの更新
- 外部キー制約の削除
- データベース・テーブルの削除
- シーケンスの削除
- テーブルの切り捨て
- 主キーの入れ替え
- 整数カラム型
- 文字列とTextデータ型
- タイムスタンプ列型
- JSONのデータベースへの保存
- 暗号化された属性
- テスト
- データマイグレーション
- マイグレーションでのアプリケーションコードの使用 (推奨しません)
- トラフィックの多いテーブル
マイグレーション・スタイルガイド
GitLabのマイグレーションを書くときは、何十万ものあらゆる規模の組織で実行され、データベースには何年ものデータがあることを考慮しなければなりません。
さらに、アップグレードの大小に関わらず、サーバーをオフラインにしなければならないことは、ほとんどの組織にとって大きな負担です。このような理由から、マイグレーションは慎重に書かれ、オンラインで適用でき、以下のスタイルガイドを遵守することが重要です。
マイグレーションでは、GitLabのインストールをオフラインにする必要はありません。マイグレーションは常にダウンタイムを避けるように書かなければなりません。過去には、DOWNTIME
定数を設定することでダウンタイムを許容するマイグレーション定義のプロセスがありました。古いマイグレーションを見るとわかるかもしれません。このプロセスは一度も使われることなく4年間実施されました。そのため、ダウンタイムを回避するためにマイグレーションを別の方法で書く方法を常に見つけ出すことができることを学びました。
マイグレーションを作成する際には、データベースに古いデータや不整合があるかもしれないことも考慮し、そのような事態に備えましょう。データベースの状態については、できるだけ仮定しないようにしてください。
GitLab固有のコードは将来のバージョンで変わる可能性があるので、依存しないでください。必要であれば、マイグレーションにGitLabのコードをコピーペーストして互換性を持たせてください。
適切なマイグレーションタイプを選択してください。
新しいマイグレーションを追加する前の最初のステップは、どのタイプが最も適切かを決めることです。
現在、マイグレーションには、実行する必要のある作業の種類と完了までにかかる時間に応じて、3つの種類があります:
-
通常のスキーママイグレーション。
db/migrate
、新しいアプリケーションコードがデプロイされる_前_(GitLab.comの場合はCanaryがデプロイされる前_)に_実行される従来のRailsマイグレーションです。つまり、デプロイを不必要に遅らせないように、数分以内の比較的高速なものでなければなりません。ただし、アプリケーションを正しくオペレーションするために絶対に必要なマイグレーションは例外です。例えば、一意なタプルを強制するインデックスや、アプリケーションの重要な部分でクエリのパフォーマンスに必要なインデックスがあるかもしれません。しかし、マイグレーションに許容できないほど時間がかかるような場合、機能フラグでその機能を保護し、代わりにデプロイ後のマイグレーションを実行する方が良い選択肢かもしれません。マイグレーションが終了した後に、この機能を有効にすることができます。
新しいモデルを追加するためのマイグレーションも、通常のスキーママイグレーションの一部です。唯一の違いは、マイグレーションを生成するRailsコマンドと、生成される追加ファイル(モデル用とモデルのspec用)です。
-
デプロイ後のマイグレーション。これらは
db/post_migrate
の Rails マイグレーションで、GitLab.com のデプロイとは独立して実行されます。保留中のポストマイグレーションは、デプロイ後のマイグレーションパイプラインを通じて、リリースマネージャの判断で毎日実行されます。これらのマイグレーションは、アプリケーションのオペレーションにとって重要でないスキーマの変更や、せいぜい数分しかかからないデータマイグレーションに使うことができます。ポストデプロイで実行すべきスキーマ変更の一般的な例には、次のようなものがあります:- 未使用カラムの削除などのクリーンアップ。
- トラフィックの多いテーブルに重要でないインデックスを追加。
- 作成に時間がかかる重要でないインデックスの追加。
これらのマイグレーションは、アプリケーションの動作に不可欠なスキーマの変更には使用しないでください。デプロイ後のマイグレーションでこのようなスキーマ変更を行うと、過去にイシューなどの問題が発生しました。常に通常のスキーママイグレーションを行うべきであり、デプロイ後のマイグレーションで実行すべきではない変更には、以下のようなものがあります:
- 新しいテーブルの作成 例:
create_table
. - 既存のテーブルへの新しいカラムの追加、例:
add_column
.
-
バッチバックグラウンドマイグレーション。これは通常のRailsマイグレーションではなく、Sidekiqジョブを介して実行されるアプリケーションコードです。デプロイ後マイグレーションのタイミングガイドラインを超えるデータマイグレーションにのみ使用してください。バッチバックグラウンドマイグレーションでは、スキーマを変更_しないで_ください。
以下の図を参考に決定してください。ただし、これは単なるツールであり、最終的な結果は常に具体的な変更内容に依存することに留意してください:
マイグレーションにかかる時間
一般的に、GitLab.comでは1回のデプロイにかかるマイグレーションに1時間以上かかることはありません。以下のガイドラインは厳密なルールではなく、マイグレーションにかかる時間を最小限に抑えるために見積もられたものです。
マイグレーションタイプ | 推奨期間 | 備考 |
---|---|---|
定期マイグレーション | <= 3 minutes | 有効な例外は、アプリケーションの機能またはパフォーマンスが著しく低下し、遅延させることができない変更です。 |
デプロイ後のマイグレーション | <= 10 minutes | スキーマの変更はバックグラウンドマイグレーションで起こってはならないので、有効な例外です。 |
バックグラウンドマイグレーション | > 10 minutes |
1 second これらは大規模なテーブルに適しているため、正確なタイミングガイドラインを設定することはできませんが、コールドキャッシュを使用する場合、どのクエリも実行時間 を下回る必要があります。 |
対象とするデータベースの決定
GitLab は2つの異なる Postgres データベースに接続します:main
とci
。この分割はマイグレーションに影響を与える可能性があります。
追加するマイグレーションがこのことを考慮すべきなのか、あるいはどのように考慮すべきなのかを理解するには、複数データベースのマイグレーションを読んでください。
通常のスキーママイグレーションの作成
マイグレーションを作成するには、以下のRailsジェネレータを使用できます:
bundle exec rails g migration migration_name_here
db/migrate
でマイグレーションファイルを生成します。
新しいモデルを追加する通常のスキーママイグレーション
新しいモデルを作成するには、次のRailsジェネレータを使います:
bundle exec rails g model model_name_here
これで生成されます:
- のマイグレーションファイルが生成されます。
db/migrate
- のモデルファイル
app/models
- スペックファイル
spec/models
スキーマの変更
スキーマへの変更はdb/structure.sql
にコミットする必要があります。 このファイルはbundle exec rails db:migrate
を実行すると Rails によって自動的に生成されるので、通常はこのファイルを手で編集すべきではありません。マイグレーションでテーブルにカラムを追加する場合、そのカラムは一番下に追加されます。Railsが生成したdb/structure.sql
を使っている他の人を混乱させることになるので、既存のテーブルのカラムを手動で並べ替えないでください。
add_concurrent_index
でコミットしてください。GDK の内部データベースがmain
のスキーマと異なっている場合、スキーマの変更を Git にきれいにコミットするのは難しいでしょう。そのような場合は、scripts/regenerate-schema
スクリプトを使って、追加するマイグレーション用のdb/structure.sql
をきれいに再生成することができます。このスクリプトはdb/migrate
やdb/post_migrate
で見つかったマイグレーションをすべて適用します。スキーマにコミットしたくないマイグレーションがある場合は、名前を変更するか削除してください。ブランチがデフォルトの Git ブランチをターゲットにしていない場合は、環境変数TARGET
を設定します。
# Regenerate schema against `main`
scripts/regenerate-schema
# Regenerate schema against `12-9-stable-ee`
TARGET=12-9-stable-ee scripts/regenerate-schema
scripts/regenerate-schema
スクリプトは、さらなる差分を作成する可能性があります。このような場合は、<migration ID>
をマイグレーションファイルのDATETIME
の部分とする手動手順を使用してください。
# Rebase against master
git rebase master
# Rollback changes
VERSION=<migration ID> bundle exec rails db:rollback:main
# Checkout db/structure.sql from master
git checkout origin/master db/structure.sql
# Migrate changes
VERSION=<migration ID> bundle exec rails db:migrate:main
テーブルが作成されたら、データベース辞書ガイドに記載されている手順に従って、そのテーブルをデータベース辞書に追加する必要があります。
ダウンタイムの回避
マイグレーションにおけるダウンタイムの回避」というドキュメントでは、以下のような様々なデータベースオペレーションが規定されています:
へのプライマリキーの移行とインデックスの追加、テーブルの追加と削除、外部キーの移行を、ダウンタイムなしで実行する方法を説明します。
可逆性
マイグレーションは可逆的でなければなりません。脆弱性やバグがあった場合にダウングレードが可能でなければならないからです。
Note: GitLabの本番環境では、問題が発生した場合、db:rollback
を使ってマイグレーションをロールバックする代わりにロールフォワード戦略を使います。セルフマネージドインスタンスでは、アップグレードプロセスが始まる前に作成されたバックアップを復元するようユーザーに助言します。down
の方法は、主に開発環境で使われます。例えば、開発者がコミットやブランチを切り替えるときに、ローカルのstructure.sql
ファイルとデータベースのコピーが一貫した状態にあることを確認したい場合などです。
マイグレーションに、マイグレーションの可逆性がどのようにテストされたかを記述したコメントを追加してください。
一部のマイグレーションは元に戻せません。たとえば、マイグレーション前のデータベースの状態に関する情報が失われるため、データマイグレーションを元に戻せないものがあります。マイグレーション中に実行された変更が元に戻せなくても、マイグレーション自体は元に戻せるように、up
メソッドで実行された変更が元に戻せない理由を説明するコメント付きのdown
メソッドを作成する必要があります:
def down
# no-op
# comment explaining why changes performed by `up` cannot be reversed.
end
このようなマイグレーションは本質的にリスクが高く、レビューのためにマイグレーションを準備する際には追加のアクションが必要です。
原子性とトランザクション
デフォルトでは、マイグレーションは単一のトランザクションです。トランザクションはマイグレーションの開始時にオープンされ、すべてのステップが処理された後にコミットされます。
マイグレーションを単一のトランザクションで実行することで、ステップのひとつが失敗しても、どのステップも実行されず、データベースは有効な状態に保たれます。そのため、次のいずれかを実行します:
- すべてのマイグレーションを1つの単一トランザクションマイグレーションにまとめます。
- 必要であれば、ほとんどのアクションを1つのマイグレーションにまとめ、1つのトランザクションで実行できないステップについては別のマイグレーションを作成します。
例えば、空のテーブルを作成し、インデックスを作成する必要がある場合、通常のシングルトランザクションマイグレーションとデフォルトのRailsスキーマステートメントを使用する必要があります:add_index
。このオペレーションはブロッキングオペレーションですが、テーブルがまだ使用されていないため、レコードがないので問題は発生しません。
単一トランザクションでの重いオペレーション
単一トランザクションによるマイグレーションを使用する場合、トランザクションはマイグレーション期間中データベース接続を保持します。一般的に、トランザクションは迅速に実行されなければなりません。そのため、マイグレーションで実行される各クエリの最大クエリ時間制限に注意してください。
単一トランザクションのマイグレーションに時間がかかる場合、いくつかの選択肢があります。いずれの場合も、マイグレーションにかかる時間に応じて適切なマイグレーション・タイプを選択することを忘れないでください。
-
マイグレーションを複数の単一トランザクションマイグレーションに分割します。
-
disable_ddl_transaction!
](#disable-transaction-wrapped-migration)を使用して[で複数のトランザクションを使用します。 -
ステートメントとロックのタイムアウト設定を調整した後、単一トランザクションのマイグレーションを使用してください。重いワークロードがトランザクションの保証を使用する必要がある場合は、マイグレーションがタイムアウト制限に達することなく実行できることを確認する必要があります。同じアドバイスが、シングルトランザクションマイグレーションと個々のトランザクションの両方に適用されます。
- ステートメントのタイムアウト: GitLab.comの本番データベースでは、ステートメントのタイムアウトは
15s
に設定されていますが、インデックスの作成には15秒以上かかることがよくあります。add_concurrent_index
を含む既存のヘルパーを使用すると、必要に応じてステートメントタイムアウトを自動的にオフにします。まれに、disable_statement_timeout
を使ってタイムアウトの制限を自分で設定する必要があるかもしれません。 - ロックタイムアウト: マイグレーションがトランザクションとして実行されなければならないが、ロック取得中にタイムアウトする可能性がある場合、
enable_lock_retries!
を使用してください。
- ステートメントのタイムアウト: GitLab.comの本番データベースでは、ステートメントのタイムアウトは
statement_timeout
やlock_wait_timeout
のような設定を制御するPgBouncerをバイパスして、プライマリデータベースに直接接続します。ステートメントのタイムアウト制限を一時的にオフにします。
マイグレーションヘルパーdisable_statement_timeout
では、ステートメントタイムアウトを一時的にトランザクションごとまたは接続ごとに0
に設定することができます。
-
CREATE INDEX CONCURRENTLY
のように、ステートメントが明示的なトランザクション内部での実行をサポートしていない場合、接続ごとのオプションを使用します。 -
ALTER TABLE ... VALIDATE CONSTRAINT
のように、ステートメントが明示的トランザクション・ブロックをサポートしている場合は、per-transaction オプションを使用する必要があります。
ほとんどのマイグレーションヘルパーは、必要なときにすでに内部で使用しているため、disable_statement_timeout
を使用する必要はほとんどありません。たとえば、インデックスの作成には通常15秒以上かかります。これはGitLab.comの本番データベースで設定されているデフォルトのステートメントタイムアウトです。ヘルパーadd_concurrent_index
はdisable_statement_timeout
に渡されたブロックの内部でインデックスを作成し、接続ごとのステートメントタイムアウトを無効にします。
マイグレーションで生の SQL 文を書く場合は、手動でdisable_statement_timeout
を使う必要があるかもしれません。その際はデータベースのレビュアーやメンテナーに相談してください。
トランザクションラップされたマイグレーションを無効にします。
disable_ddl_transaction!
ActiveRecordのメソッドで disable_ddl_transaction!
、マイグレーションを単一のトランザクションとして実行しないようにすることができます。disable_ddl_transaction!
このメソッドは他のデータベースシステムでも呼び出せるかもしれませんが、結果は異なります。GitLab ではもっぱら PostgreSQL を使っています。 disable_ddl_transaction!
という意味でdisable_ddl_transaction!
読んで disable_ddl_transaction!
ください:
「このマイグレーションを単一のPostgreSQLトランザクションで実行しないでください。PostgreSQLのトランザクションは、必要な_ときに_ _必要_なだけ開きます。”
.transaction
(もしくはBEGIN; COMMIT;
)を使用しなくても、すべてのSQL文はトランザクションとして実行されます。トランザクションに関するPostgreSQLのドキュメントを参照してください。disable_ddl_transaction!
を使ったマイグレーションを非トランザクションマイグレーションと呼ぶことがあります。これは、マイグレーションが_単一の_トランザクションとして実行されないことを意味します。どのような場合にdisable_ddl_transaction!
を使うべきですか?ほとんどの場合、既存の RuboCop ルールやマイグレーションヘルパーが、disable_ddl_transaction!
. disable_ddl_transaction!
NET を使うべきかどうかを検出できます。マイグレーションで使用すべきかどうかわからない場合はdisable_ddl_transaction!
スキップ disable_ddl_transaction!
し、RuboCop のルールとデータベースのレビューに任せましょう。
PostgreSQLが明示的なトランザクションの外部でオペレーションを実行する必要がある場合、disable_ddl_transaction!
。
- このようなオペレーションの最も顕著な例は
CREATE INDEX CONCURRENTLY
.CREATE INDEX CONCURRENTLY
NETコマンドです。CREATE INDEX CONCURRENTLY
PostgreSQLでは、CREATE INDEX
トランザクション内でCREATE INDEX
ブロック版()を実行することができます。CREATE INDEX
とは異なりCREATE INDEX
CREATE INDEX CONCURRENTLY
、()はトランザクション外で実行CREATE INDEX CONCURRENTLY
しなければCREATE INDEX CONCURRENTLY
なりません。そのため、マイグレーションがたった1つの文CREATE INDEX CONCURRENTLY
を実行するだけであっても、disable_ddl_transaction!
を無効にすべきです。これは、ヘルパーadd_concurrent_index
requiresdisable_ddl_transaction!
CREATE INDEX CONCURRENTLY
の使用が規則よりも例外に近い理由でもあります。
何らかの理由でマイグレーションで複数のトランザクションを実行する必要がある場合は、disable_ddl_transaction!
を使用してください。ほとんどの場合、1つの遅いトランザクションの実行を避けるために複数のトランザクションを使用することになるでしょう。
- 例えば、(DML) 大量のデータを挿入、更新、削除する場合は、バッチで実行する必要があります。バッチごとにオペレーションをグループ化する必要がある場合は、バッチ処理時に明示的にトランザクションブロックをオープンすることができます。それなりに大きなワークロードに対しては、バッチバックグランドマイグレーションを使用することを検討してください。
マイグレーションヘルパーが必要とする場合は、disable_ddl_transaction!
。さまざまなマイグレーションヘルパーは、いつ、どのようにトランザクションをオープンするかを正確に制御する必要があるため、disable_ddl_transaction!
。
- 外部キーは、
CREATE INDEX CONCURRENTLY
.NCREATE INDEX CONCURRENTLY
ETとは異なり、トランザクションの内部で追加する_ことが_できます。CREATE INDEX CONCURRENTLY
しかし、PostgreSQLは.NETのようなオプションを提供してCREATE INDEX CONCURRENTLY
いません。 ヘルパーadd_concurrent_foreign_key
は、外部キーの追加と検証の間、ロックを最小限にする方法でソーステーブルとターゲットテーブルをロックするために、代わりに独自のトランザクションをオープンします。 - 先にアドバイスしたように、自信がない場合は
disable_ddl_transaction!
をスキップし、RuboCopチェックに違反しないか確認してください。
マイグレーションが実際にはPostgreSQLデータベースに触れないか、_複数の_PostgreSQLデータベースに触れる場合は、disable_ddl_transaction!
。
- 例えば、マイグレーションはRedisサーバを対象とするかもしれません。原則として、PostgreSQLトランザクション内で外部サービスとやりとりすることはできません。
- トランザクションは単一のデータベース接続に使用されます。
ci
、main
データベースなど、複数のデータベースをマイグレーション対象とする場合は、Migrations for multiple databasesに従ってください。
命名規則
データベースオブジェクト(テーブル、インデックス、ビューなど)の名前は小文字にする必要があります。小文字にすることで、引用符で囲まれていない名前でのクエリがエラーにならないようにします。
カラム名はActiveRecordのスキーマ規約と一致させてください。
カスタムのインデックス名と制約名は、制約命名規約のガイドラインに従ってください。
長いインデックス名の切り捨て
PostgreSQLは列名やインデックス名のような識別子の長さを制限しています。列名は通常問題ありませんが、インデックス名は長くなりがちです。長すぎる名前を短くするいくつかの方法があります:
-
index_
の代わりにi_
を先頭につけます。 - 冗長な接頭辞は省略。例えば、
index_vulnerability_findings_remediations_on_vulnerability_remediation_id
はindex_vulnerability_findings_remediations_on_remediation_id
になります。 - 列の代わりに、
index_users_for_unconfirmation_notification
のようにインデックスの目的を指定します。
マイグレーションタイムスタンプ年齢
マイグレーションファイル名のタイムスタンプ部分はマイグレーションの実行順序を決定します。大まかな相関を保つことが重要です:
- マイグレーションが GitLab コードベースに追加されたとき。
- マイグレーション自体のタイムスタンプ。
新しいマイグレーションのタイムスタンプは、前のハードストップより前であってはなりません。マイグレーションは時々つぶされ、タイムスタンプが前のハードストップより前になるマイグレーションが追加された場合、イシュー408304で起きたような問題が発生する可能性があります。
例えば、現在 GitLab 16.0 に対して開発している場合、以前のハードストップは 15.11 です。 15.11は2023年4月23日にリリースされました。したがって、許容できる最小のタイムスタンプは 20230424000000 となります。
ベストプラクティス
上記は厳密なルールと考えるべきですが、マイグレーションのタイムスタンプは、前回のハードストップからの経過時間にかかわらず、マイグレーションがアップストリームにマージされると予想される日から3週間以内に保つようにするのがベストプラクティスです。
マイグレーションのタイムスタンプを更新するには
-
ci
とmain
DB のマイグレーションをダウンします:rake db:migrate:down:main VERSION=<timestamp> rake db:migrate:down:ci VERSION=<timestamp>
- マイグレーションファイルを削除します。
- マイグレーションスタイルガイドに従ってマイグレーションを再作成します。
マイグレーション・ヘルパーとバージョニング
GitLab 14.3で導入されました。
データベースマイグレーションにおける多くの一般的なパターンに対して、様々なヘルパーメソッドが用意されています。これらのヘルパーはGitlab::Database::MigrationHelpers
や関連モジュールにあります。
時間とともにヘルパーのふるまいを変更できるようにするために、マイグレーションヘルパーのバージョン管理スキームを実装します。これにより、すでに存在するマイグレーションに対してはヘルパーのふるまいをメンテナーに維持し、新しいマイグレーションに対してはふるまいを変更することができます。
この目的のために、すべてのデータベースマイグレーションはGitlab::Database::Migration
を継承しなければなりません。新しいマイグレーションでは、マイグレーションヘルパーの最新バージョンを使うために、最新バージョンを使うべきです (Gitlab::Database::Migration::MIGRATION_CLASSES
で調べることができます)。
この例では、マイグレーションクラスのバージョン2.1を使います:
class TestMigration < Gitlab::Database::Migration[2.1]
def change
end
end
マイグレーションにGitlab::Database::MigrationHelpers
を直接含めないでください。代わりに、マイグレーションヘルパーの最新バージョンを自動的に公開する最新バージョンのGitlab::Database::Migration
。
マイグレーションヘルパーとバージョニングは GitLab 14.3 で導入されました。以前の安定ブランチを対象としたマージリクエストには、古い形式を使用し、Gitlab::Database::Migration[2.1]
の代わりにActiveRecord::Migration[6.1]
を継承してください。
データベースロック取得時のリトライメカニズム
データベース・スキーマを変更する際には、DDL (Data Definition Language) ステートメントを呼び出すヘルパー・メソッドを使用します。場合によっては、これらの DDL 文は特定のデータベース・ロックを必要とします。
使用例:
def change
remove_column :users, :full_name, :string
end
このマイグレーションを実行するには、users
テーブルに対する users
排他ロックが必要です。users
テーブルが他のプロセスから同時にアクセスされ変更される場合、ロックの取得に時間がかかることがあります。ロック要求はキューで待機しており users
、いったんキューに入れられるとusers
テーブルに対する他のクエリをブロックする可能性が users
あります。
PostgreSQLのロックについての詳細はこちらを参照してください:明示的ロック
安定性の理由から、GitLab.comではstatement_timeout
。マイグレーションが実行されると、どのデータベースクエリも実行にかかる時間は決まっています。最悪のシナリオでは、リクエストはロックキューに置かれ、設定されたステートメントのタイムアウトの間、他のクエリをブロックし、canceling statement due to statement timeout
エラーで失敗します。
この問題は、アプリケーションのアップグレード処理に失敗したり、テーブルが短時間アクセスできなくなる可能性があるため、アプリケーションの安定性に問題が発生したりする可能性があります。
データベースマイグレーションの信頼性と安定性を高めるために、GitLabコードベースでは、lock_timeout
の設定と試行間の待機時間を変えてオペレーションを再試行する方法を提供しています。必要なロックを取得するための複数の短い試行により、データベースは他のステートメントを処理することができます。
ロックの再試行には2つの方法があります:
- トランザクションマイグレーション内部:
enable_lock_retries!
を使用します。 - 非トランザクションマイグレーション内部:
with_lock_retries
を使用してください。
可能であれば、トラフィックの多いテーブルに触れるマイグレーションではロック・リトライを有効にしてください。
トランザクションマイグレーションでの使用法
通常のマイグレーションはトランザクション内で完全なマイグレーションを実行します。マイグレーションレベルでenable_lock_retries!
を呼び出すことで、ロックリトライ手法を有効にすることができます。
これにより、このマイグレーションではロックのタイムアウトが制御されます。また、タイムアウト内にロックが許可されなかった場合、マイグレーション全体を再試行することができます。
これは現在オプトインの設定ですが、私たちはすべてのマイグレーションでロック再試行を使用することを希望しており、将来的にはこれをデフォルトにする予定です。
時折、マイグレーションは異なるオブジェクトに対して複数のロックを取得する必要があるかもしれません。カタログの肥大化を防ぐには、DDLを実行する前に明示的にすべてのロックを要求します。より良い戦略は、マイグレーションを分割して、一度に1つのロックしか取得しないようにすることです。
カラムの削除
enable_lock_retries!
def change
remove_column :users, :full_name, :string
end
同じテーブルに対する複数の変更
ロック・リトライ手法を有効にすると、すべてのオペレーションは1つのトランザクションにまとまります。ロックを取得したら、後で別のロックを取得しようとするのではなく、トランザクションの中でできる限りのことを行うべきです。ブロック内で長いデータベース文を実行することには注意してください。取得したロックはトランザクション(ブロック)が終了するまで保持され、ロックの種類によっては他のデータベースオペレーションをブロックする可能性があります。
enable_lock_retries!
def up
add_column :users, :full_name, :string
add_column :users, :bio, :string
end
def down
remove_column :users, :full_name
remove_column :users, :bio
end
外部キーの削除
enable_lock_retries!
def up
remove_foreign_key :issues, :projects
end
def down
add_foreign_key :issues, :projects
end
カラムのデフォルト値の変更
カラムのデフォルト値を変更すると、複数のリリースプロセスに従わない場合、アプリケーションのダウンタイムが発生する可能性があることに注意してください。詳細については、列のデフォルト値を変更するマイグレーションにおけるダウンタイムの回避を参照してください。
enable_lock_retries!
def up
change_column_default :merge_requests, :lock_version, from: nil, to: 0
end
def down
change_column_default :merge_requests, :lock_version, from: 0, to: nil
end
外部キーを持つ新しいテーブルの作成
create_table
メソッドをwith_lock_retries
でラップします:
enable_lock_retries!
def up
create_table :issues do |t|
t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
t.string :title, limit: 255
end
end
def down
drop_table :issues
end
2つの外部キーがある場合の新しいテーブルの作成
1つのトランザクションで作成する外部キーは1つだけにしてください。これは、、外部キー制約の追加には、参照されるテーブルに対するSHARE ROW EXCLUSIVE
ロックが必要であり、同じトランザクションで複数のテーブルをロックすることは避ける必要があるためです。
そのためには、3つのマイグレーションが必要です:
- 外部キーなし(インデックス付き)のテーブルの作成。
- 最初のテーブルに外部キーを追加します。
- 2番目のテーブルに外部キーを追加します。
テーブルの作成
def up
create_table :imports do |t|
t.bigint :project_id, null: false
t.bigint :user_id, null: false
t.string :jid, limit: 255
t.index :project_id
t.index :user_id
end
end
def down
drop_table :imports
end
projects
への外部キーの追加 :
このヘルパーメソッドにはロックの再試行が組み込まれているので、この場合はadd_concurrent_foreign_key
メソッドを使うことができます。
disable_ddl_transaction!
def up
add_concurrent_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :imports, column: :project_id
end
end
users
への外部キーの追加 :
disable_ddl_transaction!
def up
add_concurrent_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :imports, column: :user_id
end
end
非トランザクションマイグレーションでの使用 (disable_ddl_transaction!
)
disable_ddl_transaction!
を使ってトランザクションマイグレーションを無効にした場合のみ、with_lock_retries
ヘルパーを使って個々の一連のステップをガードすることができます。これは、与えられたブロックを実行するためにトランザクションをオープンします。
カスタム RuboCop ルールは、許可されたメソッドのみがロック再試行ブロック内に配置できるようにします。
disable_ddl_transaction!
def up
with_lock_retries do
add_column :users, :name, :text unless column_exists?(:users, :name)
end
add_text_limit :users, :name, 255 # Includes constraint validation (full table scan)
end
RuboCopルールは一般に、以下に示す標準的なRailsマイグレーションメソッドを許可します。この例ではRuboCop違反が発生します:
disable_ddl_transaction!
def up
with_lock_retries do
add_concurrent_index :users, :name
end
end
ヘルパーメソッドを使うタイミング
with_lock_retries
ヘルパーメソッドを使えるのは、実行がまだオープントランザクション内部でない場合だけです (PostgreSQL サブトランザクションの使用は推奨されません)。標準的なRailsマイグレーションヘルパーメソッドと一緒に使うことができます。同じテーブルに対して実行するのであれば、複数のマイグレーションヘルパーを呼び出しても問題ありません。
with_lock_retries
ヘルパーメソッドを使うことをお勧めするのは、データベースのマイグレーションがトラフィックの多いテーブルの1つを含む場合です。
変更例
-
add_foreign_key
/remove_foreign_key
-
add_column
/remove_column
change_column_default
-
create_table
/drop_table
change
メソッド内部でwith_lock_retries
メソッドを使用することはできません。マイグレーションをリバーシブルにするには、手動でup
メソッドとdown
メソッドを定義する必要があります。
ヘルパーメソッドの仕組み
- 50回繰り返します。
- 各反復ごとに、事前に設定した
lock_timeout
を設定します。 - 与えられたブロックの実行を試みます。(
remove_column
). -
LockWaitTimeout
エラーが発生した場合、事前に設定されたsleep_time
の間スリープし、ブロックを再試行します。 - エラーが発生しなかった場合、現在のイテレーションでブロックが正常に実行されたことになります。
詳細はGitlab::Database::WithLockRetries
クラスを参照してください。with_lock_retries
ヘルパーメソッドはGitlab::Database::MigrationHelpers
モジュールに実装されています。
最悪の場合、このメソッドは
- 40分間に最大50回ブロックを実行します。
- ほとんどの時間は、各反復の後にあらかじめ設定されたスリープ期間に費やされます。
- 50回目の再試行の後、ブロックは
lock_timeout
なしで実行され、標準的なマイグレーション呼び出しと同じようになります。 - ロックを取得できない場合、マイグレーションは
statement timeout
エラーで失敗します。
users
テーブルにアクセスするトランザクションが非常に長い時間 (40 分以上) 実行されている場合、マイグレーションが失敗する可能性があります。
SQLレベルでのロック・リトライ手法
このセクションでは、lock_timeout
の使用を示す簡略化したSQLの例を提供します。与えられたスニペットを複数のpsql
セッションで実行することで、一緒に追うことができます。
カラムを追加するためにテーブルを変更する場合、AccessExclusiveLock
ほとんどのロック・タイプと競合する , がテーブルに必要 AccessExclusiveLock
です。AccessExclusiveLock
ターゲットテーブルが非常にビジーなものである場合、カラムを追加するトランザクションが AccessExclusiveLock
タイムリーにAccessExclusiveLock
取得できない可能性が AccessExclusiveLock
あります。
あるトランザクションがテーブルに行を挿入しようとしているとします:
-- Transaction 1
BEGIN;
INSERT INTO my_notes (id) VALUES (1);
この時点でトランザクション1はRowExclusiveLock
my_notes
。トランザクション1は、コミットまたはアボートする前に、さらにステートメントを実行することができます。. my_notes
テーブルへの行挿入を試みるトランザクションがあるとします。
トランザクションマイグレーションが、ロック再試行ヘルパーを使用せずにテーブルに列を追加しようとしているとします:
-- Transaction 2
BEGIN;
ALTER TABLE my_notes ADD COLUMN title text;
トランザクション 1 がまだ実行中でmy_notes
上のRowExclusiveLock
を保持しているため、トランザクション 2 はmy_notes
テーブル上のAccessExclusiveLock
を取得できず、ブロックされました。
さらに悪質な影響として、トランザクション2がAccessExclusiveLock
を取得するためにキューに入っているため、通常はトランザクショ ン1と競合しないはずのトランザクションがブロックされています。通常であれば、別のトランザクションがトランザクション1と同時に同じテーブルmy_notes
から読み取りと書き込みを行おうとした場合、読み取りと書き込みに必要なロックはトランザクション1が保持しているRowExclusiveLock
と競合しないため、トランザクションは通過します。しかし、AccessExclusiveLock
の取得要求がキューに入れられると、トランザクション1と同時に実行できるにもかかわらず、テーブル上の競合するロックに対する後続の要求がブロックされることになります。
with_lock_retries
を使用した場合、トランザクション 2 は指定された時間内にロックを取得できなかった後、すぐにタイムアウトし、他のトランザクションが処理を続行できるようになります:
-- Transaction 2 (version with with lock timeout)
BEGIN;
SET LOCAL lock_timeout to '100ms'; -- added by the lock retry helper.
ALTER TABLE my_notes ADD COLUMN title text;
ロック再試行ヘルパーは、成功するまで同じトランザクションを異なる時間間隔で繰り返し試行します。
SET LOCAL
はパラメータ (lock_timeout
) の変更をトランザクションにスコープすることに注意してください。
インデックスの削除
インデックスを削除する際にテーブルが空でない場合は、通常のremove_index
メソッドではなく、remove_concurrent_index
メソッドを必ず使用してください。remove_concurrent_index
メソッドは同時にインデックスを削除するので、ロックは必要なく、ダウンタイムも発生しません。このメソッドを使用するには、マイグレーション・クラスの内部でdisable_ddl_transaction!
メソッドを呼び出してシングル・トランザクション・モードを無効にする必要があります:
class MyMigration < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
INDEX_NAME = 'index_name'
def up
remove_concurrent_index :table_name, :column_name, name: INDEX_NAME
end
end
インデックスがThanosで使用されていないことを確認できます:
sum by (type)(rate(pg_stat_user_indexes_idx_scan{env="gprd", indexrelname="INSERT INDEX NAME HERE"}[30d]))
しかし、削除するインデックスの名前を指定する必要があります。これは、remove_index
またはremove_concurrent_index
の適切な形式にオプションとして名前を渡すか、remove_concurrent_index_by_name
メソッドを使用することで可能です。名前を明示的に指定することは、正しいインデックスを確実に削除するために重要です。
小さなテーブル(空のテーブルやレコード数が1,000
未満のテーブルなど)では、remove_index
をシングルトランザクションのマイグレーションで使用し、disable_ddl_transaction!
を必要としない他のオペレーションと組み合わせることをお勧めします。
インデックスの無効化
インデックスを削除する前に、そのインデックスを無効にしたい場合があります。詳細はメンテナンス・オペレーション・ガイドを参照してください。
インデックスの追加
インデックスを追加する前に、インデックスが必要かどうかを検討します。データベースのインデックスの追加ガイドには、インデックスが必要かどうかを判断するための詳細と、インデックスを追加するためのベストプラクティスが記載されています。
インデックスの存在のテスト
マイグレーションでインデックスの有無に基づく条件ロジックが必要な場合は、その名前を使用してインデックスの存在をテストする必要があります。これにより、Railsがインデックス定義を比較する際の問題を回避することができます。
詳しくはデータベースインデックスの追加ガイドをレビューしてください。
外部キー制約の追加
既存のカラムまたは新しいカラムに外部キー制約を追加する場合は、カラムにインデックスを追加することも忘れないでください。
これはすべての外部キーに対して必要です。たとえば、効率的なカスケード削除をサポートするためです。テーブルの行が大量に削除されると、参照されているレコードも削除する必要があります。インデックスがない場合、これはテーブルのシーケンシャルスキャンとなり、長い時間がかかります。
外部キー制約を持つ新しいカラムを追加する例を示します。index: true
、インデックスを作成していることに注意してください。
class Migration < Gitlab::Database::Migration[2.1]
def change
add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade }
end
end
空でないテーブルの既存の列に外部キー制約を追加する場合、add_reference
の代わりに、add_concurrent_foreign_key
とadd_concurrent_index
を使用しなければなりません。
トラフィックの多いテーブルを参照しない新しいテーブルや空のテーブルがある場合は、add_reference
を単一トランザクションマイグレーションで使用することをお勧めします。disable_ddl_transaction!
を必要としない他のオペレーションと組み合わせることができます。
既存の列に外部キー制約を追加する方法については、こちらを参照してください。
NOT NULL
制約
GitLab 13.0から導入されました。
詳しくはNOT NULL
constraints のスタイルガイドをご覧ください。
デフォルト値を持つカラムの追加
add_column
PostgreSQL 11 が GitLab 13.0 以降の最小バージョンになったことで、デフォルト値のカラムを追加するのがとても簡単になりました。
PostgreSQL 11以前では、デフォルト値を持つカラムを追加することは、テーブルの全面的な書き換えを引き起こすという問題がありました。
NULLにできない列のデフォルトの削除
NULLでないカラムを追加し、既存のデータを入力するためにデフォルト値を利用した場合、少なくともアプリケーションコードが更新されるまでデフォルト値を維持する必要があります。モデルコードが更新される前にマイグレーションが実行され、モデルは古いスキーマキャッシュを持つので、このカラムのことを知らず、設定できないからです。この場合
- 標準マイグレーションでカラムをデフォルト値で追加します。
- デプロイ後のマイグレーションではデフォルトを削除します。
デプロイ後のマイグレーションはアプリケーションの再起動後に行われ、新しいカラムが検出されたことを確認します。
カラムのデフォルトの変更
change_column_default
、デフォルトのカラムを変更することは、大きなテーブルでは高価で破壊的なオペレーションだと思われるかもしれませんが、実際にはそうではありません。
次のマイグレーションを例にとってみましょう:
class DefaultRequestAccessGroups < Gitlab::Database::Migration[2.1]
def change
change_column_default(:namespaces, :request_access_enabled, from: false, to: true)
end
end
上記のマイグレーションは、最大のテーブルの1つのデフォルトのカラム値を変更します:namespaces
。これは次のように変換できます:
ALTER TABLE namespaces
ALTER COLUMN request_access_enabled
SET DEFAULT false
この特定のケースでは、デフォルト値は存在し、request_access_enabled
カラムのメタデータを変更するだけで、namespaces
テーブルのすべての既存レコードの書き換えを意味するものではありません。デフォルト値を持つ新しいカラムを作成する場合のみ、すべてのレコードが書き換えられます。
上述の理由から、disable_ddl_transaction!
を必要としないシングルトランザクションのマイグレーションでは、change_column_default
を使用するのが安全です。
既存のカラムの更新
既存のカラムを特定の値に更新するには、update_column_in_batches
を使用します。これは更新をバッチに分割するので、一度のステートメントで多くの行を更新することはありません。
これは、projects
テーブルの列foo
を 10 に更新します。some_column
は'hello'
です:
update_column_in_batches(:projects, :foo, 10) do |table, query|
query.where(table[:some_column].eq('hello'))
end
計算による更新が必要な場合、値はArel.sql
でラップすることができるので、ArelはこれをSQLリテラルとして扱います。これはRails 6の必須非推奨でもあります。
以下の例は上の例と同じですが、値はbar
とbaz
列の積に設定されています:
update_value = Arel.sql('bar * baz')
update_column_in_batches(:projects, :foo, update_value) do |table, query|
query.where(table[:some_column].eq('hello'))
end
update_column_in_batches
の場合は、テーブルの行の一部だけを更新するのであれば、大きなテーブルで実行してもよいかもしれません。しかし、GitLab.com のステージング環境で検証することなく、あるいは誰かにお願いすることなく、それを無視しないでください。
外部キー制約の削除
外部キー制約を削除する場合、外部キーに関連する両方のテーブルのロックを取得する必要があります。書き込みパターンが多いテーブルでは、with_lock_retries
を使用することをお勧めします。また、通常アプリケーションはparent,child
の順番で書き込みを行うため、ロック取得時にデッドロックが発生する可能性があります。しかし、外部キーを削除すると、child,parent
の順番でロックを取得します。これを解決するには、例えば、parent,child
で明示的にロックを取得します:
disable_ddl_transaction!
def up
with_lock_retries do
execute('lock table ci_pipelines, ci_builds in access exclusive mode')
remove_foreign_key :ci_builds, to_table: :ci_pipelines, column: :pipeline_id, on_delete: :cascade, name: 'the_fk_name'
end
end
def down
add_concurrent_foreign_key :ci_builds, :ci_pipelines, column: :pipeline_id, on_delete: :cascade, name: 'the_fk_name'
end
データベース・テーブルの削除
データベーステーブルを削除することは珍しく、Railsが提供するdrop_table
の方法が一般的に安全と考えられています。テーブルを削除する前に、以下を検討してください:
テーブルがトラフィックの多いテーブルの外部キーを持っている場合(projects
など)、DROP TABLE
ステートメントはステートメントタイムアウトエラーで失敗するまで同時トラフィックを滞らせる可能性があります。
テーブルにはレコードがなく(機能は使用されていません)、外部キーもありません:
- マイグレーションには
drop_table
メソッドを使用してください。
def change
drop_table :my_table
end
テーブルにはレコードがありますが、外部キーがありません:
- モデル、コントローラ、サービスなど、テーブルに関連するアプリケーションコードを削除します。
- デプロイ後のマイグレーションでは、
drop_table
。
コードが使用されないことが確実であれば、これはすべて1つのマイグレーションで行うことができます。リスクを少し減らしたいのであれば、アプリケーションの変更がマージされた後に、マイグレーションを2回目のマージリクエストに入れることを検討してください。このアプローチはロールバックの機会を提供します。
def up
drop_table :my_table
end
def down
# create_table ...
end
テーブルには外部キーがあります:
- モデル、コントローラ、サービスなど、テーブルに関連するアプリケーションコードを削除します。
- デプロイ後のマイグレーションでは、
with_lock_retries
ヘルパーメソッドを使用して外部キーを削除します。デプロイ後の別のマイグレーションでは、drop_table
を使用します。
コードが使用されないことが確実であれば、これはすべて1つのマイグレーションで行うことができます。リスクを少し減らしたいのであれば、アプリケーションの変更がマージされた後に、マイグレーションを2回目のマージリクエストに入れることを検討してください。このアプローチはロールバックの機会を提供します。
非トランザクションマイグレーションを使用してprojects
テーブルの外部キーを削除します:
# first migration file
class RemovingForeignKeyMigrationClass < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key :my_table, :projects
end
end
def down
add_concurrent_foreign_key :my_table, :projects, column: COLUMN_NAME
end
end
テーブルの削除:
# second migration file
class DroppingTableMigrationClass < Gitlab::Database::Migration[2.1]
def up
drop_table :my_table
end
def down
# create_table with the same schema but without the removed foreign key ...
end
end
テーブルが削除されたら、データベース辞書ガイドの手順に従って、そのテーブルをデータベース辞書に追加します。
シーケンスの削除
GitLab 15.1で導入されました。
シーケンスの削除は一般的ではありませんが、データベースチームが提供するdrop_sequence
メソッドを使うことができます。
その仕組みは次のようなものです:
シーケンスを削除します:
- シーケンスが実際に使用されている場合、デフォルト値を削除します。
- 実行
DROP SEQUENCE
。
シーケンスを再追加します:
- 現在の値を指定してシーケンスを作成します。
- 列のデフォルト値を変更します。
Railsマイグレーションの例:
class DropSequenceTest < Gitlab::Database::Migration[2.1]
def up
drop_sequence(:ci_pipelines_config, :pipeline_id, :ci_pipelines_config_pipeline_id_seq)
end
def down
default_value = Ci::Pipeline.maximum(:id) + 10_000
add_sequence(:ci_pipelines_config, :pipeline_id, :ci_pipelines_config_pipeline_id_seq, default_value)
end
end
add_sequence
は外部キーを持つカラムには使用しないでください。これらのカラムにシーケンスを追加できるのは、downメソッド(以前のスキーマ状態を復元する)のみです。テーブルの切り捨て
GitLab 15.11 で導入。
テーブルの切り捨ては一般的ではありませんが、データベースチームが提供するtruncate_tables!
メソッドを使うことができます。
その仕組みは次のようなものです:
- 切り捨てられるテーブルの
gitlab_schema
を検索します。 - テーブルの
gitlab_schema
が接続のgitlab_schema
に含まれていれば、TRUNCATE
文を実行します。 -
gitlab_schema
for thegitlab_schema
tablesが接続のsに含まれていなければ、何もしません。
主キーの入れ替え
GitLab 15.5 で導入されました。
パーティション・キーがプライマリ・キーに含まれている必要があるため、プライマリ・キーを入れ替える必要があります。
データベースチームが提供するswap_primary_key
。
その仕組みは次のようなものです:
- 主キー制約を削除します。
- 事前に定義したインデックスを使用して主キーを追加します。
class SwapPrimaryKey < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
TABLE_NAME = :table_name
PRIMARY_KEY = :table_name_pkey
OLD_INDEX_NAME = :old_index_name
NEW_INDEX_NAME = :new_index_name
def up
swap_primary_key(TABLE_NAME, PRIMARY_KEY, NEW_INDEX_NAME)
end
def down
add_concurrent_index(TABLE_NAME, :id, unique: true, name: OLD_INDEX_NAME)
add_concurrent_index(TABLE_NAME, [:id, :partition_id], unique: true, name: NEW_INDEX_NAME)
unswap_primary_key(TABLE_NAME, PRIMARY_KEY, OLD_INDEX_NAME)
end
end
整数カラム型
デフォルトでは、整数カラムは4バイト(32ビット)までの数値を保持できます。最大値は2,147,483,647です。ファイルサイズをバイト単位で保持するカラムを作成する場合は、この点に注意してください。ファイルサイズをバイト単位で追跡する場合、最大ファイルサイズは2GB強に制限されます。
整数カラムに8バイト(64ビット)までの数値を保持させるには、明示的に制限を8バイトに設定します。これにより、カラムは9,223,372,036,854,775,807
までの値を保持することができます。
Railsマイグレーションの例:
add_column(:projects, :foo, :integer, default: 10, limit: 8)
文字列とTextデータ型
GitLab 13.0から導入されました。
詳しくはテキストデータ型のスタイルガイドをご覧ください。
タイムスタンプ列型
デフォルトでは、Railsはtimestamp
タイムゾーン情報を含まないタイムスタンプデータを格納するデータ型を timestamp
使用します。timestamp
この timestamp
データ型は、add_timestamps
またはtimestamps
メソッドを呼び出すことで使用されます。
また、Railsは:datetime
データ型をtimestamp
データ型に変換します。
使用例:
# timestamps
create_table :users do |t|
t.timestamps
end
# add_timestamps
def up
add_timestamps :users
end
# :datetime
def up
add_column :users, :last_sign_in, :datetime
end
これらのメソッドを使用する代わりに、タイムゾーンを含むタイムスタンプを保存するには以下のメソッドを使用します:
add_timestamps_with_timezone
timestamps_with_timezone
datetime_with_timezone
これにより、すべてのタイムスタンプにタイムゾーンが指定されることになります。これにより、システムのタイムゾーンが変更されたときに、既存のタイムスタンプが突然異なるタイムゾーンを使用することがなくなります。また、最初にどのタイムゾーンが使用されたかも明確になります。
JSONのデータベースへの保存
Rails 5はネイティブでJSONB
(binary JSON)カラムタイプをサポートしています。このカラムを追加するマイグレーション例:
class AddOptionsToBuildMetadata < Gitlab::Database::Migration[2.1]
def change
add_column :ci_builds_metadata, :config_options, :jsonb
end
end
デフォルトではハッシュキーは文字列になります。オプションでカスタムデータ型を追加して、キーに異なるアクセスを提供することもできます。
class BuildMetadata
attribute :config_options, :ind_jsonb # for indifferent accesss or :sym_jsonb if you need symbols only as keys.
end
JSONB
カラムを使用する場合は、JsonSchemaValidatorを使用して、時間の経過とともに挿入されるデータを管理します。
class BuildMetadata
validates :config_options, json_schema: { filename: 'build_metadata_config_option' }
end
暗号化された属性
GitLab 14.0 で導入されました。
attr_encrypted
属性を:text
としてデータベースに保存しないでください。代わりに:binary
を使ってください。これはPostgreSQLのbytea
型を使用し、ストレージをより効率的にします:
class AddSecretToSomething < Gitlab::Database::Migration[2.1]
def change
add_column :something, :encrypted_secret, :binary
add_column :something, :encrypted_secret_iv, :binary
end
end
暗号化された属性をバイナリ列に格納する場合、attr_encrypted
にencode: false
とencode_iv: false
オプションを指定する必要があります:
class Something < ApplicationRecord
attr_encrypted :secret,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
end
テスト
Railsマイグレーションのテストスタイルガイドを参照してください。
データマイグレーション
通常のActiveRecord構文よりも、ArelやプレーンSQLを優先してください。プレーンSQLを使用する場合は、quote_string
ヘルパーを使用して手動ですべての入力を引用符で囲む必要があります。
Arelを使った例:
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
#update other tables with these results
プレーン SQL とquote_string
ヘルパーを使った例:
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
origin_tag_id = duplicate_ids.first
duplicate_ids.delete origin_tag_id
execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end
より複雑なロジックが必要な場合、マイグレーションにローカルなモデルを定義して使うことができます。たとえば
class MyMigration < Gitlab::Database::Migration[2.1]
class Project < MigrationRecord
self.table_name = 'projects'
end
def up
# Reset the column information of all the models that update the database
# to ensure the Active Record's knowledge of the table structure is current
Project.reset_column_information
# ... ...
end
end
その際には、モデルのテーブル名を明示的に設定し、クラス名や名前空間から派生しないようにしてください。
マイグレーションでモデルを使用する際の制限に注意してください。
既存データの変更
ほとんどの場合、データベース内のデータを変更する場合は、一括でデータをマイグレーションすることをお勧めします。
新しいヘルパーeach_batch_range
を導入し、コレクションを繰り返し処理する際のパフォーマンスを向上させました。バッチのデフォルトサイズはBATCH_SIZE
定数で定義されています。
次の例を見てください。
バッチ内のデータのパージ
include ::Gitlab::Database::DynamicModelHelpers
disable_ddl_transaction!
def up
each_batch_range('ci_pending_builds', scope: ->(table) { table.ref_protected }, of: BATCH_SIZE) do |min, max|
execute <<~SQL
DELETE FROM ci_pending_builds
USING ci_builds
WHERE ci_builds.id = ci_pending_builds.build_id
AND ci_builds.status != 'pending'
AND ci_builds.type = 'Ci::Build'
AND ci_pending_builds.id BETWEEN #{min} AND #{max}
SQL
end
end
- 最初の引数は変更するテーブルです:
'ci_pending_builds'
. - 2番目の引数は、選択された関連データセットを取得するラムダを呼び出します (デフォルトは
.all
です):scope: ->(table) { table.ref_protected }
. - 第3引数はバッチサイズ(デフォルトは
BATCH_SIZE
定数に設定):of: BATCH_SIZE
.
新しいヘルパーの使い方を示すMRの例です。
予約パスの名前の変更
プロジェクトの新しいルートが導入されると、既存のレコードと衝突する可能性があります。これらのレコードのパスの名前を変更し、関連するデータをディスク上に移動する必要があります。
私たちはすでに何度かこの作業をしなければならなかったので、現在ではこの作業を支援するヘルパーが用意されています。
これを使うには、マイグレーションにGitlab::Database::RenameReservedPathsMigration::V1
。これは3つのメソッドを提供し、拒否したいパスを1つ以上渡すことができます。
-
rename_root_paths
:parent_id
を持たない、指定された名前のすべての_名前空間の_パスをリネームします。 -
rename_child_paths
:parent_id
を持つ、指定された名前のすべての_名前_空間のパスを変更します。 -
rename_wildcard_paths
:すべての_プロジェクトと_、project_id
を持つすべての_名前_空間のパスを変更します。
これらの行のpath
列は、以前の値の後に整数が付いた名前に変更されます。例:users
は次のようになります。users0
マイグレーションでのアプリケーションコードの使用 (推奨しません)
マイグレーションでアプリケーションコード (モデルを含む) を使うことは一般的に推奨されません。マイグレーションは長い間放置され、マイグレーションが依存するアプリケーションコードが変更され、将来マイグレーションが壊れる可能性があるからです。過去には、複数のファイルにまたがる何百行ものコードをマイグレーションにコピーするのを避けるために、アプリケーションコードを使用する必要があるバックグラウンドマイグレーションもありました。このようなまれなケースでは、マイグレーションに適切なテストがあることを確認することが重要です。将来、コードをリファクタリングする人が、マイグレーションを壊してしまうかどうかを知ることができるようにするためです。アプリケーションコードを使うことも、バッチバックグラウンドマイグレーションでは推奨されません。
通常、MigrationRecord
を継承するクラスを定義することで、マイグレーションでアプリケーションコード (特にモデル) を使わないようにできます (下記の例を参照)。
モデル (マイグレーションで定義されたものを含む) を使う場合、最初にreset_column_information
を使ってカラムキャッシュをクリアしなければなりません。
単一テーブル継承(STI)を利用するモデルを使用する場合、特別な考慮事項があります。
これは、使用するカラムが以前のマイグレーションで変更されキャッシュされていた場合の問題を回避します。
例users テーブルにカラムmy_column
を追加します。
古いスキーマがキャッシュから削除され、ActiveRecordが更新されたスキーマ情報をロードするように、User.reset_column_information
コマンドを省略しないことが重要です。
class AddAndSeedMyColumn < Gitlab::Database::Migration[2.1]
class User < MigrationRecord
self.table_name = 'users'
end
def up
User.count # Any ActiveRecord calls on the model that caches the column information.
add_column :users, :my_column, :integer, default: 1
User.reset_column_information # The old schema is dropped from the cache.
User.find_each do |user|
user.my_column = 42 if some_condition # ActiveRecord sees the correct schema here.
user.save!
end
end
end
基礎となるテーブルが変更され、ActiveRecord経由でアクセスされます。
もし両方のマイグレーションが同じdb:migrate
プロセスで実行されるのであれば、以前の別のマイグレーションでテーブルが変更された場合にも、これを使用する必要があることに注意してください。
この結果、以下のようになります。my_column
が内部に含まれていることに注意してください:
== 20200705232821 AddAndSeedMyColumn: migrating ==============================
D, [2020-07-06T00:37:12.483876 #130101] DEBUG -- : (0.2ms) BEGIN
D, [2020-07-06T00:37:12.521660 #130101] DEBUG -- : (0.4ms) SELECT COUNT(*) FROM "user"
-- add_column(:users, :my_column, :integer, {:default=>1})
D, [2020-07-06T00:37:12.523309 #130101] DEBUG -- : (0.8ms) ALTER TABLE "users" ADD "my_column" integer DEFAULT 1
-> 0.0016s
D, [2020-07-06T00:37:12.650641 #130101] DEBUG -- : AddAndSeedMyColumn::User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1000]]
D, [2020-07-18T00:41:26.851769 #459802] DEBUG -- : AddAndSeedMyColumn::User Update (1.1ms) UPDATE "users" SET "my_column" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["my_column", 42], ["updated_at", "2020-07-17 23:41:26.849044"], ["id", 1]]
D, [2020-07-06T00:37:12.653648 #130101] DEBUG -- : ↳ config/initializers/config_initializers_active_record_locking.rb:13:in `_update_row'
== 20200705232821 AddAndSeedMyColumn: migrated (0.1706s) =====================
スキーマキャッシュ (User.reset_column_information
) のクリアをスキップすると、このカラムはActiveRecordによって使用されず、意図した変更が行われないため、my_column
がクエリから欠落した以下のような結果になります。
== 20200705232821 AddAndSeedMyColumn: migrating ==============================
D, [2020-07-06T00:37:12.483876 #130101] DEBUG -- : (0.2ms) BEGIN
D, [2020-07-06T00:37:12.521660 #130101] DEBUG -- : (0.4ms) SELECT COUNT(*) FROM "user"
-- add_column(:users, :my_column, :integer, {:default=>1})
D, [2020-07-06T00:37:12.523309 #130101] DEBUG -- : (0.8ms) ALTER TABLE "users" ADD "my_column" integer DEFAULT 1
-> 0.0016s
D, [2020-07-06T00:37:12.650641 #130101] DEBUG -- : AddAndSeedMyColumn::User Load (0.7ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1000]]
D, [2020-07-06T00:37:12.653459 #130101] DEBUG -- : AddAndSeedMyColumn::User Update (0.5ms) UPDATE "users" SET "updated_at" = $1 WHERE "users"."id" = $2 [["updated_at", "2020-07-05 23:37:12.652297"], ["id", 1]]
D, [2020-07-06T00:37:12.653648 #130101] DEBUG -- : ↳ config/initializers/config_initializers_active_record_locking.rb:13:in `_update_row'
== 20200705232821 AddAndSeedMyColumn: migrated (0.1706s) =====================
トラフィックの多いテーブル
現在アクセス数の多いテーブルのリストです。
どのテーブルが高トラフィックなのかを判断するのは難しいかもしれません。セルフマネージド・インスタンスでは、GitLab のさまざまな機能をさまざまな利用パターンで使っている可能性があり、GitLab.com から推測するだけでは不十分です。
GitLab.comの高トラフィックテーブルを特定するために、以下の指標を考慮します。ここでリンクされているメトリクスはGitLab内部のみのものであることに注意してください:
- 読み取りオペレーション
- レコード数
- サイズが10GB以上
現在の高トラフィックテーブルと比較して、読み取りオペレーションが高いテーブルが良い候補になるかもしれません。
一般的なルールとして、純粋にGitLab.comの分析やレポーティングのためだけにトラフィックの多いテーブルにカラムを追加することはお勧めしません。これは、すべてのセルフマネージド・インスタンスに直接的な価値を提供することなく、パフォーマンスに悪影響を及ぼす可能性があります。