본문 바로가기
C.W.K.
Lesson 03 of 06 · published

조인 테이블로 하는 다중 상속

~14 min · 다중 상속, 조인 테이블, sqlite, 이벤트

Level 0밑준비
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

부모 둘은 평범한 일이야

개념 표의 요리 줄에 대놓고 적혀 있어. 다중 상속은 평범하다고. 예로 드는 게 감바스 알 아히요야. 기름에 향 내기에, 빨리 익는 단백질을 제 소스에 담아내는 게 더해진 요리지. 기법이 둘이고, 둘 다 구현한 요리라면 둘 밑에 다 앉아야 해. 기록에선 1강의 조인 테이블만 있으면 돼. class_parents는 간선 하나에 행 하나라서, 원리가 둘인 요리는 행이 둘이고, 요리 다섯을 거느린 원리는 행이 다섯이야.

방향마다 조인 하나로 시작해. parents는 자식에서 부모로 조인하고, 쿼리도 그 하나로 끝나. children은 부모에서 자식으로 조인한 다음, 자식마다 klass로 통째로 불러와. klass는 자식의 행을 읽고 증거 쿼리도 한 번 돌려. 그래서 자식이 N개면 SELECT가 1 + 2N번 나가. 요리하는 사람 한 명의 트리 크기에선 싸지만, 쿼리 하나가 아니라 루프인 건 맞아. 웹 클래스 페이지는 앞의 것을 계보 한 줄로 찍어. Inherits(상속) 뒤에 부모를 전부 늘어놓고, 변형이면 Derived from(갈라져 나온 곳)이라고 써. 뒤의 것은 Dishes that inherit it(이걸 상속한 요리들)으로, 요리 밑에선 Versions of it(이 요리의 버전들)으로 찍고. 폰은 부모마다 Inherits 링크를 하나씩 보여줘.

간선은 어디서 오나

간선 대부분은 읽기 하나로 트리를 키우는 함수 integrate가 써. 전체 흐름은 트랙 4에서 따라가. 접시 위 요리마다 읽기가 말한 원리를 많아야 둘까지 집고, 하나씩 아빠 트리에서 찾거나 새로 제안해. 요리도 같은 식으로 찾고, 원리마다 ensure_parent를 한 번씩 불러. 형제도 간선을 받아. 추론된 형제는 자기가 같이 쓴다고 말한 원리 밑에 붙고, 그 이름이 아무것과도 안 맞으면 요리의 첫 원리 밑에 붙어.

ensure_parent는 쓰기 전에 둘을 거절해. 자기 자신을 부모로 삼는 것, 그리고 이미 있는 간선. 그걸 통과해야 class.parented 이벤트를 남겨. 프로젝션은 INSERT OR IGNORE로 넣으니까, 저널에서 중복 이벤트가 재생돼도 행이 둘이 되진 않아. 복합 기본 키가 마지막 방어선이고, 도메인 검사는 아무것도 안 바꿀 이벤트가 저널에 쌓이지 않게 막아.

상한은 요리 항목 하나의 몫이야

루브릭은 요리마다 루트 원리 하나, 많아야 둘이라고 해. 코드는 슬라이스로 그걸 지켜. dish.principles[:2]. 내가 셋째 원리를 말하면 무시돼. 근데 이 경계는 요리 항목 하나에 걸린 거야. 읽기 한 번에 걸린 것도, 클래스 하나에 걸린 것도 아니야. 조인 테이블엔 한도가 없고, 부모를 더하기 전에 세는 코드도 없어. 세 갈래 길로 요리의 부모가 둘을 넘을 수 있어.

  • 읽기 한 번 안에서. 타파스 한 판엔 요리가 여럿 있을 수 있고, 읽기는 요리마다 항목을 따로 적어. 같은 요리를 또 말한 둘째 항목은 같은 클래스로 접히면서 자기 원리 둘을 따로 데려와. 이 요리를 형제로 꼽은 항목은 그 항목이 이 요리 몫으로 말한 원리 밑에 이 요리를 붙여. 어느 쪽이든 읽기가 끝나기도 전에 셋째 간선이 생겨.
  • 같은 요리의 접시를 또 읽으면 이름이 기존 클래스로 접혀. 이때 이번 읽기가 처음엔 없던 원리를 말하면 셋째 간선이 생겨.
  • 다른 접시의 읽기가 이 요리를 그 접시 원리의 형제로 꼽으면, 요리가 이미 있으니까 그 원리도 부모로 붙어.

그리고 간선을 지우는 이벤트는 없어. 카탈로그엔 class.parented만 있고 되돌리는 건 없으니까, 지난 읽기가 붙인 간선은 그 접시를 새 루브릭으로 다시 읽어도 남아. 접시와 클래스 사이 링크랑 비교해 봐. current_links 뷰는 읽기의 링크를 그 읽기가 접시의 현재 읽기일 때만 세. 증거는 옛 읽기를 잊고, 상속은 기억해.

버그냐고? 아니, 알아 둬야 할 성질이야. 상한은 요리 항목 하나가 요리를 눈에 띄는 기법마다 다 붙이지 못하게 막고, 늘어나는 부모 목록은 어떤 읽기든 그 요리에서 본 원리를 전부 기억해. 요리 기록이라면 괜찮은 기억이야. 잘못 붙은 부모를 되돌릴 수 있어야 하는 곳에 이 설계를 쓴다면, Sizzle에 없는 그 이벤트가 필요해. 부모 클래스를 잘못 고르면 뭘 치르는지는 OO Quest 트랙 5이 다뤄.

