레일즈의 다형성 연관관계는 데이터 무결성을 약화시키고 확장 시 문제를 일으킬 수 있습니다. 이에 대한 대안은 다음과 같습니다.
레일즈 다형성 연관관계의 문제점
레일즈의 다형성 연관관계는 _type 및 _id 컬럼을 사용하여 여러 모델과 연결되는데, 이는 관계형 데이터베이스의 참조 무결성을 직접적으로 보장하지 못합니다. GitLab 개발자 문서, SQL Antipatterns 저자 등 다수의 전문가들은 이러한 방식이 대규모 시스템에서 성능 저하와 복잡성을 초래할 수 있다고 경고합니다.
단순한 대안
-
개별 테이블 사용: GitLab 개발자 문서에서는 각 타입마다 별도의 테이블을 사용할 것을 권장합니다.
-
CHECK 제약조건 활용: PostgreSQL에서는 공통 컬럼 세트에서 약간만 벗어나는 소수의 간단한 타입에 대해 각 서브타입에 CHECK 제약조건이 있는 테이블을 사용하는 것이 적합합니다. 예를 들어,
poly_type에 따라 특정 컬럼의NULL여부를 강제하는 방식입니다.sql CREATE TABLE poly ( poly_id serial PRIMARY KEY , poly_type "char" NOT NULL REFERENCES poly_type , common_col text , a_col1 text , a_col2 text , b_col3 text , CONSTRAINT poly_allowed_cols_type_a CHECK (poly_type = 'a' OR (a_col1, a_col2) IS NULL) , CONSTRAINT poly_allowed_cols_type_b CHECK (poly_type = 'b' OR (b_col3) IS NULL) );
데이터베이스에서 다형성 연관관계 재구현 (슈퍼타입 테이블)
더 엄격한 대안은 슈퍼타입(Supertype) 테이블을 도입하는 것입니다. 예를 들어, standup_updates와 comments에 모두 반응(reactions)을 추가하고 싶다면, 이 둘의 슈퍼타입인 reactionables 테이블을 생성합니다.
참조 무결성 보장
reactionables 테이블은 id와 type을 가지며, 각 서브타입 테이블(standup_updates, comments)은 reactionable_id를 통해 reactionables를 참조합니다. 이때, 서브타입이 실제 reactionables 테이블에 존재하는지 확인하기 위해 사용자 정의 함수와 CHECK 제약조건을 사용합니다.
-
check_reactionable_exists함수:target_id와target_type을 받아reactionables테이블에 해당 레코드가 있는지 확인하는 함수를 생성합니다.ruby class AddCheckConstraintFunction < ActiveRecord::Migration[8.0] def up execute <<-SQL CREATE OR REPLACE FUNCTION check_reactionable_exists(target_id bigint, target_type CHAR(1)) RETURNS int AS ' SELECT COALESCE( (SELECT 1 FROM reactionables WHERE reactionables.id = target_id AND reactionables.type = target_type), 0 ); ' LANGUAGE SQL; SQL end # ... (down method) end -
CHECK 제약조건 추가: 각 서브타입 테이블에
reactionable_id와 해당 서브타입의 고유 식별자(예: ‘S’ forstandup_updates, ‘C’ forcomments)를 사용하여 위 함수를 호출하는 CHECK 제약조건을 추가합니다.ruby class AddCheckConstraintToStandupUpdates < ActiveRecord::Migration[8.0] def up execute <<-SQL ALTER TABLE standup_updates ADD CONSTRAINT supertype_check CHECK(check_reactionable_exists(standup_updates.reactionable_id, 'S') = 1); SQL end # ... (down method) end
고아 레코드 방지
슈퍼타입 레코드가 서브타입 레코드보다 먼저 삭제되지 않도록 BEFORE DELETE 트리거를 사용하여 연쇄 삭제를 구현합니다. 이는 서브타입 레코드 삭제 시 관련 슈퍼타입 reactionable 레코드도 함께 삭제되도록 하여 고아 레코드를 방지합니다.
```sql CREATE FUNCTION cascade_delete_reactionable() RETURNS trigger AS \(BEGIN DELETE FROM reactionables WHERE reactionables.id=OLD.id; RETURN OLD; END;\) LANGUAGE plpgsql;
CREATE TRIGGER cascade_delete_standup_updates BEFORE DELETE ON standup_updates FOR EACH ROW EXECUTE PROCEDURE cascade_delete_reactionable(); ```