다형성 연관
요약: 다형성 연관 대신 별도의 테이블을 항상 사용하십시오.
Rails를 사용하면 “다형성 연관”이라고 하는 것을 정의할 수 있습니다. 이는 일반적으로 테이블에 두 개의 열을 추가하여 가능합니다: 대상 유형 열과 대상 ID입니다. 예를 들어, 작성 시점에서 members
에 대해 다음과 같은 열이 있는 설정이 있습니다.
-
source_type
: 사용할 모델을 정의하는 문자열로,Project
또는Namespace
일 수 있습니다. -
source_id
:source_type
을 기반으로 검색할 행의 ID입니다. 예를 들어,source_type
이Project
인 경우source_id
에는 프로젝트 ID가 포함됩니다.
이런 설정이 유용해 보일 수 있지만, 이는 피할 수 있는 만큼 많은 결점이 있습니다.
공간 낭비
이 설정은 모델 결정을 위해 문자열 값을 사용하므로 많은 공간을 낭비합니다. 예를 들어, Project
및 Namespace
의 경우, PostgreSQL을 사용할 때 문자열마다 추가 바이트가 필요하며 최대 크기는 9바이트입니다. 테이블 및 행이 이러한 설정을 사용하는 경우 디스크 공간과 메모리 (모든 인덱스에 대해)을 상당히 낭비할 수 있습니다.
인덱스
연관이 두 개의 열로 분할되기 때문에 쿼리를 효율적으로 수행하기 위해 복합 인덱스가 필요할 수 있습니다. 복합 인덱스는 전혀 잘못된 것이 아니지만, 이러한 인덱스의 열 순서는 최적의 성능을 보장하기 위해 중요할 수 있습니다.
일관성
다형성 연관의 실제 큰 문제 중 하나는 외래 키를 사용하여 데이터 일관성을 데이터베이스 수준에서 강제할 수 없다는 것입니다. 데이터베이스 수준에서 일관성을 강제하려면 다형성 연관을 지원하기 위한 고유한 외래 키 논리를 작성해야 합니다.
데이터베이스 수준에서 일관성을 강제하는 것은 건강한 환경을 유지하기 위해 절대적으로 중요하며, 따라서 다형성 연관을 피해야 하는 또 다른 이유입니다.
쿼리 오버헤드
다형성 연관을 사용할 때 항상 두 열을 사용하여 필터링해야 합니다. 예를 들어 다음과 같은 쿼리를 작성할 수 있습니다.
SELECT *
FROM members
WHERE source_type = 'Project'
AND source_id = 13083;
여기서 PostgreSQL은 두 열이 모두 인덱싱되어 있다면 쿼리를 효율적으로 수행할 수 있습니다. 쿼리가 더 복잡해지면 이러한 인덱스를 효과적으로 사용할 수 없게 될 수도 있습니다.
혼합 역할
함수와 클래스와 마찬가지로, 테이블은 단일 책임을 가져야 합니다: 특정 사전 정의된 열 집합으로 데이터를 저장하기. 다형성 연관을 사용하면 테이블에 동일한 테이블에 서로 다른 유형의 데이터를 (아마도 서로 다른 열 집합으로) 저장합니다.
해결방법
다행히도 이러한 문제에 대한 해결책이 있습니다: 동일한 테이블에 저장할 수 있는 각 유형에 대해 별도의 테이블을 사용하십시오. 별도의 테이블을 사용하면 데이터 일관성을 보장하고 쿼리를 효율적으로 수행하기 위해 데이터베이스가 제공하는 모든 것을 활용할 수 있으며, 추가 응용 프로그램 논리가 필요하지 않습니다.
예를 들어, 프로젝트와 그룹의 모두가 승인된 멤버 및 보류 중인 멤버를 저장하는 members
테이블이 있다고 가정해보겠습니다. 보류 상태는 열 requested_at
가 설정되어 있는지 여부에 따라 결정됩니다. 이러한 설정은 스키마적으로이 테이블에서 특정 행에만 열이 설정되어 공간이 낭비되는 경우가 있습니다. 또한 특정 인덱스가 특정 행에만 설정될 수 있습니다. 마지막으로, 이러한 테이블에서 쿼리를 수행하는 것은 좋지 않은 쿼리가 될 수 있습니다. 예를 들어:
SELECT *
FROM members
WHERE requested_at IS NULL
AND source_type = 'GroupMember'
AND source_id = 4
대신 이러한 테이블을 별도의 테이블로 분할해야 합니다. 예를 들어, 이 경우 4개의 테이블이 생성될 수 있습니다.
project_members
group_members
pending_project_members
pending_group_members
이렇게 하면 데이터를 쿼리하는 것이 매우 간단해집니다. 예를 들어, 그룹의 멤버를 얻으려면 다음을 실행할 수 있습니다.
SELECT *
FROM group_members
WHERE group_id = 4
다시 말해, 그룹의 보류 중인 멤버를 모두 얻으려면 다음을 실행할 수 있습니다.
SELECT *
FROM pending_group_members
WHERE group_id = 4
둘 모두를 얻고 싶다면 UNION
을 사용할 수 있지만, 그 결과 집합이 첫 번째 쿼리의 열을 사용하기 때문에 SELECT
할 열을 명시적으로 지정해야 합니다. 예를 들어:
SELECT id, 'Group' AS target_type, group_id AS target_id
FROM group_members
UNION ALL
SELECT id, 'Project' AS target_type, project_id AS target_id
FROM project_members
위 예제는 아마도 다소 어리석은 것처럼 보일 수 있지만, 데이터를 병합하여 동일한 페이지에서 표시하는 데는 아무런 장애물이 없음을 보여줍니다. 열을 명시적으로 선택하면 데이터베이스가 데이터를 얻기 위해 덜 작업해야 하므로 쿼리 수행도 가속화될 수 있습니다.
스키마가 더욱 단순해집니다. 이제 더 이상 source_type
열을 저장하고 인덱싱할 필요가 없으며, 외래 키를 쉽게 정의할 수 있으며, IS NULL
조건을 사용하여 행을 필터링할 필요가 없습니다.
요약하면, 별도의 테이블을 사용하면 외래 키를 효과적으로 사용할 수 있고 필요한 곳에만 인덱스를 정의하며 공간을 절약하고 데이터를 효율적으로 쿼리하고 이러한 테이블을 더 쉽게 확장할 수 있습니다(예: 별도의 디스크에 저장). 이로 인해 코드도 단순화되어 단일 모델이 서로 다른 종류의 데이터를 처리하는 데 책임이 없어집니다.