외부 키 및 연관성

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

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은 중첩되는 외부 키를 추가하는 것을 방지하지 않습니다. 최근에 추가된 제약 조건을 우선시합니다. 이를 통해 열에서 외부 키 보호를 언제나 잃지 않고 외부 키를 바꿀 수 있습니다.

외부 키를 바꾸려면 다음을 수행하세요:

  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'
    
      # 외부 키 추가됨 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
        # 작업 없음
      end
    end
    
  3. 이전 외부 키 제거:

    class RemoveFkOld < Gitlab::Database::Migration[2.1]
      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
        remove_foreign_key_if_exists(:packages_packages, column: :project_id, on_delete: :cascade, name: OLD_CONSTRAINT_NAME)
      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 접미사를 사용합니다. 따라서 두 테이블 간의 연관성에 대해서만 이 접미사를 사용해야 합니다. 제3자 플랫폼의 ID를 참조하려면 _xid 접미사를 권장합니다.

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

종속적 삭제

연관성을 정의할 때 dependent: :destroy 또는 dependent: :delete와 같은 옵션을 정의하지 마십시오. 이러한 옵션을 정의하는 것은 데이터베이스가 가장 효율적인 방법으로 데이터를 처리하는 대신 Rails가 이를 처리하도록 하는 것을 의미합니다.

다시 말하지만, 이것은 나쁘며 가능한한 피해야 합니다:

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

실제로 이것이 필요한 경우에는 먼저 데이터베이스 전문가의 승인을 받아야 합니다.

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

이러한 경우에는 연결된 테이블의 불필요한 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 Pingbatch counting을 덜 효율적으로 만들 수 있습니다. 서비스 통계에 필요한 테이블이라면 일반적인 id 열을 사용하는 것을 고려해보세요.