간선을 타고 찾아오는 건 없어

프로그래밍 언어의 다중 상속엔 유명한 문제가 따라와. 부모 둘이 같은 메서드를 정의하면 자식은 어느 걸 받지? 파이썬은 메서드 결정 순서로 답해. Sizzle은 답할 일이 없어. 간선에 동작이 실려 있지 않거든. klass는 그 클래스 자신의 칸만 돌려주고, 부모에서 가져오는 건 없어. 요리의 바인딩은 요리 거고, 원리의 역할은 원리에 남고, 찾아 올라가서 물려받는 필드는 하나도 없어.

간선은 보여주고 세는 데 써. 계보 한 줄, 자식 목록, Tree 페이지, 클래스 대화가 나한테 맥락으로 넘기는 부모와 자식, 그리고 원리의 증거. 원리의 증거는 4강에서 직계 자식으로 계산해. 그래서 다이아몬드가 생겨도 해가 없어. 원리 둘 밑의 요리 밑에 있는 변형은 걸어 내려갈 수 있는 길일 뿐이야. 풀어야 할 충돌이 아니야.

변형은 한 번만 갈라져 나와

변형은 부모 하나로 시작해. 자기가 갈라져 나온 요리나 변형이야. 변형에서 또 갈라질 수 있으니까, 계보가 집 버전의 사슬로 이어질 수도 있어. 대신 원리에서는 못 갈라져. 도메인은 derive_variant에서 한 번, 내가 제안한 variant 액션을 검증할 때 또 한 번 그걸 거절하고, 두 번 다 같은 메시지를 내. 다만 그 검사가 막는 건 갈라져 나오기지, 부모 붙이기가 아니야. 읽기가 요리 항목을 변형으로 찾아내면 그 변형을 원리 밑에도 붙일 수 있어. 그러면 변형의 부모 중에 원리가 끼게 되고, 그게 증거에 뭘 하는지는 4강이 보여줘. 다중 상속을 포함한 어휘 전체는 OO Quest 트랙 7에 있어.

Code

cwkSizzle, backend/reading.py: 읽기 안의 요리 항목마다 원리는 많아야 둘 (원문 그대로, 208-223행)·python
def integrate(sizzle: Sizzle, encounter_id: str, reading_id: str, reading: PlateReading) -> None:
    """Grow the tree from one reading: resolve or propose the principles and
    dishes it names, parent them, link the plate, and add inferred siblings."""
    principle_by_name: dict[str, str] = {}
    for dish in reading.dishes:
        principle_ids: list[str] = []
        for p in dish.principles[:2]:
            pid = _resolve_principle(sizzle, p, encounter_id)
            if pid:
                principle_ids.append(pid)
                principle_by_name[_key(p.name)] = pid
        dish_id = _resolve_dish(sizzle, dish, encounter_id, principle_ids)
        for pid in principle_ids:
            sizzle.ensure_parent(dish_id, pid)
        sizzle.record("class.linked", {"encounter_id": encounter_id, "class_id": dish_id, "source": "reading",
                                       "reading_id": reading_id, "confidence": dish.confidence})
cwkSizzle, backend/domain.py: 새 간선이고 자기 고리가 아닐 때만 기록해 (원문 그대로, 167-169행)·python
    def ensure_parent(self, child: str, parent: str) -> None:
        if child != parent and not self.store.has_parent(child, parent):
            self.record("class.parented", {"child": child, "parent": parent})
cwkSizzle, backend/store.py: 조인 테이블의 두 방향 (원문 그대로, 439-451행)·python
    def parents(self, class_id: str) -> list[dict[str, Any]]:
        return self._all(
            "SELECT c.id, c.kind, c.name, c.gloss, c.status FROM class_parents p JOIN classes c ON c.id = p.parent"
            " WHERE p.child = ? ORDER BY c.name",
            (class_id,),
        )

    def children(self, class_id: str) -> list[dict[str, Any]]:
        rows = self._all(
            "SELECT c.id FROM class_parents p JOIN classes c ON c.id = p.child WHERE p.parent = ? ORDER BY c.created_at",
            (class_id,),
        )
        return [self.klass(r["id"]) for r in rows]

External links

Exercise

직접 다루는 데이터에서 부모가 여럿일 수 있는 is-a 관계를 하나 찾아봐. 여러 분류에 걸친 태그, 여러 컬렉션에 든 상품, 여러 분야에 속한 기술 같은 거. 그걸 복합 기본 키를 가진 조인 테이블로 모델링해. 그리고 세 가지를 정해서 적어. 자식이 간선을 타고 읽어 오는 필드가 있는지, 있다면 뭔지. 쓰기 한 번이 부모를 몇 개까지 더할 수 있는지. 간선을 지우는 이벤트가 뭐고, 그걸 누가 낼 수 있는지.
Hint
간선을 타고 읽는 필드가 없으면 정할 결정 순서도 없고, 간선은 순수한 길 안내가 돼. 있다면 둘째 부모가 오기 전에 순서를 적어 둬. 지우기도 정해 둬. 덧붙이기만 하는 로그에선 되돌리는 이벤트가 따로 있어야 해. 프로젝션에서 행만 지우면 다음 재생 때 다시 살아나.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.