버튼 수집상

[Python] 쿼리 읽기가 어려워서 ORM을 써봤다. 본문

TIL - Python

[Python] 쿼리 읽기가 어려워서 ORM을 써봤다.

cocokaribou 2023. 11. 19. 17:10

빠르게 보는 결론

쿼리를 일반 문자열로 적었더니 재사용도 어렵고 가독성도 떨어져서 ORM을 도입해봤다.

그랬더니 리스폰스 속도가 너무 느려졌다.

일단 잠정 중단하고 무엇을 어떻게 도입해봤는지 행적을 남긴다.ㅠㅠ

 

 

배경

리스트를 뿌리는 기능이 대부분인 BackOffice 사이트에 FastAPI로 API 백엔드를 만들었다.

그런데 내가 쿼리를 못 짜서인지, get 함수 바디가 엄청나게 길고 안 읽혔다.

예시

그냥 통짜 문자열이라서 신택스 하이라이팅이 안 되는 게 너무 불편했다.

문득 ORM이라는 것을 기억해내고 장단점을 찾아봤다.

복잡한 데이터 조작없이 select 쿼리가 대부분이라면 ORM을 써도 괜찮지 않을까 싶어서 chatGPT에 물어봤다.

Yes, considering your emphasis on selecting data with minimal modification, using an ORM could enhance code readability, simplify data retrieval, and make your dashboard/back office development more intuitive. Consider the trade-offs and whether the potential downsides of ORM are acceptable for your project.

 

물론 chatGPT가 하는 말이라고 딱히 근거가 있는 건 아니다.

그래도 어느정도 확신을 얻고 한 번 도입해봤다.

 

 

세팅

SQLAlchemy를 install

pip install sqlalchemy

 

FastAPI 제작자가 만들어서 호환성이 더 좋다는 SQLModel이라는 라이브러리도 있다.

SQLAchemy를 쓴 이유는 연식이 오래됐고 sql server 관련해서 자료가 더 많은 것 같아서다.

 

DB 연결

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from contextlib import contextmanager

user = "USER"
pw = "PW"
server = "SERVER_URL"
db_name = "Test"

engine = create_engine(f"mssql+pymssql://{user}:{pw}@{server}/{db_name}")
session= scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))

Base = declarative_base() # 여기에 정의

@contextmanager
def db():
    try:
        yield session
    finally:
        session.close()

 

데이터 모델

from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from database.sqlalchemy import Base


class User(Base):
    __tablename__ = "ud_users"

    ud_idx = Column(Integer, primary_key=True)
    ds_user_idx = Column(Integer) # different DB

    schedules = relationship('Schedule', back_populates='user')


class Schedule(Base):
    __tablename__ = "ud_schedule"

    schedule_idx = Column(Integer, primary_key=True)
    ud_idx = Column(Integer, ForeignKey('ud_users.ud_idx'))
    user = relationship('User', back_populates='schedules')
    regdate = Column(DateTime)
    schedule_name = Column(String)

    teacher_name = Column(String)
    academy_name = Column(String)
    curriculum_name = Column(String)

    @property
    def teacher_academy_subject(self):
        return f"{self.teacher_name}-{self.academy_name}-{self.curriculum_name}"

 

모델끼리의 관계를 정의하는 relationship이라는 메소드도 있다.

관계를 정의하는 것이기 때문에 one to one, one to many 관계가 명확한 테이블끼리 쓰면 좋겠다.

예시 데이터는 유저 한 명당 여러개의 일정을 관리할 수 있기 때문에

User 클래스에 schedules라는 변수를, Schedule 클래스에 user 라는 변수를 각각 정의했다.

 

API 메소드

from fastapi import APIRouter
# import 생략 ...

router = APIRouter()

# 일정을 전부 가져온다.
# TODO server-side pagination
@router.get("/schedule-list")
def get_schedule_list():
    result_list = []
    with db() as session:
        for schedule in session.query(Schedule).options(joinedload(Schedule.user)).all():
            user = schedule.user
            result = {
                "schedule": schedule.schedule_name,
                "location": get_location_by_user(schedule.ud_idx),
                "teacher_academy_subject": schedule.teacher_academy_subject,
                "user": user
            }
            # 다른 데이터베이스의 테이블
            with ds_db() as session1:
                ds_user: DSUser = session1.query(DSUser).get(user.ds_user_idx)
                result["user_name"] = ds_user.name

            result_list.append(result)

    return result_list

 

리팩토링 한 번

# generated by chatGPT 3.5

@router.get("/schedule-list")
def get_schedule_list():
    result_list = []
    with db() as session:
        schedules = session.query(Schedule).options(joinedload(Schedule.user)).all()
		
        # 다른 데이터베이스에 있는 정보를 싱글쿼리로 가져온다
        user_ids = [schedule.user.ds_user_idx for schedule in schedules]
        with ds_db() as session1:
            ds_users = session1.query(DSUser).filter(DSUser.user_idx.in_(user_ids)).all()
            ds_user_dict = {ds_user.user_idx: ds_user for ds_user in ds_users}

        for schedule in schedules:
            user = schedule.user
            ds_user = ds_user_dict.get(user.ds_user_idx)
            result = {
               "schedule": schedule.schedule_name,
                "location": get_location_by_user(schedule.ud_idx),
                "teacher_academy_subject": schedule.teacher_academy_subject,
                "user": user,
                "user_name": ds_user.name if ds_user else None  # ds_user가 없을 때
            }
            result_list.append(result)

    return result_list

 

아래처럼 고쳐도 62개밖에 안 되는 데이터를 가져오는데 3.5초나 걸린다..

피드백

어차피 파이썬 백엔드와 자바스크립트 프런트엔드가 나뉘어있는 프로젝트에서 이런 매핑은 굳이 없어도 될 것 같다.

 

피드백

인프런 등등 자바 Spring에서 JPA를 잘만 쓰고 있는데, 왜 파이썬에 ORM을 도입했을 땐 느린걸까??

 

 

728x90