マイグレーションスタイルガイド

GitLabのマイグレーションを書くときは、何十万ものあらゆる規模の組織で実行されることを考慮しなければなりません。

さらに、アップグレードの大小にかかわらず、サーバーをオフラインにしなければならないことは、ほとんどの組織にとって大きな負担です。 このため、マイグレーションを慎重に記述し、オンラインで適用できるようにし、以下のスタイルガイドを遵守することが重要です。

マイグレーションでは、_絶対に必要な_場合を除き、GitLabのインストールをオフラインにする必要はありません

ダウンタイムが必要な場合、その移行は承認者により承認されなければなりません:

  1. エンジニアリング担当副社長
  2. バックエンドメンテナー
  3. データベース・メンテナー

これらの肩書きを持つ人物の最新リストは、https://about.gitlab.com/company/team/でご覧いただけます。

マイグレーションを記述する際には、データベースに古いデータや不整合があるかもしれないことも考慮し、そのような事態に備えましょう。 データベースの状態については、できるだけ仮定しないようにしましょう。

GitLab固有のコードは将来のバージョンで変更される可能性があるため、依存しないでください。 必要であれば、GitLabのコードをマイグレーションにコピーペーストして互換性を持たせてください。

GitLab.comでは、Canaryがデプロイされる前に通常のマイグレーション(db/migrate)が実行され、本番環境へのデプロイが完了した後にデプロイ後のマイグレーション(db/post_migrate)が実行されることを考慮してください。

スキーマの変更

スキーマへの変更はdb/structure.sql.N db/structure.sqlETにコミットしてください。db/structure.sqlこのファイルはRailsによって自動的に生成されるので、通常はこのファイルを手で編集するべきではありません。 マイグレーションによってテーブルにカラムが追加される場合、そのカラムは一番下に追加されます。 既存のテーブルのカラムを手動で並べ替えると db/structure.sql、Railsによって生成されたdb/structure.sqlテーブルを使っている他の人を混乱さ db/structure.sqlせる原因になるのでやめてください。

GDK の内部データベースがmaster のスキーマから分岐している場合、スキーマの変更を Git にきれいにコミットするのは難しいかもしれません。そのような場合は、scripts/regenerate-schema スクリプトを使って、追加する移行用のdb/structure.sql をきれいに再生成することができます。このスクリプトはdb/migratedb/post_migrateにある移行をすべて適用します。スキーマにコミットしたくない移行がある場合は、名前を変更するか削除してください。ブランチがmaster を対象としていない場合は、TARGET 環境変数を設定します。

# Regenerate schema against `master`
scripts/regenerate-schema

# Regenerate schema against `12-9-stable-ee`
TARGET=12-9-stable-ee scripts/regenerate-schema

ダウンタイムが必要なものとは?

What Requires Downtime? “というドキュメントでは、以下のようなさまざまなデータベースオペレーションを規定しています。

また、ダウンタイムが必要かどうか、可能な限りダウンタイムを回避するにはどうしたらよいか、などです。

ダウンタイム・タギング

すべての移行はダウンタイムが必要かどうかを指定し、ダウンタイムが必要な場合はその理由も指定しなければなりません。 ダウンタイムが必要_な_移行を見つけやすくするため、99%の移行がダウンタイムを必要としない場合でも、これは必要です。

マイグレーションにタグを付けるには、マイグレーション・クラスの本体に次の2つの定数を追加します:

  • DOWNTIMEtrue 、マイグレーションにダウンタイムが必要であることを示します。
  • DOWNTIME_REASONこの定数は、DOWNTIMEtrueに設定されている場合に設定されなければなりません

使用例:

class MyMigration < ActiveRecord::Migration[4.2]
  DOWNTIME = true
  DOWNTIME_REASON = 'This migration requires downtime because ...'

  def change
    ...
  end
end

マイグレーションクラスにDOWNTIME 定数がない場合、エラーとなります。

可逆性

脆弱性やバグがあった場合にダウングレードできるようにする必要があります。

