文字列とTextデータ型

GitLab 13.0から導入されました

文字列やその他のテキスト情報を格納するために新しいカラムを追加する場合:

  1. 私たちは常に、string データ型の代わりにtext データ型を使用します。
  2. text テーブルを作成する際には#text ... limit: 100 ヘルパー (下記を参照ください) を使用してcreate_table を使用し、 既存のテーブルを変更する際にはadd_text_limit を使用します。

Railsの標準的なtext 列型には制限を定義できませんが、create_table を拡張してlimit: 255 オプションを追加しました。create_table 以外では、add_text_limit を使用して、すでに存在する列にチェック制約を追加することができます。

背景情報

text カラムの上限を更新したい場合、ALTER TABLE ... コマンドを実行しなければならないという欠点があるからstring です string

制限が追加される間、ALTER TABLE ... コマンドはテーブル上でEXCLUSIVE LOCK を必要とします。 はカラムの更新処理中も、既存のすべてのレコードの検証中も保持され、大きなテーブルではこの処理に時間がかかります。

一方、テキストはPostgreSQLでは多かれ少なかれ文字列と同等ですが、既存の列に制限を追加したり、その制限を更新したりする際に、非常にコストのかかるEXCLUSIVE LOCKEXCLUSIVE LOCK 、列の宣言を更新するためだけに必要です。そして、VALIDATE CONSTRAINT を使用して、後の段階で検証することができます。SHARE UPDATE EXCLUSIVE LOCK を必要とするだけです(他の検証やインデックス作成と競合するだけで、読み書きは可能です)。

note
attr_encrypted 属性にテキストカラムを使用しないでください。代わりに:binary

テキスト・カラムを持つ新しいテーブルを作成します。

新しいテーブルを追加するときは、テーブルの作成と同じマイグレーションですべてのテキストカラムの制限を追加する必要があります。Railsの#text メソッドにlimit: 属性を追加し、このカラムの制限を追加できるようにします。

たとえば、db/migrate/20200401000001_create_db_guides.rb という2つのテキストカラムを持つテーブルを作成するマイグレーションを考えてみましょう:

class CreateDbGuides < Gitlab::Database::Migration[2.1]
  def change
    create_table :db_guides do |t|
      t.bigint :stars, default: 0, null: false
      t.text :title, limit: 128
      t.text :notes, limit: 1024
    end
  end
end

既存のテーブルにテキストカラムを追加

既存のテーブルにカラムを追加するには、そのテーブルの排他ロックが必要です。そのロックが保持される時間は短いとはいえ、add_column が実行を完了するまでに必要な時間は、テーブルへのアクセス頻度によって異なります。例えば、非常に頻繁にアクセスされるテーブルの排他ロックの取得には、GitLab.com では数分かかることがあり、with_lock_retries を使う必要があります。

テキスト制限を追加する際には、disable_ddl_transaction! でトランザクションを無効にする必要があります。これは、マイグレーションが失敗してもカラムの追加がロールバックされないことを意味します。マイグレーションを再実行しようとすると、すでにカラムが存在するためにエラーが発生します。

このような理由から、既存のテーブルにテキストカラムを追加するには、以下のどちらかの方法があります:

カラムとリミットを別々のマイグレーションで追加します。

新しいテキストカラムextended_title をテーブルsprints,db/migrate/20200501000001_add_extended_title_to_sprints.rb に追加するマイグレーションを考えます:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]

  # rubocop:disable Migration/AddLimitToTextColumns
  # limit is added in 20200501000002_add_text_limit_to_sprints_extended_title
  def change
    add_column :sprints, :extended_title, :text
  end
  # rubocop:enable Migration/AddLimitToTextColumns
end

2回目のマイグレーションは、最初のマイグレーションに続いて、extended_title,db/migrate/20200501000002_add_text_limit_to_sprints_extended_title.rb に制限を追加する必要があります:

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

  def up
    add_text_limit :sprints, :extended_title, 512
  end

  def down
    # Down is required as `add_text_limit` is not reversible
    remove_text_limit :sprints, :extended_title
  end
end

カラムがすでに存在するかどうかをチェックしながら、1回のマイグレーションでカラムとリミットを追加します。

新しいテキストカラムextended_title をテーブルsprints,db/migrate/20200501000001_add_extended_title_to_sprints.rb に追加するマイグレーションを考えます:

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

  def up
    with_lock_retries do
      add_column :sprints, :extended_title, :text, if_not_exists: true
    end

    add_text_limit :sprints, :extended_title, 512
  end

  def down
    with_lock_retries do
      remove_column :sprints, :extended_title, if_exists: true
    end
  end
end

既存のカラムにテキスト制限制約を追加

既存のデータベース列にテキスト制限を追加するには、少なくとも2つの異なるリリースに分かれた複数のステップが必要です:

  1. リリースN.M (現在のリリース)

    • デプロイ後のマイグレーションを追加し、validate: false でテキスト列に制限を追加。
    • 既存のレコードを修正するために、デプロイ後のマイグレーションを追加します。

      note
      テーブルのサイズによっては、次のリリースでクリーンアップのためのバックグラウンドマイグレーションが必要になる可能性があります。詳細は、大規模テーブルのテキスト制限制約を参照してください。
    • 次のマイルストーンでテキスト制限を検証するためのイシューを作成してください。
  2. リリースN.M+1 (次のリリース)

    • デプロイ後のマイグレーションを使用してテキスト制限を検証します。

物件例

