Skip to content
Search

SQLAlchemy

lastmod 2026-04-14created 2024-07-3117 edits

Install

pip install sqlalchemy

Quickstart

Declare Models

from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
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_account"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(30))
    fullname: Mapped[Optional[str]]
    addresses: Mapped[List["Address"]] = relationship(
        back_populates="user", cascade="all, delete-orphan"
    )
    def __repr__(self) -> str:
        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

class Address(Base):
    __tablename__ = "address"
    id: Mapped[int] = mapped_column(primary_key=True)
    email_address: Mapped[str]
    user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    user: Mapped["User"] = relationship(back_populates="addresses")
    def __repr__(self) -> str:
        return f"Address(id={self.id!r}, email_address={self.email_address!r})"

Create an Engine

from sqlalchemy import create_engine

engine = create_engine("sqlite://", echo=True)

INFORMATION

Content

Emit CREATE TABLE DDL

Base.metadata.create_all(engine)

Create Objects and Persist

from sqlalchemy.orm import Session

with Session(engine) as session:
    spongebob = User(
        name="spongebob",
        fullname="Spongebob Squarepants",
        addresses=[Address(email_address="[email protected]")],
    )
    sandy = User(
        name="sandy",
        fullname="Sandy Cheeks",
        addresses=[
            Address(email_address="[email protected]"),
            Address(email_address="[email protected]"),
        ],
    )
    patrick = User(name="patrick", fullname="Patrick Star")
    session.add_all([spongebob, sandy, patrick])
    session.commit()

Simple SELECT

from sqlalchemy import select

session = Session(engine)

stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))

for user in session.scalars(stmt):
    print(user)

SELECT with JOIN

stmt = (
    select(Address)
    .join(Address.user)
    .where(User.name == "sandy")
    .where(Address.email_address == "[email protected]")
)
sandy_address = session.scalars(stmt).one()
print(sandy_address)

declarative_base

declarative_base is a function in SQLAlchemy that provides the base class used when defining ORM (object-relational mapping) models.

With this function you can map Python classes to database tables: each class corresponds to a table in the database, and each class attribute corresponds to a column of the table.

For example, it can be used as follows:

from sqlalchemy.ext.declarative import declarative_base

# 기본 클래스를 생성
Base = declarative_base()

# 사용자 테이블을 나타내는 클래스 정의
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

In this way, you can define classes that represent database tables by inheriting from the Base class created with declarative_base().

This lets SQLAlchemy handle the mapping between the database and Python objects automatically.

Connecting to MsSQL (pymssql)

from sqlalchemy import create_engine
engine = create_engine('mssql+pymssql://username:passwd@host/database', echo=True)

How to define a Table

Setting the table name:

from sqlalchemy.ext.declarative import declarative_base

# 기본 클래스를 생성
Base = declarative_base()

# 사용자 테이블을 나타내는 클래스 정의
class TbEpAiscrapchargeIfTable(Base):
    __tablename__ = "TB_EP_AISCRAPCHARGE_IF"
    __table_args__ = {"extend_existing": True}

    idx = Column(Integer, primary_key=True)
    # ...
__tablename__ = "TB_EP_AISCRAPCHARGE_IF"
This attribute specifies the name of the table that will actually be created in the database. Here, the table is created with the name "TB_EP_AISCRAPCHARGE_IF".
__table_args__ = {"extend_existing": True}
Used to specify additional settings when creating the table.
  • extend_existing=True - Means that if a table with this name already exists, the existing table will be extended. This lets you add extra columns or settings while keeping the existing table's schema.

Setting a DateTime default value

from sqlalchemy import Column, DateTime, func

class TbEpAiscrapchargeIfTable(Base, BaseTable):
    __tablename__ = "TB_EP_AISCRAPCHARGE_IF"
    __table_args__ = {"extend_existing": True}

    insert_date = Column("INSERT_DATE", DateTime, default=func.now())

Just set default=func.now().

Function that fetches rows after a specific date and sorts them in ascending order by date

from datetime import datetime
from sqlalchemy.orm import Session
from typing import List

def get_rows_after_date(session: Session, target_date: datetime) -> List[TbEpAiscrapchargeIfTable]:
    """
    특정 날짜 이후 insert_date 컬럼에 저장된 행을 가져오고, 날짜 기준으로 오름차순 정렬하는 함수.

    Parameters:
    - session: SQLAlchemy 세션 객체
    - target_date: datetime 객체로 특정 날짜

    Returns:
    - List[TbEpAiscrapchargeIfTable]: 해당 날짜 이후의 모든 행 목록 (오름차순 정렬)
    """
    return (session.query(TbEpAiscrapchargeIfTable)
            .filter(TbEpAiscrapchargeIfTable.insert_date > target_date)
            .order_by(TbEpAiscrapchargeIfTable.insert_date.asc())
            .all())

Columns not accessible after the context exits

