SQLAlchemy

[SQLALchemy] 1:1, 1:N, N:M 관계에서 테이블 정의와 relationship의 활용 방법

bluebamus 2025. 2. 1.

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

댓글