외부 키 및 연관성

모델에 연관을 추가할 때는 외부 키도 추가해야 합니다.
예를 들어, 다음과 같은 모델이 있다고 합시다.

class User < ActiveRecord::Base
  has_many :posts
end

여기서 posts.user_id 열에 외부 키를 추가하세요. 이렇게 하면 데이터 일관성이 데이터베이스 수준에서 강제됩니다. 외부 키는 데이터베이스가 연관된 데이터(예: 사용자 삭제 시)를 매우 빠르게 제거할 수 있도록 하며, 이를 Rails가 처리하는 것보다 우수합니다.

마이그레이션에서 외부 키 추가

외부 키는 Gitlab::Database::MigrationHelpers에서 정의된 add_concurrent_foreign_key를 사용하여 동시에 추가할 수 있습니다. 자세한 내용은 마이그레이션 스타일 가이드를 참조하세요.

기존 테이블에 안전하게 외부 키를 추가하려면 고아 행(orphanned rows)을 모두 제거한 후에 가능합니다. add_concurrent_foreign_key 메서드가 이 부분을 처리하지 않으므로 매뉴얼으로 해야 합니다. 기존 열에 외부 키 제약 조건을 추가하려면 기존 열에 외부 키 제약 조건 추가하기을 참조하세요.

마이그레이션에서 외부 키 업데이트

가끔 외부 키 제약 조건을 유지하지만 조건을 업데이트해야 할 수 있습니다. 예를 들어, ON DELETE CASCADE에서 ON DELETE SET NULL로 또는 그 반대로 변경해야 할 수 있습니다.

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'
         
      # <link to MR or path to migration adding new FK>에서 추가된 외부 키
      def up
        validate_foreign_key(:packages_packages, :project_id, name: NEW_CONSTRAINT_NAME)
      end
         
      def down
        # no-op
      end
    end
    
  3. 이전 외부 키 제거:

    class RemoveFkOld < Gitlab::Database::Migration[2.1]
      disable_ddl_transaction!
         
      OLD_CONSTRAINT_NAME = 'fk_old'
         
      # 신규 외부 키는 <link to MR or path to migration adding new FK>에서 추가되었으며, 새로운 외부 키 유효성을 검사한 <link to MR or path to migration validating new FK>에서 검증되었습니다.
      def up
        with_lock_retries do
          remove_foreign_key_if_exists(:packages_packages, column: :project_id, on_delete: :cascade, name: OLD_CONSTRAINT_NAME)
        end
      end
         
      def down
        # 여기서는 유효성 검사가 생략되므로 롤백할 경우 별도의 마이그레이션에서 유효성을 다시 검증해야 합니다.
        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 접미사를 사용합니다. 따라서 두 테이블 사이의 연관성에만 이 접미사를 사용해야 합니다. 써드 파티 플랫폼의 ID를 참조하려면 _xid 접미사가 권장됩니다.

spec/db/schema_spec.rb 스펙은 _id 접미사가 있는 모든 열에 외부 키 제약 조건이 있는지 테스트합니다. 따라서 해당 스펙이 실패하면 IGNORED_FK_COLUMNS에 열을 추가하는 것이 아니라 외부 키 제약 조건을 추가하거나 다르게 명명하는 것을 고려해야 합니다.

종속적인 제거

연관을 정의할 때 dependent: :destroydependent: :delete와 같은 옵션을 정의하지 마세요. 이러한 옵션을 정의하면 데이터 삭제를 Rails가 매우 효율적으로 처리하는 방식으로 데이터베이스에 맡기는 대신 Rails가 처리하게 됩니다.

다시 말해, 다음과 같이 하는 것은 좋지 않으며 가능한 피해야 합니다.

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

이러한 필요성이 실제로 있는 경우에는 반드시 데이터베이스 전문가의 승인을 받아야 합니다.

모델에 before_destroyafter_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

이러한 경우에는 연관된 테이블의 불필요한 id 열을 제거할 수 있는 기회가 있을 수 있습니다. 이 예에서는 연관된 테이블 user_config.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의 배치 카운팅 예제에서 효율적이지 않을 수 있습니다. 테이블이 Service Ping에 관련이 있다면 일반적인 id 열을 사용하는 것을 고려해야 합니다.