BackEnd King KY

TIL44 - SQLAlchemy에서 Django select_related 구현 본문

FastAPI

TIL44 - SQLAlchemy에서 Django select_related 구현

Django King, Lee 2023. 2. 12. 16:29
728x90

SQLAlchemy에서 Django select_related 구현

Django의 select_related는 필요한 데이터를 SQL상에서 Join을 통해 가져오는 Eager Loading이란 특징을 가지고 있는 ORM 문법입니다.


FastAPI를 하며 SQLAlchemy를 사용하고 있는데, Django의 이 기능을 SQLAlchemy에서 어떻게 구현하고 있는지 찾아보게 되었습니다.


출처는 SQLAlchemy 공식문서입니다.


티스토리로 보기 불편하실 경우, Github에서 보실 수 있습니다.


joinedload

SQLAlchemy 공식문서에선 아래와 같이 나와 있습니다.

In the Object Relational Tutorial, we introduced the concept of Eager Loading. 
We used an option in conjunction with the Query object in order to indicate that a relationship should be loaded at the same time as the parent, within a single SQL query. This option, known as joinedload(), 
connects a JOIN (by default a LEFT OUTER join) to the statement and populates the scalar/collection 
from the same result set as that of the parent:

요약하자면, 부모-자식 관계의 Query(기준 테이블과 참조 테이블)를 로드 해야 하는데, 이 때 사용하는 것이 joinedload 이며 기본 조인으로 LEFT OUTER JOIN을 통해 연결한다고 합니다.

그래서 간단한 예시와 쿼리를 추출하여 결과까지 보도록 하겠습니다.


결과 확인

두 개의 모델을 설계 해보겠습니다.

가수(Singer) - 소속사(Company)이며, 소속사를 가수가 참조하게 됩니다.

모델 정의

class Company(Base):
    __tablename__ = "companies"

    id = Column(Integer, primary_key=True)
    created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.now())
    name = Column(String(length=100), nullable=False)

class Singer(Base):
    __tablename__ = "singers"

    id = Column(Integer, primary_key=True, autoincrement=True)
    created_at = Column(DateTime(True), nullable=False, default=datetime.now())
    name = Column(String(length=100))
    company_id = Column(
        Integer, ForeignKey(column="companies.id"), index=True, primary_key=True
    )

    company = relationship("Company", backref="singer", foreign_keys=company_id)

API정의

from fastapi import APIRouter, Depends
from apps.configs.databases import get_db
from sqlalchemy.orm import Session, joinedload
from apps.singers.models.singers import Singer


def get_singers(db: Session):
    singers = db.query(Singer).options(joinedload(Singer.company))
    print(singers.statement)
    singers = singers.all()
    return singers


@app.get(path="/singers")
def get(db: Session = Depends(get_db)):
    singer_list = get_singers(db=db)
    result = {"result": singer_list}
    return result

SQL Query

SELECT singers.id, singers.created_at, singers.name, singers.company_id, companies_1.id AS id_1, companies_1.created_at AS created_at_1, companies_1.name AS name_1 
FROM singers LEFT OUTER JOIN companies AS companies_1 ON companies_1.id = singers.company_id

결과를 보면, LEFT OUTER JOIN을 통해 결과를 가져오고, 그 결과에 조인된 테이블의 전체 결과를 가져온 걸 확인할 수 있습니다.

그러면 3개 이상의 모델을 조인해서 가져오려면 어떻게 해야 될까요?


간단하게 Album 이라는 모델을 만들어 Company - Singer - Album 으로 참조 관계를 만들어보겠습니다.

class Album(Base):
    __tablename__ = "albums"

    id = Column(Integer, primary_key=True)
    name = Column(String(length=200), nullable=False)
    singer_id = Column(ForeignKey("singers.id"), index=True)

    singer = relationship("Singer", backref="album", foreign_keys=singer_id)

def get_albums(db: Session):
    albums = db.query(Album).options(joinedload("singer"), joinedload("singer.company"))
    print(albums.statement)
    albums = albums.all()
    return albums


@app.get("/albums")
def get_album_list(db: Session = Depends(get_db)):
    albums = get_albums(db=db)
    return {"result": albums}

이렇게 API를 호출하게 되면

SELECT 
albums.id, albums.name, albums.singer_id, 
singers_1.id AS id_2, singers_1.created_at AS created_at_1, singers_1.name AS name_2, singers_1.company_id 
companies_1.id AS id_1, companies_1.created_at, companies_1.name AS name_1, 
FROM albums 
LEFT OUTER JOIN singers AS singers_1 ON singers_1.id = albums.singer_id 
LEFT OUTER JOIN companies AS companies_1 ON companies_1.id = singers_1.company_id

이렇게 다중조인을 해서 나오게 됩니다. 그리고 joinledload에서 변수를 참조할땐 relatioship에서 선언한 변수로 접근해야 합니다.

'FastAPI' 카테고리의 다른 글

TIL41 - psycopg2-binary 2.8.6 버전 설치 시 발생했던 에러  (0) 2023.01.07