외래 키 및 연관성

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

class User < ActiveRecord::Base
  has_many :posts
end

여기에 posts.user_id 컬럼에 외래 키를 추가하세요.
이렇게 함으로써 데이터 일관성이 데이터베이스 수준에서 강제됩니다.
외래 키는 또한 데이터베이스가 관련된 데이터(예: 사용자 제거 시)를 신속하게 제거할 수 있게 하며, 이를 Rails가 처리하는 것보다 효율적으로 합니다.

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

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

기존 테이블에 외래 키를 안전하게 추가하려면 그동안 고아로 남은 행을 제거해야 합니다. add_concurrent_foreign_key 메서드는 이를 자동으로 처리하지 않으므로 수동으로 처리해야 합니다. 기존 열에 외래 키 제약 조건 추가을 참조하세요.

외래 키에는 bigint 사용

새로운 외래 키를 추가할 때는 반드시 bigint로 정의해야 합니다.
참조하는 테이블이 integer 기본 키 유형이더라도 새로운 외래 키를 bigint로 참조해야 합니다.
모든 기본 키를 bigint로 마이그레이션하는 중이기 때문에 bigint 외래 키를 사용하면 부모 테이블의 bigint 기본 키로 마이그레이션할 때 시간을 절약하고 단계가 적게 필요합니다.

reverse_lock_order를 고려하세요

고트래픽 테이블에서 reverse_lock_order를 사용하는 것을 고려하세요. add_concurrent_foreign_keyremove_foreign_key_if_exists는 기본적으로 reverse_lock_order 옵션을 가지는데, 이는 기본적으로 거짓(false)입니다.

이에 대한 구체적인 맥락은 원본 이슈에서 더 자세히 읽을 수 있습니다.

이것은 우리가 매우 빈도가 높은 동일한 테이블에서 알려진 쿼리를 사용하는 경우 유용할 수 있습니다.

예를 들어, 다음과 같이 외래 키를 추가하려는 시나리오를 고려해보세요.

ALTER TABLE ONLY todos
    ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;

그리고 다음과 같은 가설적인 애플리케이션 코드를 고려해보세요.

Todo.transaction do
   note = Note.create(...)
   # 여기서 외래 키를 추가하려고 시도했을 때의 결과를 관찰해보세요!
   todo = Todo.create!(note_id: note.id)
end

위의 두 insert 문 사이에 외래 키를 생성하려고 시도하면 Postgres에서 두 트랜잭션에서 데드락이 발생할 수 있습니다. 여기에 일어나는 과정은 다음과 같습니다.

  1. Note.create: notes에서 행 잠금을 획득합니다.
  2. ALTER TABLE ...: todos에서 테이블 잠금을 획득합니다.
  3. ALTER TABLE ... FOREIGN KEY: notes에서 테이블 잠금을 획득하려고 시도하지만 다른 트랜잭션에서 행 잠금을 하고 있어서 차단됩니다.
  4. Todo.create: todos에서 행 잠금을 획득하려고 시도하지만 다른 트랜잭션에서 todos에 대한 테이블 잠금을 하고 있어서 차단됩니다.

이는 두 트랜잭션이 서로 끝나기를 기다리고 있으며 둘 다 시간 초과될 것이라는 것을 보여줍니다. 일반적으로 마이그레이션에는 트랜잭션 재시도가 있기 때문에 대부분 괜찮지만 애플리케이션 코드는 시간 초과될 수 있으며 그 사용자에게 오류가 발생할 수 있습니다. 이 애플리케이션 코드가 매우 빈번하게 실행된다면 마이그레이션을 계속해서 시간 초과시키고 사용자도 정기적으로 오류를 받을 수 있습니다.

외래 키를 제거하는 데 발생하는 데드락 경우도 유사하지만, 사용 예에서 자주 발생하는 시나리오는 DELETE FROM notes WHERE id = ...입니다. 이 쿼리는 notes에서 락을 획들하고, 다음으로 todos에서 락을 획듭니다. 정확히 위에서 설명한 것과 동일한 데드락이 발생할 수 있습니다. 이러한 이유 때문에 대부분의 경우 외래 키를 제거할 때 reverse_lock_order를 사용하는 것이 가장 좋습니다.

마이그레이션에서 외래 키 갱신

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

PostgreSQL은 중복된 외래 키를 추가하는 것을 방지하지 않습니다. 가장 최근에 추가된 제약 조건을 따릅니다.
이로써 외래 키 보호 기능을 절대로 잃지 않고 외래 키를 교체할 수 있게 합니다.

외래 키를 교체하려면 다음과 같이 합니다.

  1. 새로운 외래 키 추가: ```ruby 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, 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. 이전 외래 키 제거: ```ruby class RemoveFkOld < Gitlab::Database::Migration[2.1] disable_ddl_transaction!

    OLD_CONSTRAINT_NAME = ‘fk_old’

    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, name: OLD_CONSTRAINT_NAME) end end ```

다대일 관계 삭제

모든 외래 키는 ON DELETE 절을 정의해야 하며, 대부분의 경우에 이것은 CASCADE로 설정해야 합니다.

인덱스

PostgreSQL에 외래 키를 추가할 때 컬럼은 자동으로 인덱싱되지 않기 때문에 동시 인덱스를 반드시 추가해야 합니다.
인덱스를 추가하지 않으면, 업데이트의 결과로 발생하는 삭제가 매우 느려집니다.

외래 키 명명

기본적으로 Ruby on Rails는 외래 키에 _id 접미사를 사용합니다. 따라서 이 접미사는 두 테이블 간의 연관 관계에서만 사용해야 합니다.
외부 플랫폼의 ID를 참조하려면 _xid 접미사를 권장합니다.

spec/db/schema_spec.rb_id 접미사가 있는 모든 열이 외래 키 제약 조건을 갖고 있는지 테스트합니다. 따라서 해당 사양에 실패하면 IGNORED_FK_COLUMNS에 열을 추가하는 대신 FK 제약 조건을 추가하거나 다르게 명명하는 것을 고려하세요.

종속성 제거

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

다시 말해서, 다음과 같은 것은 좋지 않으며 가능한 한 피해야 합니다:

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

이에 대한 필요성이 실제로 있는 경우에는 먼저 데이터베이스 전문가의 승인이 필요합니다.

모델에는 절대 필요하지 않은 경우에는 모델에 before_destroy 또는 after_destroy 콜백을 정의해서는 안 되며, 데이터베이스 전문가의 승인을 받은 경우에만 정의해야 합니다. 예를 들어, 테이블의 각 행이 파일 시스템에 해당 파일과 대응되는 경우 after_destroy 훅을 추가하는 것이 유혹스러울 수 있습니다. 그러나 이것은 데이터베이스 로직이 모델에 도입되는 것이며, 외래 키를 사용하여 데이터를 제거할 수 없게 됨을 의미합니다. 이러한 경우에는 대신에 비데이터베이스 데이터를 제거하는 서비스 클래스를 사용해야 합니다.

관계가 여러 데이터베이스에 걸쳐 있는 경우에는 dependent: :destroy 또는 위의 훅을 사용하는 것으로 인해 더욱 심각한 문제가 발생할 수 있습니다. Avoid dependent: :nullify and dependent: :destroy across databases에서 대안에 대해 자세히 알아볼 수 있습니다.

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 Pingbatch counting를 덜 효율적으로 만들 수 있습니다. Service Ping에서 테이블이 관련이 있는 경우 일반적인 id 열을 사용하는 것을 고려하십시오.