POST

All
Product
Team
Tech
DocVLM: Make Your VLM an Efficient Reader
  • 최윤진
  1. Tech
Python 3.10 신규 문법 : Parenthesized context managers와 PEG Parser
  • S
    seunghoChoe
  1. Tech
UReader: Universal OCR-free Visually-situated Language Understanding with Multimodal Large Language Model
  • 최윤진
  1. Tech
[팀 소개편] KPMG Lighthouse는 어떤 팀인가요?
  • L
    Lighthouse
  1. Team
[챕터 소개편] Backend Chapter를 소개합니다
  • L
    Lighthouse
  1. Team
[챕터 소개편]Frontend Chapter를 소개합니다
  • L
    Lighthouse
  1. Team
[챕터 소개편] AI Chapter를 소개합니다
  • L
    Lighthouse
  1. Team

SQLAlchemy 알아보기

Created by
  • D
    donggyun_woo
Created at
Category
  1. Tech

시작 하면서

최근 KRM ver2.0 프로젝트를 시작하면서 웹서버를 구현하기 위해 SQLAlchemy를 사용하면서 모델링을 했습니다. 작업을 하면서 SQLAlchemy에 대해 공부한 내용을 공유해 보겠습니다.

SQLAlchemy?

SQLAlchemy는 파이썬을 위한 SQL 툴킷 및 Object-Relational Mapping(ORM) 라이브러리로, 데이터베이스와의 상호작용을 보다 쉽고 직관적으로 할 수 있게 도와줍니다.
SQLAlchemy는 데이터베이스 연결 관리, 트랜잭션 관리, 스키마 정의 및 관리 등 다양한 기능을 제공합니다.

SQLAlchemy 구조

크게 Core와 ORM으로 나뉘어져 있습니다.

Core (SQL Expression Language):

SQLAlchemy의 Core는 SQL 표현 언어를 사용하여 데이터베이스 쿼리를 생성하고 실행하는 데 중점을 둡니다.
SQLAlchemy Core는 SQL을 직접 작성하고 데이터베이스와 상호작용할 수 있는 저수준 인터페이스를 제공합니다.

Engine

데이터베이스와의 연결을 관리하는 핵심 구성 요소입니다.
Engine 객체는 데이터베이스와의 상호작용을 위한 연결 풀(Connection Pool) 및 데이터베이스 드라이버를 포함하여 SQLAlchemy의 다른 부분과의 통신을 조정
Engine의 주요 기능
데이터베이스 연결 설정:
Engine은 데이터베이스 URL을 통해 데이터베이스 연결 정보를 설정합니다.
연결 풀 관리:
Engine은 연결 풀을 관리하여 데이터베이스 연결의 효율성을 높이고 성능을 최적화합니다.
연결 풀은 데이터베이스 연결을 재사용하여 연결 생성 및 소멸에 따른 오버헤드를 줄입니다.
트랜잭션 관리:
Engine은 트랜잭션을 관리할 수 있는 기능을 제공합니다. 트랜잭션은 데이터베이스 작업을 원자적으로 처리하는 단위입니다.
트랜잭션은 begin 메서드를 사용하여 시작하고, commit 또는 rollback 메서드를 통해 종료할 수 있습니다.
SQL 실행:
Engine은 SQL 문을 실행하고 결과를 반환하는 메서드를 제공합니다.
connect 메서드를 사용하여 데이터베이스 연결을 수동으로 관리할 수 있습니다.
통합된 데이터베이스 드라이버 지원:
Engine은 다양한 데이터베이스 드라이버와 통합되어, 여러 종류의 데이터베이스(MySQL, PostgreSQL, SQLite 등)를 지원합니다.
데이터베이스 URL을 통해 사용하려는 데이터베이스 드라이버를 지정할 수 있습니다.

연결 풀(Connection Pool)

데이터베이스 연결의 효율적인 관리와 성능 최적화를 위한 메커니즘
연결을 미리 생성하고 재사용함으로써, 연결 생성과 소멸에 따른 오버헤드를 줄이고 애플리케이션의 성능을 향상
pool_size: 연결 풀의 최대 연결 수(기본값은 5).
max_overflow: 풀의 연결 수가 초과되었을 때 추가로 생성할 수 있는 연결 수(기본값은 10).
pool_timeout: 풀에서 연결을 가져오기 위해 대기하는 최대 시간(기본값은 30초).
pool_recycle: 지정된 시간이 지나면 연결을 재활용하여 새로 고침(기본값은 -1, 사용 안 함).
Connection Pool 종류
1.
QueuePool:
기본 연결 풀로, 고정된 크기의 연결 풀을 사용하며 초과 요청에 대해 큐잉을 지원합니다.
2.
SingletonThreadPool:
SQLite와 같은 단일 스레드 데이터베이스에 적합하며, 각 스레드가 고유의 연결을 가지도록 합니다.
3.
NullPool:
연결 풀을 사용하지 않으며, 각 연결 요청 시 새 연결을 생성합니다. 연결 풀링이 필요하지 않은 경우에 사용합니다.

