Mapped와 DynamicMapped의 정리 - 1:1, 1:n, n:m & uselist 포함
1. Mapped[T]
- SQLAlchemy 2.0에서는 기존의 Column()과 relationship()을 사용할 때 더 명확한 타입 힌트를 제공하기 위해 Mapped[T]가 도입되었다.
1) Mapped[T]의 역할
- 타입 안정성을 제공하여 코드 가독성을 높임
- Column()뿐만 아니라 relationship()에도 사용 가능
- Optional[T], List[T] 등과 조합하여 컬럼과 관계 속성의 타입을 명확하게 표현
2) Mapped[T] 사용 방법
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from sqlalchemy.ext.declarative import DeclarativeBase
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
age: Mapped[Optional[int]] = mapped_column(nullable=True) # NULL 허용
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(nullable=False)
content: Mapped[str] = mapped_column(nullable=False)
author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
author: Mapped["User"] = relationship(back_populates="posts")
3) Mapped[T]의 장점
1. 타입 힌트를 명확하게 제공
- Mapped[int], Mapped[str]처럼 명확한 데이터 타입 지정 가능
- Mapped[Optional[T]] 사용하여 NULL 허용 여부 표현
2. 리스트 타입을 명확하게 표현
- Mapped[list["Post"]]을 사용하여 1:N 관계를 더 직관적으로 정의 가능
3. 런타임 성능 최적화
- 기존 SQLAlchemy 1.x에서 사용되던 Column()과 relationship() 방식보다 런타임에서의 속성 처리 성능이 향상됨
2. DynamicMapped[T]
- DynamicMapped[T]는 SQLAlchemy ORM에서 lazy-loading 방식으로 관계 속성을 다룰 때 사용된다.
- SQLAlchemy에서 기본적으로 relationship()을 사용할 때, 데이터를 list로 로드하는데, DynamicMapped를 사용하면 데이터를 즉시 가져오지 않고 쿼리 객체로 유지할 수 있다.
1) DynamicMapped[T]의 역할
- relationship()을 동적으로 처리하는 방식
- dynamic 로딩 옵션을 사용할 때 Mapped 대신 DynamicMapped를 사용
- 관계된 데이터를 즉시 로드하는 것이 아니라 필요할 때 쿼리를 실행하도록 만듦
2) DynamicMapped[T] 사용 방법
from sqlalchemy.orm import DynamicMapped, Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
# DynamicMapped를 사용하여 posts 관계를 동적으로 로드
posts: DynamicMapped["Post"] = relationship("Post", back_populates="author", lazy="dynamic")
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(nullable=False)
content: Mapped[str] = mapped_column(nullable=False)
author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
author: Mapped["User"] = relationship(back_populates="posts")
3) DynamicMapped[T]의 특징
1. lazy="dynamic"과 함께 사용됨
- DynamicMapped는 관계를 동적 쿼리 객체(Query)로 반환
- 즉, user.posts를 호출해도 즉시 데이터를 가져오지 않고 필요할 때만 쿼리를 실행
2. 필터링이 가능함
user = session.get(User, 1)
filtered_posts = user.posts.filter(Post.title == "SQLAlchemy Guide").all()
- user.posts는 리스트가 아니라 Query 객체이므로 필터링 및 추가적인 데이터베이스 질의가 가능
- 만약 Mapped[list["Post"]]을 사용했다면, user.posts를 호출하는 즉시 모든 데이터가 로드됨
3. 대량 데이터 처리에 유용
- DynamicMapped를 사용하면 관계된 데이터가 많을 때도 메모리를 낭비하지 않고 필요할 때만 데이터를 로드
- 예를 들어, 수천 개의 Post가 존재하는 User를 불러올 때 Mapped[list["Post"]]을 사용하면 모든 데이터를 한 번에 가져와서 성능 문제가 발생할 수 있음
- DynamicMapped는 이를 해결하기 위해 쿼리를 동적으로 실행하여 필요한 데이터만 가져옴
4. DynamicMapped 사용 예시
- 일반적인 relationship()의 문제점
from sqlalchemy.orm import relationship, Mapped
from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = 'user'
id: Mapped[int] = mapped_column(primary_key=True)
posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")
- 위 코드에서는 posts가 즉시 로딩(eager loading)되므로, User 객체를 가져올 때 관련된 모든 Post가 함께 로드된다.
- 사용하지 않을 데이터도 미리 가져오므로 불필요한 성능 저하가 발생할 수 있다.
from sqlalchemy.orm import DynamicMapped
class User(Base):
__tablename__ = 'user'
id: Mapped[int] = mapped_column(primary_key=True)
posts: DynamicMapped["Post"] = relationship("Post", back_populates="author", lazy="dynamic")
- posts가 즉시 리스트로 변환되지 않고, 실행 가능한 SQLAlchemy Query로 유지됨
- 데이터가 필요할 때만 query를 실행하여 가져올 수 있음
- 데이터 접근 방식 비교
# 기존 relationship()
user = session.get(User, 1)
print(user.posts) # 리스트 반환 (즉시 로딩됨)
# DynamicMapped 사용
user = session.get(User, 1)
print(user.posts) # Query 객체 반환
print(user.posts.filter(Post.title == "Hello").all()) # 필요한 데이터만 로드
5. DynamicMapped가 데이터를 실제로 가져오는 시점
- DynamicMapped 속성은 Query 객체를 반환하기 때문에, all(), first(), one(), one_or_none(), count(), exists() 등을 호출해야 실제 데이터를 가져온다.
user = session.get(User, 1)
# posts는 Query 객체 상태
print(user.posts) # <sqlalchemy.orm.dynamic.AppenderQuery object at ...>
# 여기서 실제 SQL 실행 (데이터를 로드하는 시점)
posts = user.posts.all()
print(posts) # 리스트 반환됨
- 이터레이션을 실행할 때 (for 루프)
- DynamicMapped 속성은 Query 객체이므로, for 루프에서 순회할 때도 실제 SQL이 실행된다.
for post in user.posts:
print(post.title) # 여기서 실제 SQL 실행
- 정리
상황 | 쿼리 실행 여부 |
user.posts 접근 | ❌ (Query 객체 반환) |
user.posts.all() | ✅ (쿼리 실행) |
for post in user.posts: | ✅ (쿼리 실행) |
user.posts.filter(...).all() | ✅ (쿼리 실행) |
user.posts.count() | ✅ (COUNT(*) 실행) |
session.query(user.posts.exists()).scalar() | ✅ (EXISTS 실행) |
3. Mapped[T] vs DynamicMapped[T] 비교
Mapped[T] | DynamicMapped[T] | |
주 용도 | 일반적인 컬럼 및 관계 속성 정의 | 관계 속성을 동적 로딩할 때 사용 |
데이터 로딩 방식 | 즉시 로딩 (lazy="select" 기본값) | 쿼리를 실행해야 데이터 로드 (lazy="dynamic") |
리스트 형태 지원 | Mapped[list[T]] 사용 가능 | DynamicMapped[T]는 Query 객체를 반환 |
필터링 가능 여부 | user.posts를 호출하면 즉시 리스트 반환 | user.posts.filter(...).all()처럼 쿼리 필터링 가능 |
대량 데이터 처리 | 대량 데이터를 로드할 경우 성능 문제 발생 가능 | 필요한 데이터만 로드하여 성능 최적화 |
4. 정리
- Mapped[T]는 일반적인 컬럼 및 관계 속성을 정의할 때 사용
- DynamicMapped[T]는 lazy-loading이 필요할 때 사용하며, Query 객체를 반환하여 필터링 가능
- 대량의 관계 데이터를 처리할 때는 DynamicMapped를 사용하여 메모리 사용량을 줄이고 필요할 때만 쿼리를 실행하는 것이 유리
1) 언제 Mapped vs DynamicMapped를 사용할까?
- 대부분의 경우 Mapped[list[T]]을 사용하면 충분
- DynamicMapped는 대량 데이터가 포함된 1:N, N:M 관계에서 사용하면 성능을 최적화할 수 있음
- 예: User가 수천 개의 Post를 가질 경우 Mapped[list[Post]]는 한 번에 모든 데이터를 로딩하므로 비효율적
- DynamicMapped["Post"]를 사용하면 user.posts.filter(...)처럼 필요한 데이터만 로드 가능
5. 1:1 관계 (One-to-One), 1:N 관계 (One-to-Many), N:M 관계 (Many-to-Many) + 중간 테이블 설정
1) 1:1 관계 (One-to-One)
1. 특징
- 한 개의 엔티티가 오직 하나의 엔티티와 연결됨
- relationship()에서 uselist=False 설정
- Foreign Key를 Unique로 설정해야 함
2. 설정 예시
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey, UniqueConstraint
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
profile: Mapped["UserProfile"] = relationship("UserProfile", back_populates="user", uselist=False)
class UserProfile(Base):
__tablename__ = "user_profile"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), unique=True) # 1:1 관계를 위해 unique=True 설정
bio: Mapped[str] = mapped_column(nullable=True)
user: Mapped["User"] = relationship("User", back_populates="profile")
3. 1:1 관계 설정 핵심
- ForeignKey("user.id")에 unique=True를 설정하여 한 개의 UserProfile만 특정 User와 연결되도록 보장
- relationship()에서 uselist=False를 설정하여 1:1 관계를 명확하게 표현
- back_populates를 사용하여 양방향 참조 가능
2) 1:N 관계 (One-to-Many)
1. 특징
- 한 개의 엔티티가 여러 개의 엔티티를 가질 수 있음
- 부모 테이블은 1개, 자식 테이블은 여러 개의 참조 가능
- Mapped[list[T]] 또는 DynamicMapped[T] 사용 가능
2. 설정 예시
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
posts: Mapped[list["Post"]] = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(nullable=False)
content: Mapped[str] = mapped_column(nullable=False)
author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
author: Mapped["User"] = relationship("User", back_populates="posts")
3. 1:N 관계 설정 핵심
- User.posts에 Mapped[list["Post"]]를 사용하여 여러 개의 Post를 포함할 수 있도록 설정
- Post 테이블의 author_id가 User의 id를 ForeignKey로 참조
- relationship()을 통해 back_populates="author"로 양방향 매핑 가능
4. DynamicMapped를 사용한 1:N 관계
- 만약 User가 수천 개의 Post를 가질 경우, Mapped[list["Post"]]를 사용하면 성능 문제가 발생할 수 있다.
- 이를 방지하려면 DynamicMapped["Post"]를 사용하여 데이터를 필요할 때만 로드하도록 설정할 수 있다.
- user.posts.all() 또는 user.posts.filter(...).all()을 실행해야 데이터가 로드된다.
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
posts: DynamicMapped["Post"] = relationship("Post", back_populates="author", lazy="dynamic")
3) N:M 관계 (Many-to-Many)
1. 특징
- 다대다 관계에서는 중간 테이블 (association table) 이 필요
- relationship()에서 secondary 매개변수를 사용하여 중간 테이블 지정
- 두 개의 테이블이 서로 여러 개의 데이터를 가질 수 있음
2. 중간 테이블을 사용한 N:M 관계 설정
- SQLAlchemy에서는 Association Table (연결 테이블) 을 사용하여 N:M 관계를 설정한다.
from sqlalchemy import Table, Column, Integer
# 중간 테이블 (Association Table)
user_group_association = Table(
"user_group_association", Base.metadata,
Column("user_id", ForeignKey("user.id"), primary_key=True),
Column("group_id", ForeignKey("group.id"), primary_key=True)
)
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
groups: Mapped[list["Group"]] = relationship("Group", secondary=user_group_association, back_populates="users")
class Group(Base):
__tablename__ = "group"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
users: Mapped[list["User"]] = relationship("User", secondary=user_group_association, back_populates="groups")
3. N:M 관계 설정 핵심
- user_group_association라는 중간 테이블을 생성하여 User와 Group을 연결
- relationship(..., secondary=user_group_association)을 사용하여 다대다 관계를 설정
- back_populates를 사용하여 양방향으로 접근 가능
4. DynamicMapped를 사용한 N:M 관계
- N:M 관계에서도 대량의 데이터를 관리할 때는 DynamicMapped를 사용하는 것이 좋다.
- user.groups.all() 또는 user.groups.filter(Group.name == "Admin").all()처럼 동적으로 데이터 로드 가능하다.
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
groups: DynamicMapped["Group"] = relationship("Group", secondary=user_group_association, back_populates="users", lazy="dynamic")
class Group(Base):
__tablename__ = "group"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False, unique=True)
users: DynamicMapped["User"] = relationship("User", secondary=user_group_association, back_populates="groups", lazy="dynamic")
6. uselist 옵션에 대한 설명 추가
- uselist는 SQLAlchemy의 relationship()에서 사용되는 옵션으로, 관계된 객체를 리스트로 반환할지 여부를 결정하는 역할을 한다.
- uselist=True (기본값)
- 다대일(1:N) 또는 다대다(N:M) 관계에서 기본적으로 리스트(list[T]) 형태로 데이터를 반환한다.
- 즉, relationship()을 사용할 때 기본적으로 uselist=True이며, 이는 여러 개의 객체를 포함할 수 있음을 의미한다.
- uselist=False
- 1:1(One-to-One) 관계에서 사용되며, relationship()이 단일 객체를 반환하도록 강제한다.
- ForeignKey 컬럼에 unique=True가 설정되어 있어야 한다.
1) uselist 옵션 사용 예제
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
profile: Mapped["UserProfile"] = relationship("UserProfile", back_populates="user", uselist=False) # 1:1 관계
class UserProfile(Base):
__tablename__ = "user_profile"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), unique=True) # 반드시 unique=True 설정
bio: Mapped[str] = mapped_column(nullable=True)
user: Mapped["User"] = relationship("User", back_populates="profile")
2) 1:1 관계에서 uselist=False가 필요한 이유
- User.profile이 단일 객체(UserProfile)로 반환됨
- Foreign Key에 unique=True를 설정하여 한 개의 User가 하나의 UserProfile만 가질 수 있도록 제한
- 만약 uselist=True라면 User.profile이 리스트(list[UserProfile])가 되어버림 (1:1 관계가 아닌 1:N처럼 동작)\
3) uselist=True를 사용한 1:N 관계 (기본값)
- User.posts는 여러 개의 Post를 가질 수 있기 때문에 리스트(list[Post])로 반환됨
class User(Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(nullable=False, unique=True)
posts: Mapped[list["Post"]] = relationship("Post", back_populates="author") # 기본적으로 uselist=True
class Post(Base):
__tablename__ = "post"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(nullable=False)
content: Mapped[str] = mapped_column(nullable=False)
author_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
author: Mapped["User"] = relationship("User", back_populates="posts")
4) 정리
- uselist는 관계의 유형을 명확하게 설정하는 중요한 옵션이며, 1:1 관계에서는 uselist=False를 반드시 설정해야 한다.
uselist 값 | 사용 예제 | 반환되는 데이터 타입 | 사용 이유 |
uselist=True (기본값) | 1:N, N:M 관계 (User.posts, User.groups) | list[T] (리스트) | 다수의 객체를 포함해야 할 때 |
uselist=False | 1:1 관계 (User.profile) | T (단일 객체) | 단일 객체만 반환해야 할 때 (Foreign Key에 unique=True 필요) |
'SQLAlchemy' 카테고리의 다른 글
__table_args__ 메소드 정리 (0) | 2025.02.13 |
---|---|
sqlalchemy의 declarative_base()를 fastapi에서 사용하는 이유 (0) | 2025.02.08 |
[SQLALchemy] 트랜잭션(Transaction) 그리고 Database Lock (0) | 2025.02.04 |
[SQLALchemy] 비동기 함수, 비동기 Database 그리고 Pool의 정리 (0) | 2025.02.01 |
[SQLALchemy] 세션(Session), 비동기 세션(Async Session) 그리고 scoped_session의 정리 (0) | 2025.02.01 |
댓글