外部キーと関連付け

モデルに関連を追加する際には、外部キーも追加する必要があります。例えば、以下のようなモデルがあるとします:

class User < ActiveRecord::Base
  has_many :posts
end

カラムposts.user_id に外部キーを追加します。これにより、データベースレベルでデータの一貫性が保証されます。外部キーはまた、(ユーザーを削除する場合などに)Railsが行うのではなく、データベースが関連データを非常に迅速に削除できることを意味します。

マイグレーションでの外部キーの追加

Gitlab::Database::MigrationHelpers で定義されているように、add_concurrent_foreign_key を使って外部キーを同時に追加することができます。詳細はマイグレーションスタイルガイドを参照してください。

既存のテーブルに外部キーを安全に追加できるのは、孤児となった行を削除した後であることに注意してください。add_concurrent_foreign_key のメソッドではこの処理は行われませんので、手動で行う必要があります。既存のカラムへの外部キー制約の追加を参照してください。

マイグレーションにおける外部キーの更新

外部キー制約を変更し、カラムは保持したまま制約条件を更新しなければならないことがあります。例えば、ON DELETE CASCADE からON DELETE SET NULL へ、あるいはその逆です。

PostgreSQLは、重複する外部キーの追加を防ぎます。PostgreSQLは、最近追加された制約を尊重します。これにより、列の外部キー保護を失うことなく外部キーを置き換えることができます。

外部キーを置き換えるには

  1. バリデーションなしで新しい外部キーを追加します。

    古い外部キーを削除する前に、新しい外部キーを追加するために外部キー制約の名前を変更する必要があります。

    class ReplaceFkOnPackagesPackagesProjectId < Gitlab::Database::Migration[2.1]
      disable_ddl_transaction!
       
      NEW_CONSTRAINT_NAME = 'fk_new'
       
      def up
        add_concurrent_foreign_key(:packages_packages, :projects, column: :project_id, on_delete: :nullify, validate: false, name: NEW_CONSTRAINT_NAME)
      end
       
      def down
        with_lock_retries do
          remove_foreign_key_if_exists(:packages_packages, column: :project_id, on_delete: :nullify, name: NEW_CONSTRAINT_NAME)
        end
      end
    end
    
  2. 新しい外部キーを検証します。

    class ValidateFkNew < Gitlab::Database::Migration[2.1]
      NEW_CONSTRAINT_NAME = 'fk_new'
       
      # foreign key added in <link to MR or path to migration adding new FK>
      def up
        validate_foreign_key(:packages_packages, name: NEW_CONSTRAINT_NAME)
      end
       
      def down
        # no-op
      end
    end
    
  3. 古い外部キーを削除します:

    class RemoveFkOld < Gitlab::Database::Migration[2.1]
      OLD_CONSTRAINT_NAME = 'fk_old'
       
      # new foreign key added in <link to MR or path to migration adding new FK>
      # and validated in <link to MR or path to migration validating new FK>
      def up
        remove_foreign_key_if_exists(:packages_packages, column: :project_id, on_delete: :cascade, name: OLD_CONSTRAINT_NAME)
      end
       
      def down
        # Validation is skipped here, so if rolled back, this will need to be revalidated in a separate migration
        add_concurrent_foreign_key(:packages_packages, :projects, column: :project_id, on_delete: :cascade, validate: false, name: OLD_CONSTRAINT_NAME)
      end
    end
    

カスケード削除

すべての外部キーはON DELETE 節を定義する必要があり、99%の場合、これはCASCADE に設定されるべきです。

インデックス

PostgreSQLで外部キーを追加する場合、列は自動的にインデックス付けされません。これを行わないと、カスケード削除が非常に遅くなります。

外部キーの命名

デフォルトでは、Ruby on Railsは外部キーに_id というサフィックスを使用します。したがって、2つのテーブル間の関連付けにのみこのサフィックスを使うべきです。サードパーティのプラットフォームでIDを参照する場合は、_xid サフィックスを使用することをお勧めします。

仕様spec/db/schema_spec.rb は、_id サフィックスを持つすべてのカラムが外部キー制約を持っているかどうかをテストします。そのため、この仕様が失敗した場合は、IGNORED_FK_COLUMNS に列を追加せず、代わりにFK制約を追加するか、別の名前を付けることを検討してください。

依存関係の削除

アソシエーションを定義するときに、dependent: :destroydependent: :delete といったオプションを定義しないでください。これらのオプションを定義すると、データの削除をデータベースに任せる代わりにRailsが最も効率的な方法で処理することになります。

つまり、これは良くないことなので、絶対に避けるべきです:

class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end

本当に必要であれば、まずデータベースの専門家の承認が必要です。

また、before_destroyafter_destroy コールバックをモデル上で after_destroy定義することは、after_destroy 絶対に必要な after_destroy場合をafter_destroy 除き、データベースの専門家が承認した場合にのみ after_destroy行うようにしてください。たとえば、after_destroy テーブルの各行がファイルシステム上に対応するファイルを持って after_destroyいる場合、フックをafter_destroy 追加したくなるかもしれません after_destroy。しかし、これはモデルにデータベース以外のロジックを導入することになり、データを削除するために外部キーに頼ることができなくなります。このような場合は、代わりにデータベース以外のデータを削除するサービスクラスを使うべきです。

リレーションが複数のデータベースにまたがっている場合、dependent: :destroy や上記のフックを使用するとさらに問題が生じます。 dependent: :nullifydependent: :destroy を避ける方法については、を参照してください。

has_one 関連付けを持つ代替主キー

一対一のリレーションシップを作成するためにhas_one のアソシエーションを使用することがあります:

class User < ActiveRecord::Base
  has_one :user_config
end

class UserConfig < ActiveRecord::Base
  belongs_to :user
end

このような場合、関連テーブル(この例ではuser_config.id )の不要なid 列を削除することができます。このような場合、関連テーブル(この例では)の不要な 列を削除することができます。代わりに、関連テーブルの主キーとして元のテーブル ID を使用することができます:

create_table :user_configs, id: false do |t|
  t.references :users, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
  ...
end

default: nil を設定することで、主キーシーケンスが作成されないようにします。また、主キーは自動的にインデックスを取得するため、index: false を設定して重複を作成しないようにします。また、新しい主キーをモデルに追加する必要があります:

class UserConfig < ActiveRecord::Base
  self.primary_key = :user_id

  belongs_to :user
end

主キーとして外部キーを使用すると、スペースを節約できますが、Service Pingでの バッチカウントの効率が悪くなります。テーブルがService Pingに関連する場合は、通常のid 列の使用を検討してください。