외래 키와 연관 관계
GitLab v19.4요약
외래 키는 관련된 데이터베이스 테이블 사이의 일관성을 보장합니다. 애플리케이션 수준에서 데이터 일관성을 보장하는 방식은 일부 불운한 경우에 실패할 수 있고, 그러면 결국 테이블에 일관되지 않은 데이터가 남습니다. 다른 테이블의 레코드를 참조하는 테이블을 만들 때는 데이터 무결성을 유지하기 위해 FK를 추가해야 합니다.
외래 키는 관련된 데이터베이스 테이블 사이의 일관성을 보장합니다. Rails 4부터 Rails에는 데이터베이스 테이블에 외래 키 제약을 추가하는
마이그레이션 헬퍼가 포함되어 있습니다. Rails 4 이전에는 일정 수준의 일관성을 보장하는 유일한 방법이 연관 관계 정의의
dependent
옵션이었습니다.
애플리케이션 수준에서 데이터 일관성을 보장하는 방식은 일부 불운한 경우에 실패할 수 있고, 그러면 결국 테이블에 일관되지 않은 데이터가 남습니다. 이는 주로 데이터베이스 수준에서 일관성을 보장하는 프레임워크 지원이 없었던 오래된 테이블에 영향을 줍니다. 이러한 데이터 불일치는 예상하지 못한 애플리케이션 동작이나 버그를 일으킬 수 있습니다.
다른 테이블의 레코드를 참조하는 테이블을 만들 때는 데이터 무결성을 유지하기 위해 FK를 추가해야 합니다. 그리고 모델에 연관 관계를 추가할 때는 외래 키도 반드시 함께 추가해야 합니다. 또한 외래 키를 추가할 때는 항상 인덱스를 먼저 추가해야 합니다.
예를 들어 다음과 같은 모델이 있다고 가정합니다:
class User < ActiveRecord::Base
has_many :posts
end
여기서는 posts.user_id 칼럼에 외래 키를 추가합니다. 이렇게 하면
데이터베이스 수준에서 데이터 일관성이 강제됩니다. 또한 외래 키가 있으면
Rails가 직접 처리하는 대신 데이터베이스가 연관 데이터를 제거할 수
있습니다(예: 사용자를 제거할 때).
다운타임 및 마이그레이션 실패 방지#
외래 키를 추가하는 작업은 두 부분으로 나뉩니다:
- FK 칼럼과 제약을 추가합니다.
- 데이터 무결성을 유지하기 위해 추가한 제약을 검증합니다.
(1)은 가장 엄격한 락(ACCESS EXCLUSIVE)을 잡는 ALTER TABLE 문을 사용하고, 제약을 검증하려면 테이블 전체를 순회해야 하므로 크거나 트래픽이 많은 테이블에서는 시간이 오래 걸립니다.
따라서 거의 모든 경우에 더 엄격한 락을 오래 유지해 테이블의 다른 작업을 장시간 차단하지 않도록 두 작업을 별도의 트랜잭션으로 실행해야 합니다.
새 테이블에서#
- 새 테이블이 다른 테이블 하나만 참조한다면 간단합니다. 참조되는 테이블과 무관하게
create_table (t.references, ..., foreign_key: true)를 사용할 수 있습니다. - 새 테이블이 서로 다른 두 테이블을 참조한다면 외래 키가 두 개일 때 새 테이블 만들기를 참고합니다.
새 칼럼에서#
레코드가 많지 않은 새 테이블이라면 다음 방법 중 하나를 사용할 수 있습니다. 외래 키를 두 개 추가해야 한다면, 같은 마이그레이션에서 테이블을 두 개 이상 락하지 않도록 마이그레이션을 나눕니다.
- add_reference(... foreign_key: true)
- 같은 트랜잭션에서 add_column(...)과 add_foreign_key(...) 사용.
그 외 모든 경우에는 칼럼 추가, FK 제약 추가, 제약 검증을 각각 별도의 트랜잭션에서 수행해야 합니다.
기존 칼럼에서#
기존 데이터베이스 칼럼에 외래 키를 추가하려면 데이터베이스 구조 변경과 데이터 변경이 필요할 수 있습니다.
테이블이 사용 중이라면 항상 일관되지 않은 데이터가 있다고 가정해야 합니다.
기존 칼럼에 FK 제약을 추가하는 일은 여러 마일스톤에 걸친 과정입니다:
N.M: 칼럼에NOT VALIDFK 제약을 추가합니다. 이렇게 하면 일관되지 않은 레코드가 생성되거나 업데이트되지도 않습니다.N.M: 기존 레코드를 수정하거나 정리하는 데이터 마이그레이션을 추가합니다.- 마이그레이션 쿼리가 타이밍 가이드라인 안에 들어온다면 일반 마이그레이션이나 배포 후 마이그레이션으로 처리할 수 있습니다.
- 그렇지 않다면 배치 백그라운드 마이그레이션으로 처리해야 합니다.
- FK 제약을 검증합니다.
- 데이터 마이그레이션이 일반 마이그레이션이나 배포 후 마이그레이션이었다면 같은 마일스톤에서 제약을 검증할 수 있습니다.
- 백그라운드 마이그레이션이었다면 BBM이 완료된 후에만 FK를 검증할 수 있습니다. 데이터 마이그레이션이 백그라운드에서 아직 실행되는 동안 FK 검증이 일어나지 않게 하려면 이렇게 해야 합니다.
기존 칼럼이든 새 칼럼이든 외래 키 제약을 추가할 때는 해당 칼럼에 인덱스가 필요합니다.
인덱스를 비동기적으로 추가했다면
인덱스가 structure.sql에 추가될 때까지 기다려야 합니다.
이는 모든 외래 키에 필요하며, 예를 들어 효율적인 연쇄 삭제를 지원하는 데 필요합니다. 테이블의 많은 행이 삭제되면 참조된 레코드도 함께 삭제해야 합니다. 데이터베이스는 참조된 테이블에서 해당 레코드를 찾아야 합니다. 인덱스가 없으면 테이블을 순차 스캔하게 되고, 그러면 시간이 오래 걸릴 수 있습니다.
예시#
다음과 같은 테이블 구조를 생각해 봅니다:
users 테이블:
id(integer, primary key)name(string)
emails 테이블:
id(integer, primary key)user_id(integer)email(string)
ActiveRecord에서 이 관계를 표현합니다:
class User < ActiveRecord::Base
has_many :emails
end
class Email < ActiveRecord::Base
belongs_to :user
end
문제: 사용자를 제거하면 제거된 사용자와 연결된 이메일 레코드가 emails 테이블에 남습니다:
user = User.find(1)
user.destroy
emails = Email.where(user_id: 1) # returns emails for the deleted user
FK 제약 추가 (NOT VALID)#
레코드를 추가하거나 업데이트할 때 일관성을 강제하는 NOT VALID 외래 키 제약을 테이블에 추가합니다.
위 예시에서 emails 테이블의 레코드는 계속 업데이트할 수 있습니다. 그러나 존재하지 않는 값으로 user_id를 업데이트하려고 하면 제약이 오류를 발생시킵니다.
NOT VALID 외래 키를 추가하는 마이그레이션 파일:
class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.1]
milestone '17.10'
disable_ddl_transaction!
def up
add_concurrent_foreign_key(
:emails,
:users,
column: :user_id,
on_delete: :cascade,
validate: false
)
end
def down
remove_foreign_key_if_exists :emails, column: :user_id
end
end
INFO:
기본적으로 add_concurrent_foreign_key 메서드는 외래 키를 검증하므로 validate: false를 명시적으로 전달합니다.
외래 키를 검증하지 않고 추가하는 것은 빠른 작업입니다. 새 데이터에 제약을 강제할 수 있게 되기 전까지 테이블에 짧은 락만 필요합니다.
또한 add_concurrent_foreign_key는 제약이 존재하지 않을 때만 제약을 추가합니다.
소스 테이블과 대상 테이블이 동일하지 않다면, 마이그레이션 파일 하나에서
add_foreign_key나 add_concurrent_foreign_key 제약을 두 번 이상 사용하지 않습니다.
기존 레코드 수정을 위한 데이터 마이그레이션#
여기서 접근 방식은 데이터 양과 정리 전략에 따라 달라집니다. 데이터베이스 쿼리로 "유효하지 않은" 레코드를 찾을 수 있고 레코드 수가 많지 않다면, 데이터 마이그레이션을 일반 Rails 마이그레이션이나 배포 후 Rails 마이그레이션으로 실행할 수 있습니다.
데이터 양이 더 많다면(1,000건 초과) 백그라운드 마이그레이션을 만드는 편이 좋습니다. 확실하지 않다면 쿼리 가이드라인을 참고하거나 데이터베이스 프레임워크 팀에 조언을 구합니다.
데이터베이스 마이그레이션에서 emails 테이블의 레코드를 정리하는 예시:
class RemoveRecordsWithoutUserFromEmailsTable < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
class Email < ActiveRecord::Base
include EachBatch
end
def up
Email.each_batch do |batch|
batch.joins('LEFT JOIN users ON emails.user_id = users.id')
.where('users.id IS NULL')
.delete_all
end
end
def down
# Can be a no-op when data inconsistency is not affecting the pre and post deployment version of the application.
# In this case we might have records in the `emails` table where the associated record in the `users` table is not there anymore.
end
end
이 데이터 마이그레이션을 추가하는 MR에는 ~data-deletion 레이블을 적용해야 합니다. 자세한 내용은 데이터 마이그레이션을 추가할 때의 준비를 참고합니다.
외래 키 검증#
외래 키를 검증하면 테이블 전체를 스캔하면서 각 관계가 올바른지 확인합니다.
다행히 실행 중에 소스 테이블(users)을 락하지는 않습니다.
앞에서 언급했듯이 배치 백그라운드 마이그레이션을 사용할 때는 BBM이 완료된 후에만 외래 키 검증이 일어나야 합니다.
외래 키를 검증하는 마이그레이션 파일:
# frozen_string_literal: true
class ValidateForeignKeyOnEmailUsers < Gitlab::Database::Migration[2.1]
def up
validate_foreign_key :emails, :user_id
end
def down
# Can be safely a no-op if we don't roll back the inconsistent data.
end
end
비동기적으로 외래 키 검증#
매우 큰 테이블에서는 외래 키 검증이 여러 시간 동안 실행되면서 관리하기
어려워질 수 있습니다. autovacuum 같은 필수 데이터베이스 작업이 실행되지 못하고,
GitLab.com에서는 마이그레이션이 끝날 때까지 배포 프로세스가
차단됩니다.
GitLab.com에 미치는 영향을 줄이기 위해 주말 시간대에 비동기적으로 검증하는 프로세스가 있습니다. 이 시간대는 대체로 트래픽이 적고 배포도 적기 때문에 더 낮은 위험으로 FK 검증을 진행할 수 있습니다.
영향이 낮은 시간에 외래 키 검증 예약#
검증할 FK 예약#
- 외래 키를 비동기 검증 대상으로 준비하는 배포 후 마이그레이션이 포함된 머지 리퀘스트를 만듭니다.
- 외래 키를 동기적으로 검증하는 마이그레이션을 추가할 후속 이슈를 만듭니다.
- 비동기 외래 키를 준비하는 머지 리퀘스트에 후속 이슈를 언급하는 댓글을 추가합니다.
비동기 헬퍼로 외래 키를 검증하는 예시는 아래 블록에서 확인할 수
있습니다. 이 마이그레이션은 외래 키 이름을
postgres_async_foreign_key_validations 테이블에 입력합니다. 주말에 실행되는
프로세스가 이 테이블에서 외래 키를 가져와 검증을 시도합니다.
# in db/post_migrate/
FK_NAME = :fk_be5624bf37
# TODO: FK to be validated synchronously in issue or merge request
def up
# `some_column` can be an array of columns, and is not mandatory if `name` is supplied.
# `name` takes precedence over other arguments.
prepare_async_foreign_key_validation :ci_builds, :some_column, name: FK_NAME
# Or in case of partitioned tables, use:
prepare_partitioned_async_foreign_key_validation :p_ci_builds, :some_column, name: FK_NAME
end
def down
unprepare_async_foreign_key_validation :ci_builds, :some_column, name: FK_NAME
# Or in case of partitioned tables, use:
unprepare_partitioned_async_foreign_key_validation :p_ci_builds, :some_column, name: FK_NAME
end
MR이 배포되었고 프로덕션에서 FK가 유효한지 확인#
- ChatOps에서
/chatops gitlab run auto_deploy status <merge_sha>를 사용해 배포 후 마이그레이션이 GitLab.com에서 실행되었는지 확인합니다. 출력에db/gprd가 반환되면 배포 후 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 자세한 내용은 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인하는 방법을 참고합니다. - FK가 주말에 걸쳐 검증될 수 있도록 다음 주까지 기다립니다.
- Database Lab로 검증이 성공했는지 확인합니다.
출력에 외래 키가
NOT VALID로 표시되지 않는지 확인합니다.
FK를 동기적으로 검증하는 마이그레이션 추가#
프로덕션 데이터베이스에서 외래 키가 유효해진 후에는 외래 키를 동기적으로
검증하는 두 번째 머지 리퀘스트를 만듭니다. 이 두 번째 머지 리퀘스트에서
스키마 변경 사항을 structure.sql에 업데이트해 커밋해야 합니다.
동기 마이그레이션은 GitLab.com에서는 아무 동작도 하지 않지만, 다른 설치 환경을 위해
예상대로 마이그레이션을 추가해야 합니다. 다음 블록은 앞의 비동기 예시에
대한 두 번째 마이그레이션을 만드는 방법을
보여 줍니다.
validate_foreign_key가 포함된 두 번째 마이그레이션을 머지하기 전에 프로덕션에서
외래 키가 유효한지 확인합니다. 검증이 실행되기 전에 두 번째 마이그레이션이
배포되면, 두 번째 마이그레이션이 실행될 때 외래 키가 동기적으로
검증됩니다.
# in db/post_migrate/
FK_NAME = :fk_be5624bf37
def up
validate_foreign_key :ci_builds, :some_column, name: FK_NAME
end
def down
# Can be safely a no-op if we don't roll back the inconsistent data.
end
end
로컬에서 데이터베이스 FK 변경 사항 테스트#
머지 리퀘스트를 만들기 전에 데이터베이스 외래 키 변경 사항을 로컬에서 테스트해야 합니다.
비동기적으로 검증된 외래 키 확인#
로컬 환경에서 비동기 헬퍼를 사용해 외래 키 검증 변경 사항을 테스트합니다:
- Rails 콘솔에서
Feature.enable(:database_async_foreign_key_validation)을 실행해 기능 플래그를 활성화합니다. bundle exec rails db:migrate를 실행해 비동기 검증 테이블에 항목이 생성되게 합니다.bundle exec rails gitlab:db:validate_async_constraints:all을 실행해 모든 데이터베이스에서 FK가 비동기적으로 검증되게 합니다.- 외래 키를 확인하려면
GDK
명령
gdk psql로 PostgreSQL 콘솔을 열고\d+ table_name명령을 실행해 외래 키가 유효한지 확인합니다. 검증이 성공하면 외래 키 정의에서NOT VALID가 제거됩니다.
외래 키 제거#
이 작업에는 다운타임이 필요하지 않습니다. 특히 영향을 받는 테이블이 크다면 락 경합 문제를 피하기 위해 배포 후 마이그레이션에서 제거해야 합니다.
파티션 테이블에서 외래 키 제거#
파티션 테이블을 다룰 때는 일반 remove_foreign_key 메서드 대신 remove_partitioned_foreign_key 헬퍼 메서드를 사용합니다. 파티션 테이블에 아직 검증된 외래 키가 없으면 remove_foreign_key가 파티션의 외래 키를 제거하지 않기 때문에 이렇게 해야 합니다. 파티션 테이블에서 외래 키를 만들 때 validate: false 옵션을 설정한 경우가 그렇습니다.
remove_partitioned_foreign_key 메서드는 파티션 테이블과 그 모든 파티션에서 외래 키를 제거합니다:
# Remove by column name
remove_partitioned_foreign_key :partitioned_table, :referenced_table, column: :referenced_table_id
# Remove by foreign key name
remove_partitioned_foreign_key :partitioned_table, name: 'fk_rails_123456'
이 메서드는 다음과 같이 동작합니다:
- 파티션 테이블에서 외래 키를 제거하며, 이때 각 파티션의 상속된 제약도 함께 제거됩니다.
- 그다음 각 파티션에서 외래 키를 개별적으로 제거합니다(상속되지 않은 제약이 있는 경우를 위해).
- 내부적으로
remove_foreign_key_if_exists를 사용하므로 외래 키가 없어도 오류를 발생시키지 않습니다. - 일반
remove_foreign_key메서드와 같은 옵션을 지원합니다.
마이그레이션 예시:
class RemovePartitionedForeignKey < Gitlab::Database::Migration[2.3]
include Gitlab::Database::PartitioningMigrationHelpers
disable_ddl_transaction!
def up
# Add partitioned foreign key
add_concurrent_partitioned_foreign_key :partitioned_table, :projects, column: :project_id
end
def down
# Remove partitioned foreign key
remove_partitioned_foreign_key :partitioned_table, :projects, column: :project_id
end
end
외래 키에 bigint 사용#
새 외래 키를 추가할 때는 bigint로 정의해야 합니다.
참조되는 테이블의 기본 키 유형이 integer여도
새 외래 키는 bigint로 참조해야 합니다. 모든 기본 키를
bigint로 마이그레이션하고 있으므로, bigint 외래 키를 사용하면
상위 테이블을 bigint 기본 키로 마이그레이션할 때 시간이 절약되고 단계도
줄어듭니다.
reverse_lock_order#
add_concurrent_foreign_key, add_concurrent_partitioned_foreign_key,
remove_foreign_key_if_exists, remove_partitioned_foreign_key는 모두
기본값이 reverse_lock_order: true입니다. 이 설정은 제약 작업 전에 대상-소스
순서로 락을 획득해, 동시에 실행되는 애플리케이션 트랜잭션과의 데드락을
방지합니다.
이에 대한 배경은 원래 이슈에서 자세히 확인할 수 있습니다.
데드락이 발생하는 경우#
마이그레이션과 애플리케이션 코드가 같은 테이블에 서로 반대 순서로 락을 획득하면 데드락이 발생할 수 있습니다.
다음과 같이 외래 키를 추가하려는 상황을 생각해 봅니다:
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
그리고 다음과 같은 가상의 애플리케이션 코드를 생각해 봅니다:
Todo.transaction do
note = Note.create(...)
# Observe what happens if foreign key is added here!
todo = Todo.create!(note_id: note.id)
end
두 insert 문 사이에 외래 키를 만들려고 하면 데드락이 발생할 수 있습니다:
Note.create:notes에 행 락을 획득합니다.ALTER TABLE ...이todos에 테이블 락을 획득합니다.ALTER TABLE ... FOREIGN KEY가notes에 테이블 락을 획득하려 하지만, 행 락을 가진 다른 트랜잭션 때문에 차단됩니다.Todo.create가todos에 행 락을 획득하려 하지만,todos에 테이블 락을 가진 다른 트랜잭션 때문에 차단됩니다.
두 트랜잭션이 서로 끝나기를 기다리며 멈춰 있고, 결국 둘 다 타임아웃됩니다. 보통은 마이그레이션 트랜잭션 재시도로 해결되지만, 애플리케이션 코드도 타임아웃되어 사용자에게 오류를 일으킬 수 있습니다. 이 애플리케이션 코드가 자주 실행되면 마이그레이션이 계속 타임아웃되고 사용자도 오류를 자주 마주할 수 있습니다.
외래 키 제거#
외래 키를 제거할 때의 데드락도 두 테이블 모두에 락을 획득하기 때문에
비슷합니다. 위 예시를 사용하면 더 흔한 상황은
DELETE FROM notes WHERE id = ...입니다. 이 쿼리는 notes에 락을 획득한 다음
todos에 락을 획득하므로, 위에서 설명한 것과 똑같은 데드락이
발생할 수 있습니다. 이를 막기 위해 remove_foreign_key_if_exists와
remove_partitioned_foreign_key도 기본값이
reverse_lock_order: true입니다.
옵트아웃#
외래 키가 상위 테이블에서 하위 테이블을 가리키는 드문 경우(예:
merge_requests.latest_merge_request_diff_id가 merge_request_diffs.id를
참조하는 경우)에는 기본 락 순서가 최적이 아닐 수 있습니다.
reverse_lock_order: false를 명시적으로 설정해 옵트아웃할 수 있습니다.
마이그레이션에서 외래 키 업데이트#
외래 키 제약을 변경해야 하는 경우가 있습니다. 칼럼은 그대로 두고
제약 조건만 업데이트하는 경우입니다. 예를 들어 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에서 외래 키를 추가할 때 칼럼이 자동으로 인덱싱되지는 않습니다. 따라서 동시 인덱스도 함께 추가해야 합니다. 인덱스는 모든 외래 키에 필요하며 외래 키보다 먼저 추가해야 합니다. 같은 마이그레이션에서 더 앞 단계에 두거나, 외래 키를 추가하는 마이그레이션보다 앞선 마이그레이션에서 추가한다는 뜻입니다. 같은 이유로, 외래 키를 지원하는 인덱스를 제거하기 전에 외래 키를 먼저 제거해야 합니다.
외래 키에 인덱스가 없으면 참조된 테이블에서 레코드가 삭제될 때마다 PostgreSQL이
테이블 전체 스캔을 하게 됩니다. 과거에 이 때문에 projects와 namespaces를
삭제할 때 타임아웃이 발생한 사고가 있었습니다.
외래 키가 복합 인덱스의 첫 번째 위치에 있다면, 이 외래 키를 포함하는
복합 인덱스를 두는 것도 괜찮습니다.
예를 들어 project_id 외래 키가 있다면
BTREE (project_id, user_id) 같은 복합 인덱스는 괜찮지만,
BTREE (user_id, project_id) 같은 인덱스는 괜찮지 않습니다. 후자는
project_id 단독으로 효율적인 조회를 할 수 없어서 연쇄 삭제가 타임아웃되는 것을
막지 못합니다. BTREE (project_id) WHERE user_id IS NULL 같은 부분 인덱스는
연쇄 삭제에 전혀 사용될 수 없으므로 외래 키를 위한 인덱스로
적합하지 않습니다.
외래 키 이름 지정#
Ruby on Rails는 기본적으로 외래 키에 _id 접미사를 사용합니다. 그래서 이 접미사는
두 테이블 사이의 연관 관계에만 사용해야 합니다. 서드파티 플랫폼의 ID를
참조하려면 _xid 접미사를 권장합니다.
spec/db/schema_spec.rb 스펙은 _id 접미사가 붙은 모든 칼럼에 외래 키 제약이
있는지 테스트합니다. 이 스펙이 실패하면, 칼럼이 다음 기준 중 하나에 해당할 때
해당 칼럼을 ignored_fk_columns_map에 추가합니다:
- 칼럼이 다른 테이블을 참조하지만, 두 테이블이 서로 외래 키를 허용하지 않는 GitLab 스키마에 속하는 경우입니다.
- 성능상의 이유로 외래 키가 Loose Foreign Key로 대체된 경우입니다.
- 칼럼이 다형 관계를 나타내는 경우입니다. 다형 연관 관계는 사용하지 않아야 합니다.
- 칼럼이 다른 테이블을 참조할 목적이 아닌 경우입니다. 예를 들어 파티션 테이블에
partition_id를 두는 것이 흔합니다.
종속 제거#
연관 관계를 정의할 때 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
이런 경우에는 연관 테이블의 불필요한 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과 관련이 있다면 일반 id 칼럼을 사용하는 것을 고려합니다.