Code like the following

def all_heat_infos():
    with DBManager.instance().query(TbEpAiscrapchargeIfTable) as q:
        return q.all()

all_heat_infos().idx  # Error!!

raises an error when the query result is accessed. This is because the returned object is disconnected from the DB instance when the context exits.

To fix this, use expunge_all(), which detaches all objects queried in the session from the session.

After expunge_all() is called, the session no longer tracks the objects, so they exist independently.

def all_heat_infos():
    with DBManager.instance().query(TbEpAiscrapchargeIfTable) as q:
        rows = q.all()
        q.session.expunge_all()
        return rows

all_heat_infos().idx  # OK

Filter example

def get_heat_infos_after_seq(if_seq: int):
    with DBManager.instance().query(TbEpAiscrapchargeIfTable) as q:
        rows = (
            q.filter(TbEpAiscrapchargeIfTable.if_seq >= if_seq)
            .order_by(asc(TbEpAiscrapchargeIfTable.if_seq))
            .all()
        )
        q.session.expunge_all()
        return rows

Function that deletes all rows before a cutoff date

from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy import and_

def delete_rows_before_date(cutoff_date: datetime, session: Session):
    """
    주어진 기준일 이전의 모든 행을 삭제합니다.

    Parameters:
        cutoff_date (datetime): 기준일
        session (Session): SQLAlchemy 세션 객체
    """
    # 기준일 이전의 모든 행을 조회하여 삭제
    session.query(HeatInfoCandidateTable) \
           .filter(HeatInfoCandidateTable.reg_ddtt < cutoff_date) \
           .delete(synchronize_session=False)
    session.commit()

# 사용 예시
with DBManager.instance().Session.begin() as session:
    cutoff_date = datetime(2023, 1, 1)  # 예: 2023년 1월 1일 이전의 데이터 삭제
    delete_rows_before_date(cutoff_date, session)

synchronize_session=False is an option that avoids session synchronization to improve performance.

LIKE comparison example

@staticmethod
def select_charging_info_with_heat_text(heat_no: int):
    with DBManager.instance().query(ChargingInfoTable) as q:
        rows = q.filter(ChargingInfoTable.heat_text.like(f"D_{heat_no}")).all()
        q.session.expunge_all()
        return rows

Troubleshooting

Adaptive Server is unavailable or does not exist

The following error occurs even though all the information is correct:

Traceback (most recent call last):
  File "src/pymssql/_pymssql.pyx", line 650, in pymssql._pymssql.connect
  File "src/pymssql/_mssql.pyx", line 2158, in pymssql._mssql.connect
  File "src/pymssql/_mssql.pyx", line 712, in pymssql._mssql.MSSQLConnection.__init__
  File "src/pymssql/_mssql.pyx", line 1884, in pymssql._mssql.maybe_raise_MSSQLDatabaseException
  File "src/pymssql/_mssql.pyx", line 1901, in pymssql._mssql.raise_MSSQLDatabaseException
pymssql._mssql.MSSQLDatabaseException: (20009, b'DB-Lib error message 20009, severity 9:\nUnable to connect: Adaptive Server is unavailable or does not exist ([email protected])\n')

Check whether you applied URL ENCODING to the url argument passed when calling the create_engine function.

For example, an "@" character in the password can cause a parsing problem. (e.g. if the password is "PassW@rd" and it is passed as mssql+pymssql://user:PassW@[email protected]:1433/db?charset=utf8, the URL syntax breaks)

In this case, replace "@" with something like "%40", or use a function such as urllib.parse.quote_plus.

from urllib.parse import quote_plus

password = quote_plus("PassW@rd")

Ignoring declarative-like tuple value of attribute '...'

SAWarning: Ignoring declarative-like tuple value of attribute 'if_seq': possibly a copy-and-paste error with a comma accidentally placed at the end of the line?
  class TbEpAiscrapchargeIfTable(Base, BaseTable):

Looks like a comma ended up at the end of the line from copy-pasting.

class TbEpAiscrapchargeIfTable(Base, BaseTable):
    __tablename__ = "TB_EP_AISCRAPCHARGE_IF"
    __table_args__ = {"extend_existing": True}

    if_seq = Column("IF_SEQ", Integer, nullable=True),  # 여기서 마지막 쉼표 빼자.

could not assemble any primary key columns for mapped table '...'

sqlalchemy.exc.ArgumentError: Mapper Mapper[TbEpAiscrapchargeIfTable(TB_EP_AISCRAPCHARGE_IF)] could not assemble any primary key columns for mapped table 'TB_EP_AISCRAPCHARGE_IF'

Occurs when no primary key column can be found. SQLAlchemy requires a primary key to be defined on each mapped table in order to set up object-relational mapping (ORM).

To fix this, check the primary key definition.

Check whether any attribute has primary_key=True set on its Column object.

See also

Favorite site

author Zer0 sitemap