relationships

star 0

1:N, N:M 관계 및 eager/lazy loading을 구현합니다.

woogi-kang By woogi-kang schedule Updated 6/5/2026

name: relationships description: | 1:N, N:M 관계 및 eager/lazy loading을 구현합니다. metadata: category: "💻 개발" version: "1.0.0"

Relationships Skill

1:N, N:M 관계 및 eager/lazy loading을 구현합니다.

Triggers

  • "관계", "relationship", "1:N", "N:M", "eager loading"

Input

항목 필수 설명
projectPath 프로젝트 경로

Output

One-to-Many Relationship

# app/infrastructure/database/models/user.py
from sqlalchemy import String, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.infrastructure.database.session import Base


class UserModel(Base):
    """User model with one-to-many relationship to posts."""

    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(String(255), unique=True)
    name: Mapped[str] = mapped_column(String(100))

    # One-to-Many: User -> Posts
    posts: Mapped[list["PostModel"]] = relationship(
        "PostModel",
        back_populates="author",
        lazy="selectin",  # Eager load by default
        cascade="all, delete-orphan",
    )

    # One-to-Many: User -> Comments
    comments: Mapped[list["CommentModel"]] = relationship(
        "CommentModel",
        back_populates="author",
        lazy="noload",  # Don't load by default
    )
# app/infrastructure/database/models/post.py
from sqlalchemy import ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.infrastructure.database.session import Base


class PostModel(Base):
    """Post model with many-to-one relationship to user."""

    __tablename__ = "posts"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    content: Mapped[str] = mapped_column(Text)
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))

    # Many-to-One: Post -> User
    author: Mapped["UserModel"] = relationship(
        "UserModel",
        back_populates="posts",
        lazy="joined",  # Always join load
    )

    # One-to-Many: Post -> Comments
    comments: Mapped[list["CommentModel"]] = relationship(
        "CommentModel",
        back_populates="post",
        lazy="selectin",
        cascade="all, delete-orphan",
    )

Many-to-Many Relationship

# app/infrastructure/database/models/associations.py
from sqlalchemy import Column, ForeignKey, Table

from app.infrastructure.database.session import Base

# Association table for Post <-> Tag (Many-to-Many)
post_tags = Table(
    "post_tags",
    Base.metadata,
    Column("post_id", ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True),
    Column("tag_id", ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True),
)

# Association table for User <-> Role (Many-to-Many)
user_roles = Table(
    "user_roles",
    Base.metadata,
    Column("user_id", ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
    Column("role_id", ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
)
# app/infrastructure/database/models/tag.py
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.infrastructure.database.models.associations import post_tags
from app.infrastructure.database.session import Base


class TagModel(Base):
    """Tag model with many-to-many relationship to posts."""

    __tablename__ = "tags"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50), unique=True)

    # Many-to-Many: Tag <-> Posts
    posts: Mapped[list["PostModel"]] = relationship(
        "PostModel",
        secondary=post_tags,
        back_populates="tags",
        lazy="selectin",
    )
# app/infrastructure/database/models/post.py (updated)
from app.infrastructure.database.models.associations import post_tags


class PostModel(Base):
    __tablename__ = "posts"

    # ... other fields

    # Many-to-Many: Post <-> Tags
    tags: Mapped[list["TagModel"]] = relationship(
        "TagModel",
        secondary=post_tags,
        back_populates="posts",
        lazy="selectin",
    )

Many-to-Many with Extra Fields

# app/infrastructure/database/models/order.py
from datetime import datetime
from sqlalchemy import ForeignKey, Integer, Numeric, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.infrastructure.database.session import Base


class OrderItemModel(Base):
    """Order item (association model with extra fields)."""

    __tablename__ = "order_items"

    id: Mapped[int] = mapped_column(primary_key=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    quantity: Mapped[int] = mapped_column(Integer, default=1)
    unit_price: Mapped[float] = mapped_column(Numeric(10, 2))

    # Relationships
    order: Mapped["OrderModel"] = relationship(back_populates="items")
    product: Mapped["ProductModel"] = relationship()


class OrderModel(Base):
    """Order model."""

    __tablename__ = "orders"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    status: Mapped[str] = mapped_column(String(20), default="pending")
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now()
    )

    # Relationships
    user: Mapped["UserModel"] = relationship()
    items: Mapped[list["OrderItemModel"]] = relationship(
        back_populates="order",
        cascade="all, delete-orphan",
        lazy="selectin",
    )

    @property
    def total_amount(self) -> float:
        return sum(item.quantity * item.unit_price for item in self.items)