マイグレーションでは、マイグレーションの可逆性がどのようにテストされたかを記述したコメントを追加してください。

マイグレーションの中には元に戻せないものもあります。 たとえば、マイグレーション前のデータベースの状態に関する情報が失われるため、元に戻せないデータマイグレーションもあります。 マイグレーション中に実行された変更が元に戻せなくても、マイグレーション自体は元に戻せるように、up メソッドで実行された変更が元に戻せない理由を説明するコメントを添えたdown メソッドを作成する必要があります:

def down
  # no-op

  # comment explaining why changes performed by `up` cannot be reversed.
end

原子性

デフォルトでは、マイグレーションは単一トランザクションです。 つまり、マイグレーションの開始時にトランザクションがオープンされ、すべてのステップが処理された後にコミットされます。

単一のトランザクションでマイグレーションを実行することで、いずれかのステップが失敗しても、どのステップも実行されず、データベースは有効な状態のままとなります。 したがって、以下のいずれかを実行します:

  • すべてのマイグレーションを1つの単一トランザクションマイグレーションにまとめます。
  • 必要であれば、ほとんどのアクションを1つのマイグレーションにまとめ、1つのトランザクションで実行できないステップについては別のマイグレーションを作成します。

例えば、空のテーブルを作成し、インデックスを作成する必要がある場合、通常のシングルトランザクションマイグレーションとデフォルトのRailsスキーマステートメントを使用することをお勧めします:add_index. これはブロッキングオペレーションですが、テーブルがまだ使用されていないため、まだレコードを持っていないため、問題は発生しません。

単一トランザクションでの重いオペレーション

シングルトランザクションマイグレーションを使用する場合、トランザクションはマイグレーションの期間中データベース接続に留まります。そのため、マイグレーションでのアクションに時間がかかりすぎないようにする必要があります。一般的に、マイグレーションで実行されるクエリはGitLab.comの15s に快適に収まる必要があります。

大量のデータを挿入、更新、または削除する必要がある場合:

データベースロック取得時のリトライ機構

データベーススキーマを変更する際には、DDL (Data Definition Language) ステートメントを呼び出すヘルパーメソッドを使用します。 場合によっては、これらの DDL ステートメントは特定のデータベースロックを必要とします。

使用例:

def change
  remove_column :users, :full_name, :string
end

このマイグレーションを実行するには、users テーブルに対する users排他ロックが必要ですusers 。 テーブルが他のプロセスによって同時にアクセスされ変更されている場合、ロックの取得に時間がかかることがあります。 ロック要求はキューで待機しており users、いったんキューに入れられるとusers テーブルに対する他の usersクエリをusers ブロックすることもあります users

PostgresSQLのロックの詳細:明示的ロック

安定性の理由から、GitLab.com には特定のstatement_timeoutが設定されています。マイグレーションが起動されると、データベースクエリの実行時間は一定になります。 最悪のシナリオでは、リクエストはロックキューに置かれ、設定されたステートメントのタイムアウトの間、他のクエリをブロックし、canceling statement due to statement timeout エラーで失敗します。

このイシューが発生すると、アプリケーションのアップグレード処理に失敗したり、テーブルが短時間アクセスできなくなるため、アプリケーションの安定性に問題が生じたりする可能性があります。

データベース移行の信頼性と安定性を高めるために、GitLabコードベースはlock_timeout の設定と試行間の待ち時間を変えてオペレーションを再試行するヘルパーメソッドを提供しています。 必要なロックを取得するために複数の小さな試行を行うことで、データベースは他のステートメントを処理できるようになります。

使用例

カラムの削除

include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    remove_column :users, :full_name
  end
end

def down
  with_lock_retries do
    add_column :users, :full_name, :string
  end
end

外部キーの削除

include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    remove_foreign_key :issues, :projects
  end
end

def down
  with_lock_retries do
    add_foreign_key :issues, :projects
  end
end

カラムのデフォルト値の変更

include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    change_column_default :merge_requests, :lock_version, from: nil, to: 0
  end
