NOT NULL
カラムを持つ新しいテーブルを作成します。NOT NULL
カラムを既存のテーブルに追加します。-
NOT NULL
制約を既存の列に追加します。 NOT NULL
大規模テーブルの制約
NOT NULL
制約
GitLab 13.0から導入されました。
値としてNULL
を持つべきでないすべての属性は、データベースでNOT NULL
カラムとして定義されるべきです。
アプリケーションのロジックに応じて、NOT NULL
列は、presence: true
バリデーションがモデルで定義されているか、データベース定義の一部としてデフォルト値を持つべきです。例として、NULL
以外の値を常に持つべきですが、アプリケーションが毎回強制する必要のない、明確に定義されたデフォルト値を持つブーリアン属性(例えば、active=true
)では、後者が当てはまります。
NOT NULL
カラムを持つ新しいテーブルを作成します。
新しいテーブルを追加する場合、NOT NULL
のカラムはすべて、create_table
の内部で直接定義する必要があります。
例えば、NOT NULL
の2つのカラムを持つテーブルを作成するマイグレーションを考えてみましょう。db/migrate/20200401000001_create_db_guides.rb
:
class CreateDbGuides < Gitlab::Database::Migration[2.1]
def change
create_table :db_guides do |t|
t.bigint :stars, default: 0, null: false
t.bigint :guide, null: false
end
end
end
NOT NULL
カラムを既存のテーブルに追加します。
add_column
PostgreSQL 11 が GitLab 13.0 以降の最小バージョンになったことで、NULL
やデフォルト値を持つカラムを追加するのがとても簡単になりました。
たとえば、テーブルdb_guides
,db/migrate/20200501000001_add_active_to_db_guides.rb
に新しいカラムNOT NULL
active
を追加するマイグレーションを考えてみましょう:
class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
def change
add_column :db_guides, :active, :boolean, default: true, null: false
end
end
NOT NULL
制約を既存の列に追加します。
NOT NULL
を既存のデータベース列に追加するには、通常、少なくとも2つの異なるリリースに分割された複数のステップが必要です。バックグラウンドマイグレーションを使用する必要がないほどテーブルが小さい場合は、これらすべてを同じマージリクエストに含めることができます。トランザクション時間を短縮するために、別々のマイグレーションを使用することをお勧めします。
必要な手順は以下の通りです:
-
リリース
N.M
(現在のリリース)- アプリケーション・レベルで$ATTRIBUTE値が設定されていることを確認してください。
- 属性にデフォルト値がある場合、新しいレコードにデフォルト値が設定されるように、モデルにデフォルト値を追加します。
- 新しいレコードでも既存のレコードでも、属性が
nil
に設定されている場所があれば、コード内のすべての場所を更新します。
- 既存のレコードを修正するために、デプロイ後のマイグレーションを追加します。
テーブルのサイズによっては、次のリリースでクリーンアップのためのバックグラウンドマイグレーションが必要になる可能性があります。詳細については、「大規模テーブルのNOT NULL
制約」セクションを参照してください。 - アプリケーション・レベルで$ATTRIBUTE値が設定されていることを確認してください。
-
リリース
N.M+1
(次のリリース)- GitLab.com上のすべての既存レコードに属性が設定されていることを確認してください。そうでない場合は、リリース
N.M
のステップ 1 に戻ってください。 - ステップ 1 に問題がなく、リリース
N.M
からのバックフィルがバッチバックグラウンドマイグレーションで行われた場合は、デプロイ後のマイグレーションを追加してバックグラウンドマイグレーションを確定します。 - モデル内の属性の検証を追加し、
nil
属性を持つレコードを防ぎます。 -
NOT NULL
制約を追加するためにデプロイ後のマイグレーションを追加します。
- GitLab.com上のすべての既存レコードに属性が設定されていることを確認してください。そうでない場合は、リリース
物件例
13.0のようなリリースのマイルストーンを考えてみましょう。
本番データベースをチェックした結果、NULL
の記述があるepics
があることがわかりました。したがって、制約の追加と検証を1つのステップで行うことはできません。
NULL
NULL
の記述があるエピックがなかったとしても、GitLabの別のインスタンスにはそのようなレコードがあるかもしれません。
新しい無効なレコードを防ぐ (現在のリリース)
属性がnil
に設定されているすべてのコードパスを更新して、新規レコードと既存レコードで属性をゼロ以外の値に設定するようにしました。
新しいレコードにデフォルト値が設定されるように、Rails attributes APIを使用したデフォルトを持つ属性がepic.rb
に追加されました:
class Epic < ApplicationRecord
attribute :description, default: 'No description'
end
既存レコードを修正するためのデータマイグレーション (現在のリリース)
ここでのアプローチは、データ量とクリーンアップ戦略に依存します。GitLab.comで修正しなければならないレコードの数は、デプロイ後のマイグレーションを使うかバックグラウンドのデータマイグレーションを使うかを決めるのに役立つ良い指標です:
- データ量が
1000
レコード以下であれば、データマイグレーションはポストマイグレーション内で実行できます。 - データ量が
1000
レコードより多い場合は、バックグラウンドマイグレーションを作成することをお勧めします。
どのオプションを使用するか不明な場合は、データベースチームにお問い合わせください。
例に戻ると、エピック・テーブルはそれほど大きくなく、頻繁にアクセスされるものでもありません。そこで、13.0マイルストーン(現在)のデプロイ後のマイグレーションを追加します。db/post_migrate/20200501000002_cleanup_epics_with_null_description.rb
:
class CleanupEpicsWithNullDescription < Gitlab::Database::Migration[2.1]
# With BATCH_SIZE=1000 and epics.count=29500 on GitLab.com
# - 30 iterations will be run
# - each requires on average ~150ms
# Expected total run time: ~5 seconds
BATCH_SIZE = 1000
disable_ddl_transaction!
class Epic < ActiveRecord::Base
include EachBatch
self.table_name = 'epics'
end
def up
Epic.each_batch(of: BATCH_SIZE) do |relation|
relation.
where('description IS NULL').
update_all(description: 'No description')
end
end
def down
# no-op : can't go back to `NULL` without first dropping the `NOT NULL` constraint
end
end
すべてのレコードが修正されたかどうかの確認(次のリリース)
postgres.aiを使って本番データベースのシンクローンを作成し、GitLab.com上のすべてのレコードに属性が設定されているかどうかをチェックします。もしそうでなければ、Prevent new invalid recordsのステップに戻り、コードのどこで属性が明示的にnil
に設定されているかを調べます。コードパスを修正し、マイグレーションを再スケジュールして既存のレコードを修正し、次のステップを実行するために次のリリースを待ちます。
バックグラウンドマイグレーションを確定します(次のリリース)。
マイグレーションがバックグラウンドマイグレーションを使用して行われた場合は、マイグレーションを確定します。
モデルへの検証の追加(次のリリース)
nil
属性を持つレコードを防ぐために、モデルに属性のバリデーションを追加します。
class Epic < ApplicationRecord
validates :description, presence: true
end
NOT NULL
制約を追加します(次回リリース)。
NOT NULL
制約を追加すると、テーブル全体がスキャンされ、各レコードが正しいことが確認されます。
まだこの例では、13.1マイルストーン(次)のために、最終的なデプロイ後のマイグレーションでadd_not_null_constraint
マイグレーション・ヘルパーを実行します:
class AddNotNullConstraintToEpicsDescription < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
# This will add the `NOT NULL` constraint and validate it
add_not_null_constraint :epics, :description
end
def down
# Down is required as `add_not_null_constraint` is not reversible
remove_not_null_constraint :epics, :description
end
end
NOT NULL
大規模テーブルの制約
トラフィックの多いテーブルのNULL可能な列(例えば、ci_builds
のartifacts
)をクリーンアップする必要がある場合、バックグラウンドマイグレーションがしばらく続き、データマイグレーションを追加した後のリリースで、追加のバッチバックグラウンドマイグレーションによるクリーンアップが必要になります。
このようなまれなケースでは、エンドツーエンドで3つのリリースが必要になります:
- リリース
N.M
-NOT NULL
制約を追加し、既存のレコードを修正するためにバックグラウンドマイグレーションを行います。 - リリース
N.M+1
- バックグラウンド・マイグレーションをクリーンアップ。 - リリース
N.M+2
-NOT NULL
制約を検証しました。
このような場合は、更新サイクルの早い段階でデータベース・チームに相談してください。NOT NULL
制約が必要ない場合や、本当に大きなテーブルや頻繁にアクセスされるテーブルに影響を与えない他のオプションが存在する場合があります。