[SQLALchemy] 1:1, 1:N, N:M 관계에서 테이블 정의와 relationship의 활용 방법
1. SQLAlchemy의 relationship 개념 및 사용법
- SQLAlchemy에서 relationship은 테이블 간의 관계를 정의하는 기능으로, ORM에서 객체 간의 연결을 쉽게 관리할 수 있도록 돕는다.
1) Foreign Key (외래 키)
- ForeignKey("target_table.column") :
- 다른 테이블의 column을 참조하는 속성을 정의한다.
- 관계의 방향을 결정하며, 이를 기반으로 relationship을 설정할 수 있다.
2) relationship()
- 테이블 간 관계를 객체 수준에서 다룰 수 있도록 설정 한다.
- 기본적으로 lazy-loading 방식으로 작동하며, back_populates를 통해 양방향 관계를 정의할 수 있다.
- 주요 옵션 :
- back_populates : 서로 연결된 두 개의 모델에서 관계를 명확히 정의할 때 사용한다.
- backref : 관계를 자동으로 역방향으로 생성한다. (명시적으로 back_populates 권장)
- uselist : 리스트 형식(True, 기본값) 또는 단일 객체(False)로 관계를 정의한다.
- cascade : 연관된 객체에 대해 삭제, 병합 등의 작업을 설정한다. (all, delete, delete-orphan 등)
- lazy : 데이터 로딩 방식 지정한다. (joined, subquery, select, dynamic 등)
- secondary: N:M 관계에서 중간 테이블을 명시한다.
3) Cascade (일관성 유지 옵션)
- 부모 테이블의 변경이 자식 테이블에 미치는 영향을 결정한다.
- 주요 옵션 :
- "save-update" (기본값): 부모 객체 변경 시, 자식 객체 자동 반영
- "delete": 부모 객체 삭제 시, 연결된 자식 객체 삭제
- "delete-orphan": 부모 없이 남은 객체 자동 삭제
- "all": 위 모든 옵션 포함
2. 1:1 (One-to-One) 관계
1) 설정 방법
- PK를 공유할 수도 있고, 별도 FK + Unique 제약을 걸 수도 있음
- relationship()에서 uselist=False 설정하여 단일 객체만 참조하도록 강제
- 외래 키(FK)에 unique=True를 추가해야 완벽한 1:1 관계가 보장됨
1. 예제
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
profile = relationship("UserProfile", back_populates="user", uselist=False, cascade="all, delete")
class UserProfile(Base):
__tablename__ = "user_profiles"
id = Column(Integer, ForeignKey("users.id"), primary_key=True) # PK = FK (완벽한 1:1 관계)
bio = Column(String)
user = relationship("User", back_populates="profile")
__table_args__ = (UniqueConstraint('id'),) # Unique 보장
2. uselist=False란?
- relationship()에서 uselist=False를 설정하면 단일 객체로만 관계를 유지한다.
- 리스트가 아닌 단일 객체로 반환되므로 profile[0] 같은 접근 방식이 필요 없다.
3. unique=True 필요 여부
- FK에 unique=True를 추가하면 완벽한 1:1 관계 보장한다.
- 만약 unique=True가 없다면 실제로 1:N 관계가 허용될 위험이 있다.
- PK = FK 방식이면 unique=True가 없어도 무관하다.
3. 1:N (One-to-Many) 관계
1) 설정 방법
- 부모 테이블이 Primary Key, 자식 테이블이 Foreign Key를 가진다.
- relationship에서 back_populates를 설정하여 양방향 연결을 한다.
- Cascade 옵션을 통해 삭제/수정 동작을 설정한다.
2) 예제
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
posts = relationship("Post", back_populates="author", cascade="all, delete-orphan") # 부모 삭제 시, 자식도 삭제됨
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
content = Column(String)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE")) # 부모 삭제 시, 자식 삭제
author = relationship("User", back_populates="posts")
3) Cascade 설정
- "all, delete-orphan": 부모 삭제 시, 자식도 자동 삭제된다.
- ondelete="CASCADE": DB 레벨에서 FK 제약 조건 적용한다.
4. N:M (Many-to-Many) 관계
1) 설정 방법
- 중간 테이블(Association Table)을 사용한다.
- relationship()에서 secondary 옵션을 사용한다.
2) 예제
from sqlalchemy import Table, ForeignKey
user_groups = Table(
"user_groups",
Base.metadata,
Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
Column("group_id", Integer, ForeignKey("groups.id", ondelete="CASCADE"), primary_key=True),
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
groups = relationship("Group", secondary=user_groups, back_populates="users")
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
users = relationship("User", secondary=user_groups, back_populates="groups")
3) Cascade 주의점
- N:M 관계는 cascade="all, delete-orphan"을 사용할 수 없다.
- ondelete="CASCADE"만 사용하여 직접 FK 삭제 연쇄를 처리한다.
5. 정리 (관계별 설정 요약)
관계 유형 | Foreign Key | uselist 설정 | unique=True 필요 여부 | Cascade 설정 |
1:01 | PK or FK (Unique 필요) | uselist=False | 필요 | "all, delete" |
1:N | FK 사용 | 기본값 (True) | 불필요 | "all, delete-orphan" |
N:M | 중간 테이블 사용 | secondary 사용 | 불필요 | ondelete="CASCADE" |
6. backref vs back_populates
- SQLAlchemy에서 양방향 관계를 설정하는 방법이다.
1) backref
- 한쪽 모델에서만 관계를 정의하면 반대쪽 모델에서 자동으로 역참조(back reference)를 생성한다.
- 더 간결한 코드로 양방향 관계를 설정할 수 있다.
- 관계를 정의할 때 명시적으로 반대쪽 모델에 속성을 정의할 필요가 없다.
- backref(name, options...) 형태로 사용되며, relationship() 안에서 직접 정의한다.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
class Profile(Base):
__tablename__ = "profiles"
id = Column(Integer, primary_key=True)
bio = Column(String)
user_id = Column(Integer, ForeignKey("users.id"), unique=True)
user = relationship("User", backref=backref("profile", uselist=False))
# 이제 자동으로 `User.profile` 속성이 생성됨
- 결과
- Profile.user → User 객체 참조
- User.profile → Profile 객체 참조 (자동 생성)
2) back_populates
- 명시적으로 양쪽에서 모두 설정해야 한다. (자동 생성 X)
- 한쪽에서 relationship을 선언할 때, 반대쪽에도 relationship을 명시적으로 선언해야 한다.
- 더 명확한 관계 설정이 필요할 때 사용한다.
- 관계가 양쪽에서 모두 정의되어야 하므로 코드 가독성이 높아진다.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
profile = relationship("Profile", back_populates="user")
class Profile(Base):
__tablename__ = "profiles"
id = Column(Integer, primary_key=True)
bio = Column(String)
user_id = Column(Integer, ForeignKey("users.id"), unique=True)
user = relationship("User", back_populates="profile")
- 결과
- Profile.user → User 객체 참조
- User.profile → Profile 객체 참조 (자동 생성 X, 명시적으로 설정 필요)
3) backref vs back_populates 비교
비교 항목 | backref | back_populates |
설정 방식 | 한쪽(relationship)에서만 정의하면 자동으로 반대쪽 생성 | 양쪽(relationship)에서 명시적으로 설정해야 함 |
가독성 | 간단하고 코드가 짧아짐 | 관계가 명확해짐 (어떤 관계인지 직접 확인 가능) |
유연성 | cascade, uselist 같은 옵션을 함께 사용 가능 | 커스텀 관계 설정이 용이 |
사용 추천 상황 | 빠르게 양방향 관계를 설정하고 싶을 때 | 관계를 명확하게 제어 |
7. relationship에서의 cascade
- 일반적으로는 필요가 없지만, ORM 기반의 자동 삭제를 위해 설정할 수 있다고 한다.
1) cascade의 주요 옵션
옵션 | 설명 |
save-update | 부모 객체가 add() 될 때, 자식 객체도 자동으로 추가됨 (기본값) |
merge | 부모 객체가 merge() 될 때, 자식 객체도 함께 병합됨 |
expunge | 부모 객체가 세션에서 제거되면, 자식 객체도 세션에서 제거됨 |
delete | 부모 객체가 삭제되면, 연결된 자식 객체도 삭제됨 |
delete-orphan | 부모 객체와 연결이 끊어진 자식 객체는 자동으로 삭제됨 |
all | 위의 모든 옵션을 포함 (기본적으로 save-update, merge, delete, refresh-expire, expunge 포함) |
2) cascade 설정 비교
옵션 | 부모 삭제 시 | 부모에서 제거 시 | 부모 추가 시 |
delete | 자식도 삭제됨 | 영향 없음 | 영향 없음 |
delete-orphan | 영향 없음 | 자식도 삭제됨 | 영향 없음 |
save-update | 영향 없음 | 영향 없음 | 자식도 추가됨 |
all | 자식도 삭제됨 | 영향 없음 | 자식도 추가됨 |
all, delete-orphan | 자식도 삭제됨 | 자식도 삭제됨 | 자식도 추가됨 |
3) all, delete-orphan 상세 설명
1. all
cascade="all"
- 부모와 연결이 끊어진(고아가 된) 자식 객체는 자동으로 삭제됨.
- 즉, 부모 객체에서 자식 객체를 제거하면 자식 객체가 데이터베이스에서도 삭제됨.
2. delete-orphan
cascade="delete-orphan"
- 부모와 연결이 끊어진(고아가 된) 자식 객체는 자동으로 삭제됨.
- 즉, 부모 객체에서 자식 객체를 제거하면 자식 객체가 데이터베이스에서도 삭제됨.
3. all, delete-orphan (조합)
cascade="all, delete-orphan"
- all: 부모와 자식 간의 모든 동작을 자동화 (save-update, merge, delete, expunge 포함)
- delete-orphan: 부모와 연결이 끊어진 자식 객체가 자동으로 삭제됨.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
class Profile(Base):
__tablename__ = "profiles"
id = Column(Integer, primary_key=True)
bio = Column(String)
user_id = Column(Integer, ForeignKey("users.id"), unique=True)
user = relationship("User", backref=backref("profile", uselist=False, cascade="all, delete-orphan"))
- 이 설정의 효과
- User가 삭제되면 Profile도 삭제됨 (delete)
- User.profile = None이 되면, 기존 Profile이 자동 삭제됨 (delete-orphan)
- session.add(user)를 하면 연결된 Profile도 자동 추가됨 (save-update)
8. ORM 관계 설정의 lazy 옵션 설명
1) lazy='select' (기본값)
1. 동작:
- 객체를 처음 조회할 때는 관련 객체를 로드하지 않고, 해당 관계 속성에 접근하는 순간 별도의 SELECT 문을 실행하여 데이터를 가져온다.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="select")
# 이 시점에서는 posts 데이터가 로드되지 않음
user = session.query(User).filter_by(id=1).first()
# 이 시점에서 posts를 로드하기 위한 추가 SELECT 쿼리가 실행됨
for post in user.posts:
print(post.title)
2. 특징:
- 가장 단순한 “지연 로딩” 방식이다.
- 이 옵션은 관련 데이터가 항상 필요하지 않을 때 유용하며, 필요할 때만 데이터를 가져오므로 메모리 사용을 최적화할 수 있습니다. 그러나 필요할 때마다 추가 SELECT 문이 실행되므로, N+1 쿼리 문제를 일으킬 수 있습니다.
2) lazy='joined'
1. 동작:
- 부모 객체를 조회할 때 SQL JOIN을 사용하여 한 번의 쿼리로 연관된 자식 객체들을 함께 가져온다.
2. 사용 예시:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="joined")
# 이 쿼리는 User와 Post 테이블을 JOIN하여 한 번에 데이터를 가져옴
user = session.query(User).filter_by(id=1).first()
# 추가 쿼리 없이 바로 posts 데이터에 접근 가능
for post in user.posts:
print(post.title)
3. 특징:
- 쿼리 횟수를 줄여 한 번에 모두 로드할 수 있으므로 N+1 문제를 회피한다.
- JOIN을 사용하므로 결과에 부모 데이터가 중복되어 나타날 수 있고, 데이터 양이 많아질 수 있다.
- 이 옵션은 부모 객체를 조회할 때 항상 관련 객체가 필요한 경우에 유용합니다. 단일 쿼리로 모든 데이터를 가져오기 때문에 N+1 쿼리 문제를 방지할 수 있습니다. 그러나 불필요한 데이터까지 항상 로드하므로 메모리 사용량이 증가할 수 있습니다.
3) lazy='subquery'
1. 동작:
- 부모 객체를 먼저 조회한 후, 부모의 기본키 목록을 서브쿼리로 만들어 이를 이용해 별도의 쿼리에서 연관된 자식들을 로드한다.
2. 사용 예시
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="subquery")
# 첫 번째 쿼리로 User 객체만 로드
users = session.query(User).all()
# users 리스트의 첫 번째 요소의 posts에 접근할 때,
# 모든 users의 posts를 서브쿼리로 한 번에 로드
for post in users[0].posts:
print(post.title)
# 이후 다른 User 객체의 posts에 접근할 때는 추가 쿼리가 발생하지 않음
for post in users[1].posts:
print(post.title)
3. 특징:
- JOIN 방식보다 복잡하지만, JOIN으로 인해 발생할 수 있는 데이터 중복 문제를 피할 수 있다.
- 서브쿼리 구조로 인해 쿼리의 복잡도가 다소 증가할 수 있다.
- 이 옵션은 여러 부모 객체를 로드한 후 각 부모의 관련 객체에 접근해야 할 때 유용합니다. 두 번의 쿼리만으로 모든 데이터를 로드하므로 효율적입니다.
4) lazy='selectin'
1. 동작:
- 부모 객체들을 조회한 후, 부모의 기본키들을 IN 절에 넣은 별도의 SELECT 문을 실행하여 관련 자식 객체들을 로드한다.
2. 사용 예시:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="selectin")
# 첫 번째 쿼리로 User 객체만 로드
users = session.query(User).all()
# 두 번째 쿼리로 모든 posts를 IN 절을 사용하여 한 번에 로드
# SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)
for post in users[0].posts:
print(post.title)
3. 특징:
- JOIN 없이도 효율적으로 여러 부모 객체에 대한 연관 데이터를 가져온다.
- 대량의 부모 객체가 있을 경우 IN 절의 길이에 따라 한 번에 로드할 수 있는 개수에 제한이 있을 수 있다.
- selectin은 subquery와 유사하지만 더 효율적인 IN 절을 사용한다. 많은 관련 객체를 로드할 때 특히 유용하며, 대부분의 데이터베이스에서 더 효율적으로 작동한다.
5) lazy='raise'
- 관련 객체에 접근하려고 할 때 예외를 발생시킵니다. 이 옵션은 특정 방식으로만 관련 객체를 로드하도록 강제한다.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="raise")
user = session.query(User).filter_by(id=1).first()
try:
# 이 코드는 예외를 발생시킴
print(len(user.posts))
except Exception as e:
print(f"Error: {e}")
# 정상적으로 로드하려면 명시적인 join이나 다른 로딩 전략을 사용해야 함
user_with_posts = session.query(User).options(
joinedload(User.posts)
).filter_by(id=1).first()
- 이 옵션은 개발자가 관련 객체의 로딩을 명시적으로 처리하도록 강제하여 실수로 인한 성능 문제를 방지하는 데 유용하다.
6) lazy='raise_on_sql'
- SQL 쿼리를 생성하는 경우에만 예외를 발생시킵니다. 이미 로드된 객체에 대한 접근은 허용된다.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="raise_on_sql")
# posts가 이미 로드된 경우 (예: joinedload를 사용한 경우)
user_with_posts = session.query(User).options(
joinedload(User.posts)
).filter_by(id=1).first()
# 이미 로드되었으므로 문제 없이 접근 가능
for post in user_with_posts.posts:
print(post.title)
# 로드되지 않은 user의 posts에 접근하면 예외 발생
user_without_posts = session.query(User).filter_by(id=2).first()
try:
print(len(user_without_posts.posts)) # 예외 발생
except Exception as e:
print(f"Error: {e}")
- 이 옵션은 raise보다 유연하며, 이미 로드된 관련 객체에 대한 접근은 허용하면서도 암시적 로딩은 방지한다.
7) lazy='noload'
1. 동작:
- 해당 관계를 아예 로드하지 않는다. (즉, 속성에 접근해도 SELECT 문이 실행되지 않으며, 빈 값 또는 None이 반환된다.)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="noload")
user = session.query(User).filter_by(id=1).first()
# posts가 로드되지 않았으므로 항상 빈 컬렉션 반환
print(len(user.posts)) # 0 출력
# 명시적으로 로드하려면 다른 쿼리나 로딩 전략 필요
user_with_posts = session.query(User).options(
joinedload(User.posts)
).filter_by(id=1).first()
2. 특징:
- 관계를 불필요하게 로드하지 않아 성능 최적화가 필요한 경우 사용한다.
- 관계 데이터가 전혀 필요 없거나, 다른 방식(예: 명시적 쿼리)으로 로드할 때 유용하다.
8) lazy='dynamic'
1. 동작:
- 컬렉션 관계(주로 one-to-many)에서 접근 시 실제 컬렉션 대신 쿼리 객체를 반환한다. 이를 통해 추가 필터링, 정렬 등 동적으로 쿼리를 조작할 수 있다.
2. 사용 예시:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="dynamic")
user = session.query(User).filter_by(id=1).first()
# posts는 쿼리 객체이므로 추가 필터링이나 정렬 가능
recent_posts = user.posts.filter(Post.created_at > yesterday).order_by(Post.created_at.desc()).all()
# 또는 특정 조건의 posts만 카운트
active_post_count = user.posts.filter(Post.is_active == True).count()
3. 특징:
- 컬렉션이 매우 크거나, 조건에 따라 부분집합만 가져와야 할 때 유리하다.
- 반환되는 값은 일반 리스트가 아닌 Query 객체이므로, 사용 시 .all(), .filter() 등 추가 작업이 필요하다.
9) 권장 사항 및 성능 고려사항
1. 기본값 select:
- 간단한 애플리케이션이나 관련 객체가 항상 필요하지 않은 경우 적합하지만, N+1 쿼리 문제에 주의해야 합니다.
2. joined:
- 부모 객체와 관련 객체가 항상 함께 필요한 경우 좋은 선택이다. 그러나 큰 데이터셋에서는 메모리 사용량이 증가할 수 있다.
3. selectin:
- 여러 부모 객체의 관련 객체가 필요한 경우 가장 효율적인 옵션이다. 대부분의 경우 subquery보다 성능이 좋다.
4. dynamic:
- 관련 객체에 대한 추가 필터링이 필요한 경우 최적의 선택이다.
5. raise와 raise_on_sql:
- 개발 단계에서 명시적인 로딩을 강제하여 성능 문제를 미리 방지하는 데 유용하다.
9. 1:N 및 N:M 관계에서의 최적 lazy 옵션 추천
1) 1:N (일대다) 관계에서의 추천 옵션\
1. 부모 쪽 설정 (One 쪽)
- 가장 추천하는 옵션: selectin
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="selectin")
- 이유:
- 여러 User 객체를 로드할 때 각 User의 posts를 효율적으로 함께 로드한다.
- IN 절을 사용하여 한 번의 추가 쿼리로 모든 관련 객체를 로드하므로 N+1 쿼리 문제를 방지한다.
- joined보다 데이터 중복이 적고 메모리 효율성이 높다.
- 특히 목록 페이지나 여러 사용자와 그들의 게시물을 표시하는 경우에 적합하다.
2. 자식 쪽 설정 (Many 쪽)
- 가장 추천하는 옵션: select (기본값)
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String)
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship("User", back_populates="posts", lazy="select")
- 이유:
- 게시물에서 작성자 정보가 항상 필요하지 않은 경우, 필요할 때만 로드하는 것이 효율적이다.
- N 쪽에서 1 쪽으로의 참조는 각 객체당 하나의 객체만 로드하므로 N+1 쿼리 문제가 심각하지 않는다.
- 게시물 목록에서 작성자 정보가 필요한 경우, 명시적으로 joinedload를 사용할 수 있다.
- 특수 상황에서의 추천 옵션
- 많은 데이터를 필터링해야 하는 경우: dynamic
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("Post", back_populates="user", lazy="dynamic")
- 이유:
- 사용자가 수천 개의 게시물을 가질 수 있고, 특정 조건(예: 최근 게시물, 특정 카테고리)으로 필터링이 필요한 경우에 유용하다.
- 데이터베이스 수준에서 필터링을 수행하므로 메모리 사용량을 크게 줄일 수 있다.
- 예: user.posts.filter(Post.created_at > last_week).limit(10).all()
2) N:M (다대다) 관계에서의 추천 옵션
1. 양쪽 모두에 대한 기본 추천 옵션: selectin
class Student(Base):
__tablename__ = 'students'
id = Column(Integer, primary_key=True)
name = Column(String)
courses = relationship("Course", secondary="student_courses", back_populates="students", lazy="selectin")
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
name = Column(String)
students = relationship("Student", secondary="student_courses", back_populates="courses", lazy="selectin")
- 이유:
- 다대다 관계에서는 보통 양쪽 모두의 관련 객체가 필요한 경우가 많다.
- IN 절을 사용하여 한 번의 추가 쿼리로 모든 관련 객체를 로드하므로 효율적이다.
- 예: 학생 목록과 각 학생이 수강하는 과목 목록을 표시하거나, 과목 목록과 각 과목을 수강하는 학생 목록을 표시할 때 적합하다.
2. 많은 관련 객체가 있는 경우: dynamic
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
name = Column(String)
students = relationship("Student", secondary="student_courses", back_populates="courses", lazy="dynamic")
- 이유:
- 인기 있는 과목에 수백 명의 학생이 등록할 수 있는 경우, 모든 학생을 메모리에 로드하는 것은 비효율적이다.
- 특정 조건(예: 특정 학년, 특정 학과)으로 필터링이 필요한 경우에 유용하다.
- 예: popular_course.students.filter(Student.year == 2023).all()
3) 사용 패턴에 따른 추천 옵션
1. 데이터 조회 중심 애플리케이션
- 읽기 작업이 많고 데이터 변경이 적은 경우: joined 또는 selectin
posts = relationship("Post", lazy="joined") # 항상 함께 로드
- 페이지 로드 시 모든 관련 데이터가 필요한 경우 효율적이다.
- 데이터 변경이 적으므로 과도한 메모리 사용의 단점이 크지 않다.
2. 대량 데이터 처리 애플리케이션
- 대량의 데이터를 처리하는 경우: select 또는 dynamic
posts = relationship("Post", lazy="select") # 필요할 때만 로드
- 메모리 사용량을 최소화하기 위해 필요한 데이터만 로드한다.
- 배치 처리나 데이터 마이그레이션 작업에 적합하다.
3. API 서버
- API 응답에 항상 관련 데이터가 포함되는 경우: selectin
posts = relationship("Post", lazy="selectin")
- API 응답에 사용자와 그들의 게시물을 함께 반환하는 경우 효율적이다.
- N+1 쿼리 문제를 방지하면서도 필요한 데이터만 로드한다.
4. API 응답에 선택적으로 관련 데이터가 포함되는 경우: noload + 명시적 로딩
posts = relationship("Post", lazy="noload")
- 기본적으로는 관련 데이터를 로드하지 않고, 클라이언트 요청에 따라 명시적으로 로드한다.
- 예: "?include=posts" 쿼리 파라미터가 있을 때만 게시물 데이터를 로드한다.
4) 결론
- 1:N 및 N:M 관계에서 가장 균형 잡힌 접근 방식은 다음과 같다.
1. 1:N 관계:
- 부모(One) 쪽: selectin - 효율적인 로딩과 N+1 쿼리 방지
- 자식(Many) 쪽: select - 필요할 때만 로드
2. N:M 관계:
- 일반적인 경우: 양쪽 모두 selectin - 효율적인 로딩과 N+1 쿼리 방지
- 많은 관련 객체가 있는 경우: dynamic - 필터링과 페이징을 통한 효율적인 데이터 접근
3. 특수 상황:
- 항상 함께 로드해야 하는 경우: joined
- 관련 객체가 많고 필터링이 필요한 경우: dynamic
- 명시적 로딩을 강제하고 싶은 경우: raise 또는 raise_on_sql
- 관련 객체가 불필요한 경우: noload
'SQLAlchemy' 카테고리의 다른 글
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 |
[SQLALchemy] Alembic을 이용한 마이그레이션 관리 방법 (0) | 2025.01.31 |
댓글