end

def down
  with_lock_retries do
    change_column_default :merge_requests, :lock_version, from: 0, to: nil
  end
end

外部キーを持つ新しいテーブルの作成

create_table メソッドをwith_lock_retriesでラップするだけです:

def up
  with_lock_retries do
    create_table :issues do |t|
      t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade }
      t.string :title, limit: 255
    end
  end
end

def down
  drop_table :issues
end

外部キーが2つある場合の新しいテーブルの作成

そのためには、3つのマイグレーションが必要です:

  1. 外部キーなし(インデックス付き)のテーブルを作成します。
  2. 最初のテーブルに外部キーを追加します。
  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
  end

  add_index :imports, :project_id
  add_index :imports, :user_id
end

def down
  drop_table :imports
end

projectsへの外部キーの追加 :

include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    add_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
  end
end

def down
  with_lock_retries do
    remove_foreign_key :imports, column: :project_id
  end
end

usersへの外部キーの追加 :

include Gitlab::Database::MigrationHelpers

def up
  with_lock_retries do
    add_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
  end
end

def down
  with_lock_retries do
    remove_foreign_key :imports, column: :user_id
  end
end

使用方法disable_ddl_transaction!

通常、with_lock_retries ヘルパーはdisabled_ddl_transaction!で動作するはずです。カスタム RuboCop ルールによって、許可されたメソッドだけがロック再試行ブロック内に配置できるようになっています。

disable_ddl_transaction!

def up
  with_lock_retries do
    add_column :users, :name, :text
  end

  add_text_limit :users, :name, 255 # Includes constraint validation (full table scan)
end

RuboCopルールでは、以下に示す標準的なRailsマイグレーションメソッドを一般的に許可しています。 この例では、RuboCop違反が発生します:

disabled_ddl_transaction!

def up
  with_lock_retries do
    add_concurrent_index :users, :name
  end
end

ヘルパーメソッドを使用する場合

with_lock_retries ヘルパーメソッドは、通常Rails標準のマイグレーションヘルパーメソッドを使用する場合に使用できます。 複数のマイグレーションヘルパーを呼び出しても、同じテーブルに対して実行されるのであれば問題ありません。

データベースの移行にトラフィックの多いテーブルが含まれる場合は、with_lock_retries ヘルパーメソッドを使用することをお勧めします:

  • users
  • projects
  • namespaces
  • issues
  • merge_requests
  • ci_pipelines
  • ci_builds
  • notes

変更例

  • add_foreign_key /remove_foreign_key
  • add_column /remove_column
  • change_column_default
  • create_table /drop_table

注: with_lock_retries メソッドはchange メソッド内部では使用できませんup メソッドとdown メソッドを手動で定義して、移行を可逆にする必要があります。

ヘルパーメソッドの仕組み

  1. 50回繰り返します。
  2. 各反復ごとに、事前に設定したlock_timeoutを設定します。
  3. 指定されたブロックの実行を試みます (remove_column)。
  4. LockWaitTimeout エラーが発生した場合は、事前に設定されたsleep_timeの間スリープし、ブロックを再試行します。
  5. エラーが発生しなければ、現在のイテレーションでブロックが正常に実行されたことになります。

詳しくはGitlab::Database::WithLockRetries クラスをご覧ください。with_lock_retries ヘルパーメソッドはGitlab::Database::MigrationHelpers モジュールに実装されています。

最悪の場合、その方法は

  • 40分間に最大50回ブロックを実行します。
    • ほとんどの時間は、各反復の後にあらかじめ設定されたスリープ期間に費やされます。
  • 50回目のリトライの後、ブロックはlock_timeoutなしで実行されます。
  • ロックを取得できない場合、マイグレーションはstatement timeout エラーで失敗します。

users テーブルにアクセスするトランザクションの実行時間が非常に長い(40分以上)場合、移行に失敗する可能性があります。

マルチスレッド

マイグレーションを高速化するために、複数のRubyスレッドを使用する必要がある場合があります。 そのためには、マイグレーションにモジュールGitlab::Database::MultiThreadedMigrationを含める必要があります:

class MyMigration < ActiveRecord::Migration[4.2]
  include Gitlab::Database::MigrationHelpers
  include Gitlab::Database::MultiThreadedMigration
end

その後、with_multiple_threads メソッドを使って、別々のスレッドで作業を行うことができます:

class MyMigration < ActiveRecord::Migration[4.2]
  include Gitlab::Database::MigrationHelpers
  include Gitlab::Database::MultiThreadedMigration

  def up
    with_multiple_threads(4) do
      disable_statement_timeout

      # ...
    end
  end
end

ここでは、disable_statement_timeout の呼び出しは、グローバル接続プールを再利用する代わりに、with_multiple_threads ブロックのローカル接続を使用します。 これにより、各スレッドが独自の接続オブジェクトを持つようになり、接続オブジェクトを取得しようとしたときにタイムアウトすることがなくなります。

注意:PostgreSQLには許可される最大接続数があります。 この制限はインストールによって異なる可能性があります。 そのため、1回の移行で32スレッドを超えるスレッドを使用しないことを推奨します。 通常は4〜8スレッドで十分です。

インデックスの削除

インデックスを削除するときにテーブルが空でない場合は、通常のremove_index メソッドではなくremove_concurrent_index メソッドを使用するようにしてください。remove_concurrent_index メソッドはインデックスを同時に削除するので、ロックは不要で、ダウンタイムも発生しません。 このメソッドを使用するには、マイグレーション・クラスの内部でdisable_ddl_transaction! メソッドを以下のように呼び出して、シングル・トランザクション・モードを無効にする必要があります:

class MyMigration < ActiveRecord::Migration[4.2]
  include Gitlab::Database::MigrationHelpers
  disable_ddl_transaction!

  def up
    remove_concurrent_index :table_name, :column_name
  end
end

インデックスを削除する前に、インデックスが存在するかどうかを確認する必要はないことに注意してください。

小さなテーブル(空のテーブルやレコード数が1,000 未満のテーブルなど)では、remove_index をシングルトランザクションのマイグレーションで使用し、disable_ddl_transaction!を必要としない他のオペレーションと組み合わせることをお勧めします。

インデックスの追加

インデックスを追加する前に、このインデックスが必要かどうかを検討してください。 たとえば、インデックスが必要ない場合もあります:

  • テーブルのサイズは小さく(1,000 レコード以下)、指数関数的に大きくなることはありません。
  • 既存のインデックスは十分な行をフィルタリングします。
  • インデックス追加後のクエリタイミングの短縮はそれほど大きくありません。

さらに、ワイドインデックスは、クエリのすべてのフィルタ条件にマッチする必要はありません。 インデックス検索が十分に小さい選択性を持つように、十分なカラムをカバーする必要があるだけです。 詳細については、データベースインデックスの追加ガイドをレビューしてください。

空でないテーブルにインデックスを追加する場合、通常のadd_index メソッドではなく、add_concurrent_index メソッドを必ず使用してください。 PostgreSQLを使用する場合、add_concurrent_index メソッドは自動的に同時インデックスを作成し、ダウンタイムの必要性を取り除きます。

このメソッドを使うには、マイグレーション・クラスの内部でdisable_ddl_transaction!

class MyMigration < ActiveRecord::Migration[6.0]
  include Gitlab::Database::MigrationHelpers

  DOWNTIME = false

  disable_ddl_transaction!

  def up
    add_concurrent_index :table, :column
  end

  def down
    remove_concurrent_index :table, :column
  end
end

一意インデックスを追加する必要がある場合、データベース内に既存の重複が存在する可能性があることに留意してください。 つまり、一意インデックスを追加する前に、必ず重複を削除するマイグレーションを_最初に_追加する必要があります。

小さなテーブル(空のテーブルやレコード数が1,000 未満のテーブルなど)では、add_index をシングルトランザクションのマイグレーションで使用し、disable_ddl_transaction!を必要としない他のオペレーションと組み合わせることをお勧めします。