Loading Strategies

# app/infrastructure/repositories/post.py
from sqlalchemy import select
from sqlalchemy.orm import selectinload, joinedload, noload

from app.infrastructure.database.models.post import PostModel


class PostRepositoryImpl:
    """Post repository with different loading strategies."""

    async def get_with_author(self, post_id: int) -> PostModel | None:
        """Get post with author (joined load)."""
        result = await self._session.execute(
            select(PostModel)
            .options(joinedload(PostModel.author))
            .where(PostModel.id == post_id)
        )
        return result.scalar_one_or_none()

    async def get_with_comments(self, post_id: int) -> PostModel | None:
        """Get post with comments (selectin load)."""
        result = await self._session.execute(
            select(PostModel)
            .options(selectinload(PostModel.comments))
            .where(PostModel.id == post_id)
        )
        return result.scalar_one_or_none()

    async def get_with_all_relations(self, post_id: int) -> PostModel | None:
        """Get post with all relations."""
        result = await self._session.execute(
            select(PostModel)
            .options(
                joinedload(PostModel.author),
                selectinload(PostModel.comments),
                selectinload(PostModel.tags),
            )
            .where(PostModel.id == post_id)
        )
        return result.scalar_one_or_none()

    async def list_without_relations(
        self,
        offset: int = 0,
        limit: int = 20,
    ) -> list[PostModel]:
        """List posts without loading relations."""
        result = await self._session.execute(
            select(PostModel)
            .options(
                noload(PostModel.author),
                noload(PostModel.comments),
                noload(PostModel.tags),
            )
            .offset(offset)
            .limit(limit)
        )
        return result.scalars().all()

    async def list_by_tag(self, tag_id: int) -> list[PostModel]:
        """List posts by tag (using many-to-many)."""
        result = await self._session.execute(
            select(PostModel)
            .join(PostModel.tags)
            .where(TagModel.id == tag_id)
            .options(selectinload(PostModel.tags))
        )
        return result.scalars().all()

Nested Schema Response

# app/schemas/post.py
from datetime import datetime
from pydantic import BaseModel


class AuthorResponse(BaseModel):
    """Nested author response."""

    id: int
    name: str
    email: str

    model_config = {"from_attributes": True}


class TagResponse(BaseModel):
    """Nested tag response."""

    id: int
    name: str

    model_config = {"from_attributes": True}


class CommentResponse(BaseModel):
    """Nested comment response."""

    id: int
    content: str
    author: AuthorResponse
    created_at: datetime

    model_config = {"from_attributes": True}


class PostResponse(BaseModel):
    """Post response with nested relations."""

    id: int
    title: str
    content: str
    author: AuthorResponse
    tags: list[TagResponse]
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class PostDetailResponse(PostResponse):
    """Post detail with comments."""

    comments: list[CommentResponse]

Routes with Relations

# app/api/v1/routes/posts.py
from fastapi import APIRouter

router = APIRouter(prefix="/posts", tags=["posts"])


@router.get("/{post_id}", response_model=PostResponse)
async def get_post(post_id: int, service: PostSvc):
    """Get post with author and tags."""
    return await service.get_with_relations(post_id)


@router.get("/{post_id}/detail", response_model=PostDetailResponse)
async def get_post_detail(post_id: int, service: PostSvc):
    """Get post with all details including comments."""
    return await service.get_with_all_relations(post_id)


@router.post("/{post_id}/tags/{tag_id}")
async def add_tag_to_post(
    post_id: int,
    tag_id: int,
    service: PostSvc,
    _: ActiveUser,
):
    """Add tag to post (many-to-many)."""
    return await service.add_tag(post_id, tag_id)


@router.delete("/{post_id}/tags/{tag_id}")
async def remove_tag_from_post(
    post_id: int,
    tag_id: int,
    service: PostSvc,
    _: ActiveUser,
):
    """Remove tag from post."""
    return await service.remove_tag(post_id, tag_id)

References

  • _references/REPOSITORY-PATTERN.md
Install via CLI
npx skills add https://github.com/woogi-kang/craft-harness --skill relationships
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator