- 마이그레이션에서 외래 키 추가하기
- 외래 키에
bigint
사용하기 reverse_lock_order
고려하기- 외래 키 업데이트하는 방법
- 연쇄 삭제
- 인덱스
- 외래 키 이름 지정
- 종속 제거
has_one
관계를 통한 대체 주요 키
외래 키와 연관
모델에 연관을 추가할 때 외래 키도 추가해야 합니다. 예를 들어, 다음과 같은 모델이 있다고 가정해 보겠습니다:
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_key
와 remove_foreign_key_if_exists
는 기본값이 false인 불린 옵션 reverse_lock_order
를 사용합니다.
원래 이슈에서 이와 관련된 내용을 더 읽을 수 있습니다.
이는 동일한 테이블에 대해 높은 빈도로 잠금을 획득하는 알려진 쿼리가 있는 경우 유용할 수 있습니다.
예를 들어, 다음과 같이 외래 키를 추가하고 싶다고 가정해 보십시오:
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
2개의 삽입 문 사이에 외래 키를 생성하려고 하면 두 거래 모두 Postgres에서 교착 상태에 빠질 수 있습니다. 그 과정은 다음과 같습니다:
-
Note.create
:notes
에서 행 잠금을 획득합니다. -
ALTER TABLE ...
는todos
에서 테이블 잠금을 획득합니다. -
ALTER TABLE ... FOREIGN KEY
는notes
에 대한 테이블 잠금을 시도하지만 이는 행 잠금을 가지고 있는 다른 거래에서 차단됩니다. -
Todo.create
는todos
에서 행 잠금을 획득하려고 시도하지만이는todos
에서 테이블 잠금을 가지고 있는 다른 거래에서 차단됩니다.
이것은 두 거래 모두 서로가 완료되기를 기다리며 교착 상태에 빠질 수 있음을 보여줍니다. 우리는 일반적으로 마이그레이션에서 거래 재시도를 하므로 일반적으로 괜찮지만 애플리케이션 코드가 시간 초과될 수 있으며 사용자가 오류를 보고할 가능성도 있습니다. 이 애플리케이션 코드가 매우 빈번하게 실행된다면 마이그레이션이 계속해서 시간 초과될 수 있으며 사용자들도 정기적으로 오류를 겪게 될 수 있습니다.
외래 키를 제거하는 경우의 교착 상태도 유사합니다. 이 경우도 두 테이블에서 잠금을 획득하지만 위의 예를 사용하면 DELETE FROM notes WHERE id = ...
와 같은 더 일반적인 시나리오가 될 것입니다. 이 쿼리는 notes
에서 잠금을 획득하고 todos
에서 잠금을 획득하는데, 위에서 설명한 것과 동일한 교착 상태가 발생할 수 있습니다. 이러한 이유로 외래 키를 제거할 때는 거의 항상 reverse_lock_order
를 사용하는 것이 가장 좋습니다.
외래 키 업데이트하는 방법
때때로 외래 키 제약 조건을 변경해야 하며, 열은 유지하면서 제약 조건을 업데이트해야 합니다. 예를 들어, ON DELETE CASCADE
에서 ON DELETE SET NULL
로 또는 그 반대로 이동하는 것입니다.
PostgreSQL은 겹치는 외래 키를 추가하는 것을 막지 않습니다. 가장 최근에 추가된 제약 조건을 존중합니다. 이를 통해 외래 키 보호를 잃지 않고도 외래 키를 교체할 수 있습니다.
외래 키를 교체하려면:
-
새로운 외래 키 추가:
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
-
이전 외래 키 제거:
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
절을 정의해야 하며, 99%의 경우 이 절은 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
또는 위의 훅을 사용하는 데 더 큰 문제가 발생합니다. 특정 방법에 대한 대안은 여러 데이터베이스에서 dependent: :nullify
및 dependent: :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
외래 키를 기본 키로 사용하면 공간을 절약할 수 있지만, 배치 카운팅에서 서비스 핑을 덜 효율적으로 만들 수 있습니다.
서비스 핑에 대해 테이블이 중요하다면 일반 id
열을 사용하는 것을 고려하세요.