外部キー制約の追加

既存のカラムまたは新しいカラムに外部キー制約を追加する場合は、カラムにインデックスを追加することも忘れないでください。

これは、すべての外部キーに必要です。たとえば、効率的なカスケード削除をサポートするためです。 テーブルの行が大量に削除されると、参照されたレコードも削除される必要があります。 データベースは、参照されたテーブルから対応するレコードを探さなければなりません。 インデックスがないと、テーブルを順次スキャンすることになり、長い時間がかかります。

外部キー制約を持つ新しいカラムを追加する例です。インデックスを作成するためにindex: true を含むことに注意してください。

class Migration < ActiveRecord::Migration[4.2]

  def change
    add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade }
  end
end

空でないテーブルの既存の列に外部キー制約を追加する場合、add_referenceの代わりに、add_concurrent_foreign_keyadd_concurrent_indexを使用する必要があります。

空のテーブル(新しいテーブルなど)については、disable_ddl_transaction!を必要としない他のオペレーションと組み合わせて、add_reference を単一トランザクションのマイグレーションで使用することをお勧めします。

既存のカラムに外部キー制約を追加する方法については、こちらを参照してください。

NOT NULL 制約

GitLab 13.0から導入されました

詳しくは、NOT NULL 制約 のスタイルガイドをご覧ください。

デフォルト値を持つカラムの追加

GitLab 13.0以降、PostgreSQL 11が最小バージョンとなったことで、デフォルト値を持つカラムの追加がより簡単になり、どのような場合でも標準のadd_column ヘルパーを使うようになりました。

PostgreSQL 11以前では、デフォルトを持つ列を追加することは、テーブルの全面的な書き換えを引き起こすため問題がありました。 対応するヘルパーadd_column_with_defaultは非推奨となり、後のリリースで削除される予定です。

注意: 既定値の列を追加するバックポートが %12.9 またはそれ以前のバージョンで必要な場合は、add_column_with_default ヘルパーを使用する必要があります。add_column_with_default大きなテーブルがadd_column_with_default含まれる場合は、%12.9 へのバックポートはお勧めできません。

列のデフォルトの変更

change_column_default 、デフォルトのカラムを変更することは、大規模なテーブルでは高価で破壊的なオペレーションだと思われるかもしれませんが、実際にはそうではありません。

次の移行を例にとってみましょう:

class DefaultRequestAccessGroups < ActiveRecord::Migration[5.2]
  DOWNTIME = false

  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
DEFAULT false

この特別なケースでは、デフォルト値は存在し、request_access_enabled カラムのメタデータを変更するだけです。namespaces テーブルの既存のレコードをすべて書き換えるわけではありません。デフォルト値を持つ新しいカラムを作成する場合のみ、すべてのレコードが書き換わります。

注意:PostgresSQL 11.0で、デフォルト値がNULLでないALTER TABLE ADD COLUMNが導入され、デフォルト値を持つ新しい列が追加された時にテーブルを書き換える必要がなくなりました。

上記の理由から、disable_ddl_transaction!を必要とせず、シングルトランザクショ ン・マイグレーションでchange_column_default を使用することは安全です。

既存の列の更新

既存のカラムを特定の値に更新するには、update_column_in_batchesを使用します。 これは更新をバッチに分割するので、1回のステートメントで多くの行を更新することはありません。

これにより、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でラップすることで、ArellはSQLリテラルとして扱います。 Rails6では必須の非推奨事項でもあります。

以下の例は上の例と同じですが、値はbarbaz 列の積に設定されます:

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

add_column_with_defaultと同様、RuboCop コップがあり、大きなテーブルでの使用を検出することができます。update_column_in_batchesの場合、テーブルの行のごく一部だけを更新するのであれば、大きなテーブルで実行しても問題ないかもしれません。しかし、GitLab.com のステージング環境で検証することなく、あるいは他の誰かにお願いすることなく、それを無視しないでください。

データベーステーブルの削除

