- 마이그레이션에서 외래 키 추가
- 외래 키에는
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
는 기본적으로 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에서 두 트랜잭션에서 데드락이 발생할 수 있습니다. 여기에 일어나는 과정은 다음과 같습니다.
-
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은 중복된 외래 키를 추가하는 것을 방지하지 않습니다. 가장 최근에 추가된 제약 조건을 따릅니다.
이로써 외래 키 보호 기능을 절대로 잃지 않고 외래 키를 교체할 수 있게 합니다.
외래 키를 교체하려면 다음과 같이 합니다.
-
새로운 외래 키 추가: ```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 ```
-
이전 외래 키 제거: ```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 Ping의 batch counting를 덜 효율적으로 만들 수 있습니다.
Service Ping에서 테이블이 관련이 있는 경우 일반적인 id
열을 사용하는 것을 고려하십시오.