あるリリースのマイルストーン、例えば13.0について、issues.title_html1024 の制限を加えたいとします。

イシューは2,500万行を超えるかなり多忙で大きなテーブルなので、更新の実行中にアクセスしようとする他のすべてのプロセスをロックしたくありません。

また、実運用データベースを確認したところ、issues 、タイトルに1024文字の制限を超える文字が含まれていることがわかりました。

note
仮にタイトルの文字数が制限を超えるレコードがなかったとしても、GitLab の別のインスタンスにはそのようなレコードがあるかもしれません。

新しい無効なレコードを防ぐ (現在のリリース)

NOT VALID 新しいレコードが挿入された時や現在のレコードが更新された時に一貫性を強制します。

上記の例では、タイトルが1024文字を超える既存のイシューは影響を受けず、issues テーブルのレコードを更新することができます。しかし、1024文字以上のタイトルを持つtitle_html を更新しようとすると、制約によりデータベース・エラーが発生します。

既存の属性に制約を追加または削除するには、アプリケーションの変更を_最初に_デプロイする必要があります。このような理由から、add_text_limit はデプロイ後のマイグレーションで実行する必要があります。

まだこの例では、13.0マイルストーン(現在)において、モデルIssue に以下の検証が追加されていると考えてください:

validates :title_html, length: { maximum: 1024 }

デプロイ後のマイグレーションでvalidate: false を使ってテキスト制限を追加することで、同じマイルストーンでデータベースを更新することもできますdb/post_migrate/20200501000001_add_text_limit_migration.rb

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

  def up
    # This will add the constraint WITHOUT validating it
    add_text_limit :issues, :title_html, 1024, validate: false
  end

  def down
    # Down is required as `add_text_limit` is not reversible
    remove_text_limit :issues, :title_html
  end
end

既存レコードを修正するためのデータマイグレーション (現在のリリース)

ここでのアプローチは、データ量とクリーンアップ戦略に依存します。GitLab.comで修正しなければならないレコードの数は、デプロイ後のマイグレーションを使うかバックグラウンドのデータマイグレーションを使うかを決めるのに役立つ良い指標です:

  • データ量が1,000 レコード以下であれば、データマイグレーションはポストマイグレーション内で実行できます。
  • データ量が1,000 レコードより多い場合は、バックグラウンドマイグレーションを作成することをお勧めします。

どのオプションを使用するか不明な場合は、データベースチームにお問い合わせください。

例に戻ると、イシュー・テーブルはかなり大きく、頻繁にアクセスされるため、13.0マイルストーン(現在)のバックグラウンド・マイグレーションを追加します。db/post_migrate/20200501000002_schedule_cap_title_length_on_issues.rb

class ScheduleCapTitleLengthOnIssues < Gitlab::Database::Migration[2.1]
  # Info on how many records will be affected on GitLab.com
  # time each batch needs to run on average, etc ...
  BATCH_SIZE = 5000
  DELAY_INTERVAL = 2.minutes.to_i

  # Background migration will update issues whose title is longer than 1024 limit
  ISSUES_BACKGROUND_MIGRATION = 'CapTitleLengthOnIssues'.freeze

  disable_ddl_transaction!

  def up
    queue_batched_background_migration(
      ISSUES_BACKGROUND_MIGRATION,
      :issues,
      :id,
      job_interval: DELAY_INTERVAL,
      batch_size: BATCH_SIZE
    )
  end

  def down
    delete_batched_background_migration(ISSUES_BACKGROUND_MIGRATION, :issues, :id, [])
  end
end

このガイドを短くするために、バックグラウンドマイグレーションの定義を省略し、バッチのスケジューリングに使用されるデプロイ後のマイグレーションの高レベルの例のみを提供します。バッチ化されたバックグラウンドマイグレーションに関する詳細な情報は、ガイドを参照してください。

テキスト制限の検証(次のリリース)

テキスト制限の検証はテーブル全体をスキャンし、各レコードが正しいことを確認します。

まだこの例では、13.1マイルストーン(次)のために、最終的なデプロイ後のマイグレーションでvalidate_text_limit マイグレーションヘルパーを実行します、db/post_migrate/20200601000001_validate_text_limit_migration.rb

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

  def up
    validate_text_limit :issues, :title_html
  end

  def down
    # no-op
  end
end

既存のカラムのテキスト制限制約を増やす

既存のデータベースカラムのテキスト制限を増やすには、まず (別の名前で) 新しい制限を追加し、それから以前の制限を削除します:

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

  def up
    add_text_limit :ci_runners, :maintainer_note, 1024, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length_1MB')
    remove_text_limit :ci_runners, :maintainer_note, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length')
  end

  def down
    # no-op: Danger of failing if there are records with length(maintainer_note) > 255
  end
end

大きなテーブルのテキスト制限制約

非常に大きなテーブル(例えば、ci_buildsartifacts )のテキスト列をクリーンアップする必要がある場合、バックグラウンドマイグレーションがしばらく続き、データマイグレーションを追加した後のリリースで、追加のバッチバックグラウンドマイグレーションによるクリーンアップが必要になります。

このようなまれなケースでは、エンドツーエンドで3つのリリースが必要になります:

  1. リリースN.M - 既存のレコードを修正するために、テキスト制限とバックグラウンドマイグレーションを追加します。
  2. リリースN.M+1 - バックグラウンド・マイグレーションをクリーンアップ。
  3. リリースN.M+2 - テキスト制限を検証。