データベースのテーブルを削除するのは珍しいことで、Railsが提供するdrop_table 。テーブルを削除する前に、以下を検討してください:

もしあなたのテーブルがトラフィックの多いテーブル(projectsなど)の外部キーを持っている場合、DROP TABLE ステートメントはステートメントタイムアウトエラーで失敗するかもしれません。 どのテーブルがトラフィックの多いテーブルなのかを判断するのは難しいかもしれません。セルフマネージドインスタンスではGitLabのさまざまな機能をさまざまな使用パターンで使っている可能性があり、GitLab.comから推測するだけでは不十分です。

テーブルにはレコードがなく(機能は使用されていません)、外部キーもありません:

  • 移行にはdrop_table メソッドを使用します。
def change
  drop_table :my_table
end

テーブルにはレコードがありますが、外部キーがありません:

  • 最初のリリース: モデル、コントローラ、サービスなど、テーブルに関連するアプリケーションコードを削除します。
  • 2つ目のリリース:マイグレーションにdrop_table メソッドを使用します。
def up
  drop_table :my_table
end

def down
  # create_table ...
end

テーブルには外部キーがあります:

  • 最初のリリース: モデル、コントローラ、サービスなど、テーブルに関連するアプリケーションコードを削除します。
  • 2番目のリリース:with_lock_retriesヘルパーメソッドを使用して外部キーを削除します。 別のマイグレーションファイルでdrop_table を使用します。

2回目のリリースのための移行:

projects テーブルの外部キーを削除します:

# first migration file

def up
  with_lock_retries do
    remove_foreign_key :my_table, :projects
  end
end

def down
  with_lock_retries do
    add_foreign_key :my_table, :projects
  end
end

テーブルを落とします:

# second migration file

def up
  drop_table :my_table
end

def down
  # create_table ...
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 < ActiveRecord::Migration[5.0]
  DOWNTIME = false

  def change
    add_column :ci_builds_metadata, :config_options, :jsonb
  end
end

変換レイヤーを提供するためにシリアライザーを使用する必要があります:

class BuildMetadata
  serialize :config_options, Serializers::JSON # rubocop:disable Cop/ActiveRecordSerialize
end

JSONB カラムを使用する場合は、JsonSchemaValidatorを使用して、時間経過とともに挿入されるデータの制御を維持します。

class BuildMetadata
  validates :config_options, json_schema: { filename: 'build_metadata_config_option' }
end

テスト

TestingRailsmigrationsスタイルガイドを参照してください。

データ移行

通常のActiveRecord構文よりも、ArelやプレーンSQLを使用してください。 プレーンSQLを使用する場合は、quote_string ヘルパーを使用してすべての入力を手動で引用符で囲む必要があります。

アレルの例:

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 < ActiveRecord::Migration[4.2]
  class Project < ActiveRecord::Base
    self.table_name = 'projects'
  end
end

その際には、クラス名や名前空間から派生しないように、モデルのテーブル名を明示的に設定するようにしてください。

予約パスの名前の変更

プロジェクトの新しいルートが導入されると、既存のレコードと衝突する可能性があります。 これらのレコードのパス名を変更し、関連するデータをディスク上に移動する必要があります。

すでに数回この作業をしなければならなかったので、現在ではこの作業を手伝ってくれるヘルパーがいます。

これを使用するには、Gitlab::Database::RenameReservedPathsMigration::V1をマイグレーションに含めます。これは3つのメソッドを提供し、拒否する必要のあるパスを1つ以上渡すことができます。

rename_root_pathsこれは、parent_idを持たない、指定された名前のすべての_名前_空間のパスをリネームします。

rename_child_pathsこれは、parent_idを持つ、指定された名前のすべての_名前_空間のパスをリネームします。

rename_wildcard_pathsすべての_プロジェクトと_、project_idを持つすべての_名前空間の_パス名を変更します。

これらの行のpath 列の名前は、以前の値の後に整数が付いたものに変更されます。 例えば、users は次のようになります。users0