1441 lines
44 KiB
Markdown
1441 lines
44 KiB
Markdown
# SQLAlchemy Relationship 완벽 가이드
|
|
|
|
> SQLAlchemy 2.0+ / Python 3.10+ 기준
|
|
|
|
---
|
|
|
|
## 목차
|
|
|
|
1. [개요](#1-개요)
|
|
2. [기본 개념](#2-기본-개념)
|
|
3. [relationship 정의 문법](#3-relationship-정의-문법)
|
|
4. [부모와 자식에서의 정의 차이](#4-부모와-자식에서의-정의-차이)
|
|
5. [FK 필드 vs back_populates](#5-fk-필드-vs-back_populates)
|
|
6. [relationship 옵션 상세](#6-relationship-옵션-상세)
|
|
7. [관계별 정의 방법 (1:1, 1:N, N:M)](#7-관계별-정의-방법-11-1n-nm)
|
|
8. [ORM 사용법](#8-orm-사용법)
|
|
9. [실무 패턴](#9-실무-패턴)
|
|
10. [Quick Reference](#10-quick-reference)
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
### 1.1 relationship이란?
|
|
|
|
`relationship()`은 SQLAlchemy ORM에서 **테이블 간의 관계를 Python 객체로 매핑**하는 기능입니다.
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Database Level ORM Level │
|
|
│ ────────────── ───────── │
|
|
│ FOREIGN KEY → relationship() │
|
|
│ JOIN 쿼리 → object.related_objects │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 1.2 ForeignKey vs relationship
|
|
|
|
| 구분 | ForeignKey | relationship |
|
|
|------|------------|--------------|
|
|
| **역할** | DB 레벨 제약조건 | ORM 레벨 객체 연결 |
|
|
| **위치** | 자식 테이블에만 | 양쪽 모두 가능 |
|
|
| **필수 여부** | FK 관계에 필수 | 선택사항 (편의 기능) |
|
|
| **결과** | 컬럼 생성 | Python 속성 생성 |
|
|
|
|
```python
|
|
# ForeignKey: DB에 실제 컬럼 생성
|
|
project_id: Mapped[int] = mapped_column(ForeignKey("project.id"))
|
|
|
|
# relationship: Python 객체 접근 경로 생성
|
|
project: Mapped["Project"] = relationship("Project")
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 기본 개념
|
|
|
|
### 2.1 용어 정의
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ 부모 (Parent) 자식 (Child) │
|
|
│ ───────────── ───────────── │
|
|
│ • "One" 쪽 • "Many" 쪽 │
|
|
│ • FK를 참조받음 • FK를 정의함 │
|
|
│ • 예: Project • 예: Image │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 2.2 관계 방향
|
|
|
|
```python
|
|
# 단방향 (Unidirectional)
|
|
# - 한쪽에서만 relationship 정의
|
|
# - 반대쪽 접근 불가
|
|
|
|
# 양방향 (Bidirectional)
|
|
# - 양쪽 모두 relationship 정의
|
|
# - back_populates로 연결
|
|
# - 실무 권장 방식
|
|
```
|
|
|
|
### 2.3 기본 예제 (가장 간단한 형태)
|
|
|
|
```python
|
|
from sqlalchemy import ForeignKey, String
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
# 부모 테이블
|
|
class Project(Base):
|
|
__tablename__ = "project"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(100))
|
|
|
|
# 자식들에 대한 접근 경로
|
|
images: Mapped[list["Image"]] = relationship("Image", back_populates="project")
|
|
|
|
|
|
# 자식 테이블
|
|
class Image(Base):
|
|
__tablename__ = "image"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
project_id: Mapped[int] = mapped_column(ForeignKey("project.id")) # FK
|
|
filename: Mapped[str] = mapped_column(String(255))
|
|
|
|
# 부모에 대한 접근 경로
|
|
project: Mapped["Project"] = relationship("Project", back_populates="images")
|
|
```
|
|
|
|
---
|
|
|
|
## 3. relationship 정의 문법
|
|
|
|
### 3.1 기본 문법
|
|
|
|
```python
|
|
from sqlalchemy.orm import relationship, Mapped
|
|
from typing import List
|
|
|
|
# 기본 형태
|
|
속성명: Mapped[타입] = relationship("대상클래스", 옵션들...)
|
|
|
|
# 부모 측 (1:N에서 "1")
|
|
children: Mapped[List["Child"]] = relationship("Child", back_populates="parent")
|
|
|
|
# 자식 측 (1:N에서 "N")
|
|
parent: Mapped["Parent"] = relationship("Parent", back_populates="children")
|
|
```
|
|
|
|
### 3.2 필수 항목
|
|
|
|
| 항목 | 설명 | 예시 |
|
|
|------|------|------|
|
|
| **첫 번째 인자** | 대상 모델 클래스명 (문자열) | `"Project"` |
|
|
|
|
```python
|
|
# 최소한의 정의 (단방향)
|
|
project: Mapped["Project"] = relationship("Project")
|
|
```
|
|
|
|
### 3.3 권장 항목
|
|
|
|
| 항목 | 설명 | 예시 |
|
|
|------|------|------|
|
|
| `back_populates` | 반대편 relationship 속성명 | `back_populates="images"` |
|
|
|
|
```python
|
|
# 권장하는 정의 (양방향)
|
|
project: Mapped["Project"] = relationship("Project", back_populates="images")
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 부모와 자식에서의 정의 차이
|
|
|
|
### 4.1 구조 비교
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 부모 (Parent) │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ • ForeignKey: 없음 │
|
|
│ • relationship 타입: List["Child"] 또는 list["Child"] │
|
|
│ • cascade 옵션: 여기에 정의 (삭제 정책) │
|
|
│ • 역할: 자식 컬렉션에 대한 접근 제공 │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
│
|
|
│ 참조
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 자식 (Child) │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ • ForeignKey: 있음 (필수) │
|
|
│ • relationship 타입: "Parent" (단수) │
|
|
│ • cascade 옵션: 일반적으로 정의 안 함 │
|
|
│ • 역할: 부모 객체에 대한 접근 제공 │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 4.2 부모 클래스 정의
|
|
|
|
```python
|
|
class Project(Base):
|
|
"""부모 클래스 - FK 없음, 자식 컬렉션 관리"""
|
|
__tablename__ = "project"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
store_name: Mapped[str] = mapped_column(String(255))
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# relationship 정의 (부모 측)
|
|
# ─────────────────────────────────────────────────────
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image", # 대상 클래스
|
|
back_populates="project", # 자식의 relationship 속성명
|
|
cascade="all, delete-orphan", # 삭제 정책 (부모에서 정의)
|
|
lazy="selectin", # 로딩 전략
|
|
order_by="Image.created_at", # 정렬 (선택)
|
|
)
|
|
```
|
|
|
|
**부모 측 특징:**
|
|
- `Mapped[list["Child"]]` - 복수형 리스트 타입
|
|
- `cascade` 옵션을 여기서 정의
|
|
- FK 컬럼 없음
|
|
|
|
### 4.3 자식 클래스 정의
|
|
|
|
```python
|
|
class Image(Base):
|
|
"""자식 클래스 - FK 있음, 부모 참조"""
|
|
__tablename__ = "image"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
original_filename: Mapped[str] = mapped_column(String(255))
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# ForeignKey 정의 (자식 측에서만)
|
|
# ─────────────────────────────────────────────────────
|
|
project_id: Mapped[int] = mapped_column(
|
|
ForeignKey("project.id", ondelete="CASCADE")
|
|
)
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# relationship 정의 (자식 측)
|
|
# ─────────────────────────────────────────────────────
|
|
project: Mapped["Project"] = relationship(
|
|
"Project", # 대상 클래스
|
|
back_populates="images", # 부모의 relationship 속성명
|
|
)
|
|
```
|
|
|
|
**자식 측 특징:**
|
|
- `Mapped["Parent"]` - 단수형 타입
|
|
- FK 컬럼 정의 필수
|
|
- cascade는 보통 정의하지 않음
|
|
|
|
### 4.4 비교 표
|
|
|
|
| 항목 | 부모 (One) | 자식 (Many) |
|
|
|------|-----------|-------------|
|
|
| **ForeignKey** | ❌ 없음 | ✅ 필수 |
|
|
| **Mapped 타입** | `list["Child"]` | `"Parent"` |
|
|
| **cascade** | ✅ 여기서 정의 | ❌ 보통 안 함 |
|
|
| **back_populates** | 자식의 속성명 | 부모의 속성명 |
|
|
| **접근 결과** | 리스트 | 단일 객체 |
|
|
|
|
---
|
|
|
|
## 5. FK 필드 vs back_populates
|
|
|
|
### 5.1 두 가지 접근 방식
|
|
|
|
```python
|
|
class Image(Base):
|
|
# 방식 1: FK 필드 직접 사용
|
|
project_id: Mapped[int] = mapped_column(ForeignKey("project.id"))
|
|
|
|
# 방식 2: relationship 사용
|
|
project: Mapped["Project"] = relationship("Project", back_populates="images")
|
|
```
|
|
|
|
### 5.2 FK 필드 직접 사용
|
|
|
|
```python
|
|
# ─────────────────────────────────────────────────────
|
|
# FK 필드로 직접 조작
|
|
# ─────────────────────────────────────────────────────
|
|
|
|
# 생성
|
|
image = Image(
|
|
project_id=1, # FK 값 직접 지정
|
|
original_filename="photo.jpg"
|
|
)
|
|
session.add(image)
|
|
session.commit()
|
|
|
|
|
|
# 수정
|
|
image = session.get(Image, 1)
|
|
image.project_id = 2 # FK 값 직접 변경
|
|
session.commit()
|
|
|
|
|
|
# 조회
|
|
image = session.get(Image, 1)
|
|
print(image.project_id) # 정수 값: 1
|
|
|
|
# 부모 객체 접근하려면 추가 쿼리 필요
|
|
project = session.get(Project, image.project_id)
|
|
```
|
|
|
|
**특징:**
|
|
- 단순 정수 값 조작
|
|
- 객체 관계를 신경 쓰지 않음
|
|
- 추가 쿼리 없이 FK 값만 필요할 때 유용
|
|
|
|
### 5.3 relationship (back_populates) 사용
|
|
|
|
```python
|
|
# ─────────────────────────────────────────────────────
|
|
# relationship으로 객체 조작
|
|
# ─────────────────────────────────────────────────────
|
|
|
|
# 생성 - 객체로 연결
|
|
project = session.get(Project, 1)
|
|
image = Image(
|
|
project=project, # 객체로 연결 (FK 자동 설정)
|
|
original_filename="photo.jpg"
|
|
)
|
|
session.add(image)
|
|
session.commit()
|
|
|
|
print(image.project_id) # 자동으로 1 설정됨!
|
|
|
|
|
|
# 수정 - 객체로 변경
|
|
image = session.get(Image, 1)
|
|
new_project = session.get(Project, 2)
|
|
image.project = new_project # 객체로 변경
|
|
session.commit()
|
|
|
|
print(image.project_id) # 자동으로 2로 변경됨!
|
|
|
|
|
|
# 조회 - 객체 직접 접근
|
|
image = session.get(Image, 1)
|
|
print(image.project.store_name) # 바로 객체 속성 접근
|
|
```
|
|
|
|
**특징:**
|
|
- 객체 지향적 접근
|
|
- FK 값 자동 동기화
|
|
- 양방향 자동 업데이트
|
|
|
|
### 5.4 양방향 동기화 동작
|
|
|
|
```python
|
|
# back_populates의 핵심: 양쪽 자동 동기화
|
|
|
|
project = Project(store_name="카페")
|
|
image = Image(original_filename="photo.jpg")
|
|
|
|
# 방법 1: 부모에 자식 추가
|
|
project.images.append(image)
|
|
print(image.project) # <Project ...> - 자동 설정!
|
|
print(image.project_id) # None (아직 flush 전)
|
|
|
|
session.add(project)
|
|
session.flush()
|
|
print(image.project_id) # 1 - flush 후 FK 설정됨
|
|
|
|
|
|
# 방법 2: 자식에 부모 지정
|
|
image2 = Image(original_filename="logo.png")
|
|
image2.project = project
|
|
print(image2 in project.images) # True - 자동 추가!
|
|
```
|
|
|
|
### 5.5 비교 표
|
|
|
|
| 상황 | FK 필드 사용 | relationship 사용 |
|
|
|------|-------------|------------------|
|
|
| **부모 ID만 필요** | ✅ `image.project_id` | ⚠️ 불필요한 객체 로딩 가능 |
|
|
| **부모 객체 접근** | ❌ 추가 쿼리 필요 | ✅ `image.project.name` |
|
|
| **자식 추가** | `image.project_id = 1` | `project.images.append(image)` |
|
|
| **일괄 생성** | FK 값 직접 지정 | 객체 연결로 자동 설정 |
|
|
| **양방향 동기화** | ❌ 수동 관리 | ✅ 자동 |
|
|
|
|
### 5.6 공식 권장 사용 방식
|
|
|
|
```python
|
|
class Image(Base):
|
|
__tablename__ = "image"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
|
|
# 1. FK 필드 - DB 레벨 관계 (필수)
|
|
project_id: Mapped[int] = mapped_column(ForeignKey("project.id"))
|
|
|
|
# 2. relationship - ORM 레벨 편의 기능 (권장)
|
|
project: Mapped["Project"] = relationship("Project", back_populates="images")
|
|
|
|
|
|
# 사용 시:
|
|
# - 단순 FK 값 조회/설정: project_id 사용
|
|
# - 객체 조작/탐색: project (relationship) 사용
|
|
|
|
image = session.get(Image, 1)
|
|
print(image.project_id) # 빠름 (추가 쿼리 없음)
|
|
print(image.project.store_name) # 객체 접근 (필요시 쿼리)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. relationship 옵션 상세
|
|
|
|
### 6.1 필수/권장 옵션
|
|
|
|
#### 첫 번째 인자: 대상 클래스
|
|
|
|
```python
|
|
# 문자열 (Forward Reference) - 권장
|
|
relationship("Project")
|
|
|
|
# 클래스 직접 참조 (순환 import 주의)
|
|
relationship(Project)
|
|
```
|
|
|
|
#### back_populates (양방향 연결)
|
|
|
|
```python
|
|
# 부모 측
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
back_populates="project" # Image.project와 연결
|
|
)
|
|
|
|
# 자식 측
|
|
project: Mapped["Project"] = relationship(
|
|
"Project",
|
|
back_populates="images" # Project.images와 연결
|
|
)
|
|
```
|
|
|
|
### 6.2 cascade 옵션
|
|
|
|
자식 객체의 생명주기 관리. **부모 측에서 정의**합니다.
|
|
|
|
```python
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
cascade="all, delete-orphan", # 가장 일반적
|
|
)
|
|
```
|
|
|
|
#### cascade 값 종류
|
|
|
|
| 값 | 설명 |
|
|
|----|------|
|
|
| `save-update` | 부모 저장 시 자식도 저장 (기본값에 포함) |
|
|
| `merge` | 부모 merge 시 자식도 merge |
|
|
| `expunge` | 부모 expunge 시 자식도 expunge |
|
|
| `delete` | 부모 삭제 시 자식도 삭제 |
|
|
| `delete-orphan` | 부모에서 분리된 자식 삭제 |
|
|
| `refresh-expire` | 부모 refresh 시 자식도 refresh |
|
|
| `all` | 위 모든 것 (delete-orphan 제외) |
|
|
|
|
#### 일반적인 조합
|
|
|
|
```python
|
|
# 1. 기본값 (자동 적용)
|
|
cascade="save-update, merge"
|
|
|
|
# 2. 부모 삭제 시 자식도 삭제
|
|
cascade="all, delete"
|
|
|
|
# 3. 부모에서 분리 시에도 삭제 (가장 엄격)
|
|
cascade="all, delete-orphan"
|
|
|
|
# 4. 삭제 방지 (자식은 독립적)
|
|
cascade="save-update, merge" # delete 없음
|
|
```
|
|
|
|
#### cascade 동작 예시
|
|
|
|
```python
|
|
class Project(Base):
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
# delete-orphan 동작
|
|
project = session.get(Project, 1)
|
|
image = project.images[0]
|
|
project.images.remove(image) # 부모에서 분리
|
|
session.commit()
|
|
# → image가 DB에서도 삭제됨 (orphan이 됨)
|
|
|
|
# delete 동작
|
|
project = session.get(Project, 1)
|
|
session.delete(project)
|
|
session.commit()
|
|
# → 모든 project.images도 삭제됨
|
|
```
|
|
|
|
### 6.3 lazy 옵션 (로딩 전략)
|
|
|
|
관계 데이터를 언제/어떻게 로딩할지 결정합니다.
|
|
|
|
```python
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
lazy="selectin", # 로딩 전략
|
|
)
|
|
```
|
|
|
|
#### lazy 값 종류
|
|
|
|
| 값 | 로딩 시점 | 쿼리 방식 | 사용 상황 |
|
|
|----|----------|----------|----------|
|
|
| `select` | 접근 시 (기본값) | 개별 SELECT | 거의 사용 안 함 |
|
|
| `selectin` | 부모 로딩 후 | SELECT ... IN | **1:N 권장** |
|
|
| `joined` | 부모와 함께 | JOIN | 1:1, N:1 권장 |
|
|
| `subquery` | 부모 로딩 후 | 서브쿼리 | 복잡한 경우 |
|
|
| `raise` | 접근 시 에러 | - | 명시적 로딩 강제 |
|
|
| `noload` | 로딩 안 함 | - | 특수 상황 |
|
|
| `dynamic` | Query 객체 반환 | - | 대량 데이터 |
|
|
| `write_only` | 쓰기 전용 | - | 대량 데이터 (2.0) |
|
|
|
|
#### 로딩 전략 예시
|
|
|
|
```python
|
|
# 1. Lazy Loading (기본값) - N+1 문제 발생!
|
|
class Project(Base):
|
|
images: Mapped[list["Image"]] = relationship("Image", lazy="select")
|
|
|
|
projects = session.scalars(select(Project)).all()
|
|
for project in projects:
|
|
print(project.images) # 매번 쿼리 발생! (N+1)
|
|
|
|
|
|
# 2. selectin - 권장 (1:N)
|
|
class Project(Base):
|
|
images: Mapped[list["Image"]] = relationship("Image", lazy="selectin")
|
|
|
|
projects = session.scalars(select(Project)).all()
|
|
# 쿼리 1: SELECT * FROM project
|
|
# 쿼리 2: SELECT * FROM image WHERE project_id IN (1, 2, 3, ...)
|
|
for project in projects:
|
|
print(project.images) # 추가 쿼리 없음
|
|
|
|
|
|
# 3. joined - 권장 (1:1, N:1)
|
|
class Image(Base):
|
|
project: Mapped["Project"] = relationship("Project", lazy="joined")
|
|
|
|
images = session.scalars(select(Image)).all()
|
|
# 쿼리: SELECT image.*, project.* FROM image JOIN project ...
|
|
for image in images:
|
|
print(image.project.name) # 추가 쿼리 없음
|
|
|
|
|
|
# 4. raise - 명시적 로딩 강제
|
|
class Project(Base):
|
|
images: Mapped[list["Image"]] = relationship("Image", lazy="raise")
|
|
|
|
project = session.get(Project, 1)
|
|
print(project.images) # 에러! 명시적 로딩 필요
|
|
```
|
|
|
|
### 6.4 uselist 옵션
|
|
|
|
반환 타입을 리스트/단일 객체로 지정합니다.
|
|
|
|
```python
|
|
# 기본값: True (리스트)
|
|
images: Mapped[list["Image"]] = relationship("Image", uselist=True)
|
|
|
|
# 1:1 관계에서 단일 객체
|
|
profile: Mapped["Profile"] = relationship("Profile", uselist=False)
|
|
```
|
|
|
|
### 6.5 foreign_keys 옵션
|
|
|
|
여러 FK가 있을 때 명시적 지정이 필요합니다.
|
|
|
|
```python
|
|
class Message(Base):
|
|
sender_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
|
|
receiver_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
|
|
|
|
# 어떤 FK를 사용할지 명시
|
|
sender: Mapped["User"] = relationship(
|
|
"User",
|
|
foreign_keys=[sender_id],
|
|
)
|
|
receiver: Mapped["User"] = relationship(
|
|
"User",
|
|
foreign_keys=[receiver_id],
|
|
)
|
|
```
|
|
|
|
### 6.6 primaryjoin 옵션
|
|
|
|
복잡한 조인 조건을 직접 정의합니다.
|
|
|
|
```python
|
|
from sqlalchemy import and_
|
|
|
|
class Project(Base):
|
|
# 활성 이미지만 조회
|
|
active_images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
primaryjoin=lambda: and_(
|
|
Project.id == Image.project_id,
|
|
Image.is_active == True
|
|
),
|
|
viewonly=True,
|
|
)
|
|
```
|
|
|
|
### 6.7 order_by 옵션
|
|
|
|
자식 컬렉션의 기본 정렬 순서를 지정합니다.
|
|
|
|
```python
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
order_by="Image.created_at.desc()", # 최신순
|
|
)
|
|
|
|
# 또는 명시적으로
|
|
from sqlalchemy import desc
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
order_by=desc(Image.created_at),
|
|
)
|
|
```
|
|
|
|
### 6.8 viewonly 옵션
|
|
|
|
읽기 전용 관계로 지정합니다 (쓰기 비활성화).
|
|
|
|
```python
|
|
# 통계/조회용 관계
|
|
recent_images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
primaryjoin="and_(Project.id == Image.project_id, Image.created_at > func.now() - interval '7 days')",
|
|
viewonly=True, # 이 관계로는 추가/삭제 불가
|
|
)
|
|
```
|
|
|
|
### 6.9 옵션 종합 표
|
|
|
|
| 옵션 | 기본값 | 설명 | 주로 사용 위치 |
|
|
|------|--------|------|---------------|
|
|
| `back_populates` | None | 양방향 연결 | 양쪽 |
|
|
| `cascade` | `save-update, merge` | 삭제 정책 | 부모 |
|
|
| `lazy` | `select` | 로딩 전략 | 양쪽 |
|
|
| `uselist` | True | 리스트/단일 | 1:1에서 False |
|
|
| `foreign_keys` | 자동 감지 | FK 명시 | 복수 FK 시 |
|
|
| `primaryjoin` | 자동 생성 | 조인 조건 | 복잡한 조건 |
|
|
| `order_by` | None | 정렬 순서 | 부모 |
|
|
| `viewonly` | False | 읽기 전용 | 특수 관계 |
|
|
|
|
---
|
|
|
|
## 7. 관계별 정의 방법 (1:1, 1:N, N:M)
|
|
|
|
### 7.1 1:1 (One-to-One)
|
|
|
|
한 레코드가 다른 테이블의 한 레코드와만 연결됩니다.
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐
|
|
│ User │────────│ Profile │
|
|
│ (parent) │ 1:1 │ (child) │
|
|
└─────────────┘ └─────────────┘
|
|
```
|
|
|
|
#### 기본 예제
|
|
|
|
```python
|
|
class User(Base):
|
|
__tablename__ = "user"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
username: Mapped[str] = mapped_column(String(50), unique=True)
|
|
|
|
# 1:1 관계 - uselist=False
|
|
profile: Mapped["Profile"] = relationship(
|
|
"Profile",
|
|
back_populates="user",
|
|
uselist=False, # 핵심! 단일 객체 반환
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
|
|
class Profile(Base):
|
|
__tablename__ = "profile"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
ForeignKey("user.id"),
|
|
unique=True # 1:1 보장
|
|
)
|
|
bio: Mapped[str | None] = mapped_column(Text)
|
|
|
|
user: Mapped["User"] = relationship(
|
|
"User",
|
|
back_populates="profile",
|
|
)
|
|
```
|
|
|
|
#### 1:1 핵심 포인트
|
|
|
|
```python
|
|
# 1. FK에 unique=True
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), unique=True)
|
|
|
|
# 2. 부모 relationship에 uselist=False
|
|
profile: Mapped["Profile"] = relationship("Profile", uselist=False)
|
|
```
|
|
|
|
#### 사용 예시
|
|
|
|
```python
|
|
# 생성
|
|
user = User(username="john")
|
|
user.profile = Profile(bio="Hello, I'm John") # 단일 객체 할당
|
|
session.add(user)
|
|
session.commit()
|
|
|
|
# 조회
|
|
user = session.get(User, 1)
|
|
print(user.profile.bio) # 직접 접근 (리스트 아님)
|
|
|
|
# 수정
|
|
user.profile.bio = "Updated bio"
|
|
session.commit()
|
|
|
|
# 교체
|
|
user.profile = Profile(bio="New profile") # 기존 profile은 orphan 삭제
|
|
session.commit()
|
|
```
|
|
|
|
### 7.2 1:N (One-to-Many)
|
|
|
|
한 레코드가 여러 레코드와 연결됩니다. **가장 일반적인 관계입니다.**
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐
|
|
│ Project │────────<│ Image │
|
|
│ (parent) │ 1:N │ (child) │
|
|
└─────────────┘ └─────────────┘
|
|
```
|
|
|
|
#### 기본 예제
|
|
|
|
```python
|
|
class Project(Base):
|
|
__tablename__ = "project"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
store_name: Mapped[str] = mapped_column(String(255))
|
|
|
|
# 1:N 관계 - 리스트 타입
|
|
images: Mapped[list["Image"]] = relationship(
|
|
"Image",
|
|
back_populates="project",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
|
|
|
|
class Image(Base):
|
|
__tablename__ = "image"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
project_id: Mapped[int] = mapped_column(ForeignKey("project.id"))
|
|
filename: Mapped[str] = mapped_column(String(255))
|
|
|
|
# N:1 관계
|
|
project: Mapped["Project"] = relationship(
|
|
"Project",
|
|
back_populates="images",
|
|
)
|
|
```
|
|
|
|
#### 사용 예시
|
|
|
|
```python
|
|
# 생성 - 방법 1: 부모에 추가
|
|
project = Project(store_name="카페")
|
|
project.images.append(Image(filename="logo.png"))
|
|
project.images.append(Image(filename="photo.jpg"))
|
|
session.add(project)
|
|
session.commit()
|
|
|
|
# 생성 - 방법 2: 한번에 정의
|
|
project = Project(
|
|
store_name="카페",
|
|
images=[
|
|
Image(filename="logo.png"),
|
|
Image(filename="photo.jpg"),
|
|
]
|
|
)
|
|
session.add(project)
|
|
session.commit()
|
|
|
|
# 조회
|
|
project = session.get(Project, 1)
|
|
for image in project.images:
|
|
print(image.filename)
|
|
|
|
# 자식에서 부모 접근
|
|
image = session.get(Image, 1)
|
|
print(image.project.store_name)
|
|
|
|
# 자식 추가
|
|
project.images.append(Image(filename="new.png"))
|
|
session.commit()
|
|
|
|
# 자식 제거 (delete-orphan이면 DB에서도 삭제)
|
|
image_to_remove = project.images[0]
|
|
project.images.remove(image_to_remove)
|
|
session.commit()
|
|
```
|
|
|
|
### 7.3 N:M (Many-to-Many)
|
|
|
|
양쪽 모두 여러 레코드와 연결됩니다. **연결 테이블(Association Table)이 필요합니다.**
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
│ Post │────────<│ post_tag │>────────│ Tag │
|
|
│ │ N:M │ (중간테이블) │ N:M │ │
|
|
└─────────────┘ └─────────────┘ └─────────────┘
|
|
```
|
|
|
|
#### 방법 1: Association Table (단순 연결)
|
|
|
|
```python
|
|
from sqlalchemy import Table, Column, Integer, ForeignKey
|
|
|
|
# 중간 테이블 정의 (모델 클래스 없이)
|
|
post_tag = Table(
|
|
"post_tag",
|
|
Base.metadata,
|
|
Column("post_id", Integer, ForeignKey("post.id"), primary_key=True),
|
|
Column("tag_id", Integer, ForeignKey("tag.id"), primary_key=True),
|
|
)
|
|
|
|
|
|
class Post(Base):
|
|
__tablename__ = "post"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
title: Mapped[str] = mapped_column(String(200))
|
|
|
|
# N:M 관계 - secondary로 중간 테이블 지정
|
|
tags: Mapped[list["Tag"]] = relationship(
|
|
"Tag",
|
|
secondary=post_tag, # 중간 테이블
|
|
back_populates="posts",
|
|
)
|
|
|
|
|
|
class Tag(Base):
|
|
__tablename__ = "tag"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(50), unique=True)
|
|
|
|
posts: Mapped[list["Post"]] = relationship(
|
|
"Post",
|
|
secondary=post_tag,
|
|
back_populates="tags",
|
|
)
|
|
```
|
|
|
|
#### N:M 사용 예시
|
|
|
|
```python
|
|
# 생성
|
|
post = Post(title="Python 팁")
|
|
tag1 = Tag(name="python")
|
|
tag2 = Tag(name="tutorial")
|
|
|
|
post.tags.append(tag1)
|
|
post.tags.append(tag2)
|
|
session.add(post)
|
|
session.commit()
|
|
|
|
# 또는 한번에
|
|
post = Post(
|
|
title="Python 팁",
|
|
tags=[Tag(name="python"), Tag(name="tutorial")]
|
|
)
|
|
|
|
# 조회
|
|
post = session.get(Post, 1)
|
|
for tag in post.tags:
|
|
print(tag.name)
|
|
|
|
tag = session.get(Tag, 1)
|
|
for post in tag.posts:
|
|
print(post.title)
|
|
|
|
# 태그 추가/제거
|
|
post.tags.append(existing_tag)
|
|
post.tags.remove(tag_to_remove) # 중간 테이블에서만 삭제
|
|
session.commit()
|
|
```
|
|
|
|
#### 방법 2: Association Object (추가 데이터 필요 시)
|
|
|
|
중간 테이블에 추가 컬럼이 필요한 경우 사용합니다.
|
|
|
|
```python
|
|
class PostTag(Base):
|
|
"""중간 테이블 - 추가 데이터 포함"""
|
|
__tablename__ = "post_tag"
|
|
|
|
post_id: Mapped[int] = mapped_column(ForeignKey("post.id"), primary_key=True)
|
|
tag_id: Mapped[int] = mapped_column(ForeignKey("tag.id"), primary_key=True)
|
|
|
|
# 추가 데이터
|
|
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
|
created_by: Mapped[int | None] = mapped_column(ForeignKey("user.id"))
|
|
|
|
# 양쪽 관계
|
|
post: Mapped["Post"] = relationship("Post", back_populates="post_tags")
|
|
tag: Mapped["Tag"] = relationship("Tag", back_populates="post_tags")
|
|
|
|
|
|
class Post(Base):
|
|
__tablename__ = "post"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
title: Mapped[str] = mapped_column(String(200))
|
|
|
|
post_tags: Mapped[list["PostTag"]] = relationship(
|
|
"PostTag",
|
|
back_populates="post",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
# 편의를 위한 프로퍼티
|
|
@property
|
|
def tags(self) -> list["Tag"]:
|
|
return [pt.tag for pt in self.post_tags]
|
|
|
|
|
|
class Tag(Base):
|
|
__tablename__ = "tag"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(50))
|
|
|
|
post_tags: Mapped[list["PostTag"]] = relationship(
|
|
"PostTag",
|
|
back_populates="tag",
|
|
)
|
|
```
|
|
|
|
#### Association Object 사용 예시
|
|
|
|
```python
|
|
# 생성 - 추가 데이터 포함
|
|
post = Post(title="Python 팁")
|
|
tag = Tag(name="python")
|
|
|
|
post_tag = PostTag(tag=tag, created_by=current_user.id)
|
|
post.post_tags.append(post_tag)
|
|
|
|
session.add(post)
|
|
session.commit()
|
|
|
|
# 조회 - 추가 데이터 접근
|
|
for pt in post.post_tags:
|
|
print(f"Tag: {pt.tag.name}, Added: {pt.created_at}")
|
|
```
|
|
|
|
### 7.4 관계 비교 표
|
|
|
|
| 관계 | FK 위치 | 부모 타입 | 자식 타입 | 특수 설정 |
|
|
|------|---------|----------|----------|----------|
|
|
| **1:1** | 자식 | `Mapped["Child"]` | `Mapped["Parent"]` | `uselist=False`, `unique=True` |
|
|
| **1:N** | 자식 | `Mapped[list["Child"]]` | `Mapped["Parent"]` | 기본 설정 |
|
|
| **N:M** | 중간 테이블 | `Mapped[list["Other"]]` | `Mapped[list["Other"]]` | `secondary=` |
|
|
|
|
---
|
|
|
|
## 8. ORM 사용법
|
|
|
|
### 8.1 생성 (Create)
|
|
|
|
```python
|
|
# ─────────────────────────────────────────────────────
|
|
# 방법 1: relationship으로 연결
|
|
# ─────────────────────────────────────────────────────
|
|
project = Project(store_name="카페")
|
|
|
|
# append로 추가
|
|
project.images.append(Image(filename="logo.png"))
|
|
project.images.append(Image(filename="photo.jpg"))
|
|
|
|
session.add(project) # project만 add해도 images도 저장됨
|
|
session.commit()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 방법 2: 생성자에서 한번에
|
|
# ─────────────────────────────────────────────────────
|
|
project = Project(
|
|
store_name="카페",
|
|
images=[
|
|
Image(filename="logo.png"),
|
|
Image(filename="photo.jpg"),
|
|
]
|
|
)
|
|
session.add(project)
|
|
session.commit()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 방법 3: FK 직접 지정
|
|
# ─────────────────────────────────────────────────────
|
|
project = Project(store_name="카페")
|
|
session.add(project)
|
|
session.flush() # ID 생성
|
|
|
|
image = Image(project_id=project.id, filename="logo.png")
|
|
session.add(image)
|
|
session.commit()
|
|
```
|
|
|
|
### 8.2 조회 (Read)
|
|
|
|
```python
|
|
# ─────────────────────────────────────────────────────
|
|
# 기본 조회
|
|
# ─────────────────────────────────────────────────────
|
|
project = session.get(Project, 1)
|
|
print(project.images) # lazy 설정에 따라 로딩
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# Eager Loading (명시적)
|
|
# ─────────────────────────────────────────────────────
|
|
from sqlalchemy.orm import selectinload, joinedload
|
|
|
|
# selectinload - 1:N에 권장
|
|
stmt = (
|
|
select(Project)
|
|
.options(selectinload(Project.images))
|
|
.where(Project.id == 1)
|
|
)
|
|
project = session.scalar(stmt)
|
|
|
|
# joinedload - 1:1, N:1에 권장
|
|
stmt = (
|
|
select(Image)
|
|
.options(joinedload(Image.project))
|
|
.where(Image.id == 1)
|
|
)
|
|
image = session.scalar(stmt)
|
|
|
|
# 중첩 로딩
|
|
stmt = (
|
|
select(Project)
|
|
.options(
|
|
selectinload(Project.lyrics)
|
|
.selectinload(Lyric.songs)
|
|
)
|
|
)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 필터링과 함께
|
|
# ─────────────────────────────────────────────────────
|
|
# 특정 조건의 자식을 가진 부모
|
|
stmt = (
|
|
select(Project)
|
|
.join(Project.images)
|
|
.where(Image.filename.like("%.png"))
|
|
.distinct()
|
|
)
|
|
|
|
# 자식 개수와 함께
|
|
from sqlalchemy import func
|
|
|
|
stmt = (
|
|
select(Project, func.count(Image.id).label("image_count"))
|
|
.join(Project.images, isouter=True)
|
|
.group_by(Project.id)
|
|
)
|
|
```
|
|
|
|
### 8.3 수정 (Update)
|
|
|
|
```python
|
|
# ─────────────────────────────────────────────────────
|
|
# 자식 추가
|
|
# ─────────────────────────────────────────────────────
|
|
project = session.get(Project, 1)
|
|
project.images.append(Image(filename="new.png"))
|
|
session.commit()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 자식 수정
|
|
# ─────────────────────────────────────────────────────
|
|
project = session.get(Project, 1)
|
|
project.images[0].filename = "updated.png"
|
|
session.commit()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 부모 변경 (relationship 사용)
|
|
# ─────────────────────────────────────────────────────
|
|
image = session.get(Image, 1)
|
|
new_project = session.get(Project, 2)
|
|
image.project = new_project # FK 자동 업데이트
|
|
session.commit()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 부모 변경 (FK 직접 사용)
|
|
# ─────────────────────────────────────────────────────
|
|
image = session.get(Image, 1)
|
|
image.project_id = 2
|
|
session.commit()
|
|
```
|
|
|
|
### 8.4 삭제 (Delete)
|
|
|
|
```python
|
|
# ─────────────────────────────────────────────────────
|
|
# 부모 삭제 (cascade 동작)
|
|
# ─────────────────────────────────────────────────────
|
|
# cascade="all, delete-orphan" 설정 시
|
|
project = session.get(Project, 1)
|
|
session.delete(project)
|
|
session.commit()
|
|
# → 모든 images도 삭제됨
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 자식만 삭제
|
|
# ─────────────────────────────────────────────────────
|
|
image = session.get(Image, 1)
|
|
session.delete(image)
|
|
session.commit()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 부모에서 분리 (delete-orphan 시 삭제됨)
|
|
# ─────────────────────────────────────────────────────
|
|
project = session.get(Project, 1)
|
|
image = project.images[0]
|
|
project.images.remove(image)
|
|
session.commit()
|
|
# → delete-orphan이면 image도 DB에서 삭제
|
|
|
|
|
|
# ─────────────────────────────────────────────────────
|
|
# 자식 전체 교체
|
|
# ─────────────────────────────────────────────────────
|
|
project = session.get(Project, 1)
|
|
project.images = [Image(filename="new1.png"), Image(filename="new2.png")]
|
|
session.commit()
|
|
# → 기존 images는 orphan이 되어 삭제됨 (delete-orphan 시)
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 실무 패턴
|
|
|
|
### 9.1 표준 모델 템플릿
|
|
|
|
```python
|
|
from datetime import datetime
|
|
from typing import List
|
|
from sqlalchemy import String, Text, ForeignKey, func
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class TimestampMixin:
|
|
"""생성/수정 시간 공통 믹스인"""
|
|
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
default=func.now(),
|
|
onupdate=func.now()
|
|
)
|
|
|
|
|
|
class Project(TimestampMixin, Base):
|
|
__tablename__ = "project"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
store_name: Mapped[str] = mapped_column(String(255))
|
|
task_id: Mapped[str] = mapped_column(String(36), unique=True, index=True)
|
|
|
|
# 1:N 관계들
|
|
images: Mapped[List["Image"]] = relationship(
|
|
"Image",
|
|
back_populates="project",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
order_by="Image.created_at.desc()",
|
|
)
|
|
|
|
lyrics: Mapped[List["Lyric"]] = relationship(
|
|
"Lyric",
|
|
back_populates="project",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Project(id={self.id}, store_name='{self.store_name}')>"
|
|
|
|
|
|
class Image(TimestampMixin, Base):
|
|
__tablename__ = "image"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
project_id: Mapped[int] = mapped_column(
|
|
ForeignKey("project.id", ondelete="CASCADE"),
|
|
index=True,
|
|
)
|
|
original_filename: Mapped[str] = mapped_column(String(255))
|
|
stored_filename: Mapped[str] = mapped_column(String(255))
|
|
url: Mapped[str] = mapped_column(Text)
|
|
|
|
# N:1 관계
|
|
project: Mapped["Project"] = relationship(
|
|
"Project",
|
|
back_populates="images",
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Image(id={self.id}, filename='{self.original_filename}')>"
|
|
```
|
|
|
|
### 9.2 서비스 레이어 패턴
|
|
|
|
```python
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
|
|
class ProjectService:
|
|
def __init__(self, session: Session):
|
|
self.session = session
|
|
|
|
def create_with_images(
|
|
self,
|
|
store_name: str,
|
|
task_id: str,
|
|
image_data: list[dict]
|
|
) -> Project:
|
|
"""프로젝트와 이미지를 함께 생성"""
|
|
project = Project(
|
|
store_name=store_name,
|
|
task_id=task_id,
|
|
images=[Image(**data) for data in image_data],
|
|
)
|
|
self.session.add(project)
|
|
self.session.commit()
|
|
self.session.refresh(project)
|
|
return project
|
|
|
|
def get_with_images(self, project_id: int) -> Project | None:
|
|
"""프로젝트와 이미지를 함께 조회"""
|
|
stmt = (
|
|
select(Project)
|
|
.options(selectinload(Project.images))
|
|
.where(Project.id == project_id)
|
|
)
|
|
return self.session.scalar(stmt)
|
|
|
|
def get_by_task_id(self, task_id: str) -> Project | None:
|
|
"""task_id로 조회"""
|
|
stmt = (
|
|
select(Project)
|
|
.options(selectinload(Project.images))
|
|
.where(Project.task_id == task_id)
|
|
)
|
|
return self.session.scalar(stmt)
|
|
|
|
def add_image(self, project_id: int, image: Image) -> Image:
|
|
"""기존 프로젝트에 이미지 추가"""
|
|
project = self.session.get(Project, project_id)
|
|
if not project:
|
|
raise ValueError("Project not found")
|
|
|
|
project.images.append(image)
|
|
self.session.commit()
|
|
self.session.refresh(image)
|
|
return image
|
|
|
|
def delete(self, project_id: int) -> bool:
|
|
"""프로젝트 삭제 (이미지도 cascade 삭제)"""
|
|
project = self.session.get(Project, project_id)
|
|
if not project:
|
|
return False
|
|
|
|
self.session.delete(project)
|
|
self.session.commit()
|
|
return True
|
|
```
|
|
|
|
### 9.3 FastAPI 엔드포인트 패턴
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
|
|
|
|
|
@router.post("/", response_model=ProjectResponse)
|
|
def create_project(
|
|
data: ProjectCreate,
|
|
session: Session = Depends(get_session),
|
|
):
|
|
service = ProjectService(session)
|
|
project = service.create_with_images(
|
|
store_name=data.store_name,
|
|
task_id=data.task_id,
|
|
image_data=[img.model_dump() for img in data.images],
|
|
)
|
|
return project
|
|
|
|
|
|
@router.get("/{project_id}", response_model=ProjectWithImagesResponse)
|
|
def get_project(
|
|
project_id: int,
|
|
session: Session = Depends(get_session),
|
|
):
|
|
service = ProjectService(session)
|
|
project = service.get_with_images(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
return project
|
|
|
|
|
|
@router.post("/{project_id}/images", response_model=ImageResponse)
|
|
def add_image(
|
|
project_id: int,
|
|
data: ImageCreate,
|
|
session: Session = Depends(get_session),
|
|
):
|
|
service = ProjectService(session)
|
|
image = Image(**data.model_dump())
|
|
return service.add_image(project_id, image)
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Quick Reference
|
|
|
|
### 10.1 relationship 정의 체크리스트
|
|
|
|
```python
|
|
# ✅ 부모 (1 쪽)
|
|
children: Mapped[list["Child"]] = relationship(
|
|
"Child", # 1. 대상 클래스
|
|
back_populates="parent", # 2. 반대편 속성명
|
|
cascade="all, delete-orphan", # 3. 삭제 정책
|
|
lazy="selectin", # 4. 로딩 전략
|
|
)
|
|
|
|
# ✅ 자식 (N 쪽)
|
|
parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id")) # FK 필수!
|
|
parent: Mapped["Parent"] = relationship(
|
|
"Parent", # 1. 대상 클래스
|
|
back_populates="children", # 2. 반대편 속성명
|
|
)
|
|
```
|
|
|
|
### 10.2 관계별 빠른 참조
|
|
|
|
```python
|
|
# 1:1
|
|
# 부모: uselist=False
|
|
# 자식 FK: unique=True
|
|
|
|
# 1:N
|
|
# 부모: list["Child"]
|
|
# 자식: "Parent"
|
|
|
|
# N:M
|
|
# secondary=association_table
|
|
# 또는 Association Object 패턴
|
|
```
|
|
|
|
### 10.3 자주 쓰는 옵션 조합
|
|
|
|
```python
|
|
# 기본 1:N (부모 측)
|
|
cascade="all, delete-orphan", lazy="selectin"
|
|
|
|
# 기본 N:1 (자식 측)
|
|
# 옵션 없이 back_populates만
|
|
|
|
# 1:1 (부모 측)
|
|
uselist=False, cascade="all, delete-orphan"
|
|
|
|
# 읽기 전용 관계
|
|
viewonly=True
|
|
|
|
# 복수 FK
|
|
foreign_keys=[column]
|
|
```
|
|
|
|
### 10.4 흔한 실수
|
|
|
|
```python
|
|
# ❌ 잘못된 예
|
|
class Parent(Base):
|
|
children = relationship("Child") # Mapped 타입 힌트 없음
|
|
|
|
class Child(Base):
|
|
parent_id = Column(Integer) # ForeignKey 없음
|
|
parent = relationship("Parent", back_populates="childs") # 오타
|
|
|
|
|
|
# ✅ 올바른 예
|
|
class Parent(Base):
|
|
children: Mapped[list["Child"]] = relationship("Child", back_populates="parent")
|
|
|
|
class Child(Base):
|
|
parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
|
|
parent: Mapped["Parent"] = relationship("Parent", back_populates="children")
|
|
```
|
|
|
|
---
|
|
|
|
## 부록: 참고 자료
|
|
|
|
- [SQLAlchemy 2.0 공식 문서](https://docs.sqlalchemy.org/en/20/)
|
|
- [SQLAlchemy Relationship Configuration](https://docs.sqlalchemy.org/en/20/orm/relationships.html)
|
|
- [SQLAlchemy Basic Relationship Patterns](https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html)
|