Dialect

Dialect는 데이터베이스와의 구체적인 통신을 담당하는 구성 요소입니다. 각 데이터베이스 시스템(MySQL, PostgreSQL, SQLite 등)은 고유한 SQL 문법과 기능을 가지고 있으며,
Dialect는 이러한 특성을 추상화하여 SQLAlchemy가 다양한 데이터베이스를 지원할 수 있도록 함.
# Engine ## **create_engine** 함수는 주어진 데이터베이스 URL을 기반으로 **Engine** 객체를 생성합니다. engine = create_engine( # Dialect 'postgresql+psycopg2://user:password@localhost/mydatabase', # Connection Pool pool_size=10, max_overflow=20, pool_timeout=30, pool_recycle=3600 )

ORM (Object-Relational Mapping):

SQLAlchemy ORM은 데이터베이스 테이블을 파이썬 클래스에 매핑하여 객체 지향 프로그래밍의 편리함을 제공하는 고수준 인터페이스입니다.
ORM을 사용하면 데이터베이스 레코드를 파이썬 객체로 표현할 수 있어 더 직관적이고 생산성 높은 코드 작성이 가능합니다.

ORM을 활용한 DB 구현하기

코드를 보면서 관련된 내용들을 설명 드리겠습니다.
SQLAlchemy 2.0 기준으로 구현
1.
데이터 베이스 연결
from sqlalchemy import create_engine from sqlalchemy.orm import Session engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/") # create session and add objects with Session(engine) as session: session.add(some_object) session.add(some_other_object) session.commit() # OR from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/") # a sessionmaker(), also in the same scope as the engine Session = sessionmaker(engine) # we can now construct a Session() without needing to pass the # engine each time with Session() as session: session.add(some_object) session.add(some_other_object) session.commit()
세션: 세션(Session)은 데이터베이스와의 모든 상호작용을 캡슐화하며, 데이터베이스 작업을 관리하는 중요한 역할.
트랜잭션 관리:
세션은 데이터베이스 트랜잭션을 관리합니다. 데이터베이스 작업은 세션을 통해 시작되고, commit() 또는 rollback()을 호출하여 트랜잭션을 명시적으로 완료하거나 취소할 수 있습니다.
객체 상태 관리:
세션은 데이터베이스에서 조회된 객체들의 상태를 추적합니다. 객체가 세션에 추가되면 세션은 그 객체의 변경 사항을 추적하고, 필요할 때 데이터베이스에 반영합니다.
연결 관리:
세션은 엔진(Engine)과 연결(Connection)을 관리하며, 데이터베이스 연결 풀(Connection Pool)을 통해 연결을 효율적으로 재사용합니다. 세션이 종료되면 연결이 풀로 반환됩니다.
쿼리 실행:
세션은 데이터베이스에 대해 쿼리를 실행하고 결과를 반환합니다. 이는 세션을 통해 SQL 쿼리 또는 ORM 쿼리를 실행함으로써 이루어집니다.
Session() VS sessionmaker()
구성 편의성:
Session() 직접 사용: 세션을 생성할 때마다 엔진을 명시적으로 바인딩해야 합니다.
sessionmaker 사용: 세션 팩토리에서 한 번만 구성을 설정하면, 팩토리를 통해 생성된 모든 세션은 동일한 구성을 따릅니다.
일관성:
Session() 직접 사용: 세션 생성 시 구성 설정을 반복해야 하므로 일관성이 떨어질 있습니다.
sessionmaker 사용: 모든 세션이 일관된 구성을 가지므로 코드가 더 일관성 있고 관리하기 쉽습니다.
유연성:
Session() 직접 사용: 특정 상황에서 더 세밀한 제어가 필요할 때 유용할 수 있습니다.
sessionmaker 사용: 여러 세션을 쉽게 생성하고 관리할 수 있어, 대부분의 경우 더 유연하고 효율적입니다.
2.
Table 매핑
from datetime import datetime from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy import func from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user" id = mapped_column(Integer, primary_key=True) name: Mapped[str] fullname: Mapped[Optional[str]] nickname: Mapped[Optional[str]] = mapped_column(String(64)) create_date: Mapped[datetime] = mapped_column(insert_default=func.now()) addresses: Mapped[List["Address"]] = relationship(back_populates="user") class Address(Base): __tablename__ = "address" id = mapped_column(Integer, primary_key=True) user_id = mapped_column(ForeignKey("user.id")) email_address: Mapped[str] user: Mapped["User"] = relationship(back_populates="addresses")
DeclarativeBase
데이터베이스 테이블과 파이썬 클래스를 매핑하기 위해 사용되는 기본 클래스입니다.
DeclarativeBase를 통해 테이블 정의와 클래스 정의를 동시에 할 수 있어 코드의 가독성과 유지보수성을 높입니다.
# 사용하지 않고 매핑하는 경우 user_table = Table( "user", Base.metadata, Column("id", Integer, primary_key=True), Column("name", String(50)), Column("fullname", String()), Column("nickname", String(30)), )
nullable, Optional
class SomeClass(Base): # ... # will be String() NULL, but type checker will not expect # the attribute to be None data: Mapped[str] = mapped_column(nullable=True)
파이썬 코드: None이 아닌 값을 사용하지만,
데이터베이스 스키마: 널 값을 허용해야 할 때 mapped_column.nullable=True로 설정합니다
relationship()
위의 코드를 보면 back_populates 를 사용해서 명시적으로 user, addresses 를 정의 했습니다. 이외에도 backref 도 같이 알아 보겠습니다.
class User(Base): __tablename__ = "user" id = mapped_column(Integer, primary_key=True) name: Mapped[str] fullname: Mapped[Optional[str]] nickname: Mapped[Optional[str]] = mapped_column(String(64)) create_date: Mapped[datetime] = mapped_column(insert_default=func.now()) addresses: Mapped[List["Address"]] = relationship(backref="user") class Address(Base): __tablename__ = "address" id = mapped_column(Integer, primary_key=True) user_id = mapped_column(ForeignKey("user.id")) email_address: Mapped[str] # user 를 명시하지 않고 User테이블에서만 정의가 가능한 장점이 있음.
특성
backref
back_populates
설정 방식
한쪽에서만 정의
양쪽에서 명시적으로 정의
코드 간결성
더 간결함
더 명시적
자동 생성
자동으로 반대편 관계를 생성
자동 생성 없음
명시적 참조
없음
관계 설정이 명시적
복잡한 관계 처리
덜 유연함
더 유연하고 복잡한 관계 설정 가능
유지보수성
작은 프로젝트에 적합
큰 프로젝트나 복잡한 관계에 적합
💬
관계가 나온김에 자주 에러가 발생하거나 실수하는 Lazy Loading에대해서 알아보겠습니다.
레이지 로딩(Lazy Loading)은 SQLAlchemy와 같은 ORM(Object Relational Mapper)에서 사용되는 기법으로, 데이터베이스로부터 데이터를 실제로 필요할 때 가져오는 방식.
객체의 특정 속성에 접근할 때까지 데이터를 지연 로드(접근 할 때, 쿼리 실행)하는 방법
성능 최적화와 메모리 사용을 줄이기 위해 유용한 기법.
#위의 테이블을 봤을때. with Session() as session: addr = session.query(Address).first() addr.user # 세션이 열려있을때는 접근이 가능하지만 반복된다면 # N+1문제가 발생할 수도 있다. addr.user # lazy loading 에러 발생
Lazy Loading의 단점
1.
N+1 문제: 부모 객체를 반복적으로 로드할 때마다 자식 객체에 대한 개별 쿼리가 실행되어 성능 저하를 초래할 수 있습니다. 이를 피하기 위해 joined 또는 **subquery**와 같은 다른 로딩 전략을 사용할 수 있습니다.
2.
복잡성 증가: 데이터를 필요할 때 로드하므로, 코드의 흐름을 이해하는 데 다소 복잡성이 추가될 수 있습니다.
해결 방법
joinedload: 관련 데이터를 JOIN을 통해 한 번에 로드하여 N+1 문제를 해결하는 데 일반적으로 사용되는 로딩 전략입니다.
with Session() as session: addr = session.query(Address).options(joinedload(Address.user)).first() addr.user addr.user

마무리

SQLAlchemy에 대해서 사용방법(주로 쿼리 위주)만 알고 있었는데, KRM 프로젝트와 세션을 준비하면서 SQLAlchemy에대해 좀 더 알 수 있게 된것 같습니다.
SQLAlchemy로 할 수 있는 성능 최적화, 쿼리문 등에 대해 추가적으로 공부할 예정입니다.

참조