파이썬에서 데이터베이스 작업을 쉽게 하기 위해 제공되는 라이브러리.
ORM 기능을 통해 파이썬 객체와 데이터베이스 테이블 간의 매핑을 간편하게 해줌.
파이썬 라이브러리이기 때문에 아래와 같이 설치를 해야한다.
pip install sqlalchemy
from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=True)
## MySQL
pip install pymysql
## PostgreSQL
pip install psycopg2
## MySQL
DATABASE_URL = "mysql+<드라이버>://<유저명>:<비밀번호>@<호스트>/<DB명>"
DATABASE_URL = "mysql+pymysql://user:1234@localhost/mydb" #ex
## PostgreSQL
DATABASE_URL = "postgresql+<드라이버>://<유저명>:<비밀번호>@<호스트>/<DB명>"
DATABASE_URL = "postgresql+psycopg2://user:1234@localhost/mydb" #ex
from sqlalchemy import create_engine
engine = create_engine(DATABASE_URL, echo=True)
테이블과 매핑할 클래스를 만들기 위해 Declarative Base를 생성한다.
from sqlalchemy.ext.declarative import DeclarativeBase
from sqlalchemy.orm import registry
from sqlalchemy import Column, Integer, String, DateTime, func
import uuid
from sqlalchemy.dialects.mysql import CHAR, VARCHAR
class Base(DeclarativeBase):
registry = registry(
type_annotation_map={
str: VARCHAR(255), # 기본적으로 모든 str 타입을 VARCHAR(255)로 매핑
uuid.UUID: CHAR(36) # UUID를 CHAR(36)으로 매핑 (MySQL에서 UUID 표현)
}
)
id = Column(Integer, primary_key=True, autoincrement=True)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
def save(self, session):
session.add(self)
session.commit()
def delete(self, session):
session.delete(self)
session.commit()
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id})>"
SQLAlchemy는 파이썬 코드를 사용하여 데이터베이스 스키마(즉, 테이블, 열, 제약 조건 등)를 정의하고 조작할 수 있다.
from sqlalchemy import (Table,
select,
Column,
Integer,
String,
create_engine,
ForeignKey,
Index,
Numeric,
DateTime,
func,
event,
MetaData)
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
아래 예제에서 ORM 부분은 위 코드가 항상 포함되어있다고 가정
SQL
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255) UNIQUE );
SQLAlchemy
class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(255), nullable=False) email = Column(String(255), unique=True, nullable=False)
SQL
CREATE TABLE departments ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) UNIQUE ); CREATE TABLE employees ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), department_id INT, FOREIGN KEY (department_id) REFERENCES departments(id) );
SQLAlchemy
class Department(Base): __tablename__ = 'departments' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(255), unique=True, nullable=False) employees = relationship('Employee', back_populates='department') class Employee(Base): __tablename__ = 'employees' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(255), nullable=False) department_id = Column(Integer, ForeignKey('departments.id')) department = relationship('Department', back_populates='employees')
SQL
CREATE TABLE students ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) ); CREATE TABLE courses ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) ); CREATE TABLE student_courses ( student_id INT, course_id INT, PRIMARY KEY(student_id, course_id), FOREIGN KEY (student_id) REFERENCES students(id), FOREIGN KEY (course_id) REFERENCES courses(id) );
SQLAlchemy
student_courses = Table('student_courses', Base.metadata, Column('student_id', Integer, ForeignKey('students.id'), primary_key=True), Column('course_id', Integer, ForeignKey('courses.id'), primary_key=True) ) class Student(Base): __tablename__ = 'students' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(255), nullable=False) courses = relationship('Course', secondary=student_courses, back_populates='students') class Course(Base): __tablename__ = 'courses' id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(255), nullable=False) students = relationship('Student', secondary=student_courses, back_populates='courses')
SQL
CREATE TABLE products ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE (name), INDEX idx_price (price) );
SQLAlchemy
class Product(Base): __tablename__ = 'products' id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(255), unique=True, nullable=False) price = Column(Numeric(10, 2), nullable=False, default=0.00) stock = Column(Integer, nullable=False, default=0) created_at = Column(DateTime, server_default=func.now()) __table_args__ = ( Index('idx_price', price), )
SQL
CREATE TRIGGER update_stock_before_insert BEFORE INSERT ON products FOR EACH ROW SET NEW.stock = IFNULL(NEW.stock, 0);
SQLAlchemy
@event.listens_for(Product, "before_insert") def set_default_stock(mapper, connection, target): if target.stock is None: target.stock = 0
SQL
CREATE VIEW product_view AS SELECT id, name, price FROM products WHERE stock > 0;
SQLAlchemy
product_view = Table('product_view', Base.metadata, Column('id', Integer, primary_key=True), Column('name', String(255)), Column('price', Numeric(10, 2)), autoload_with=engine, extend_existing=True ) Base.metadata.create_all(engine) # 명시적으로 뷰 생성 쿼리 실행 engine.execute(""" CREATE VIEW product_view AS SELECT id, name, price FROM products WHERE stock > 0; """)
SQL
CREATE TABLE log ( id INT AUTO_INCREMENT PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) PARTITION BY RANGE (YEAR(created_at)) ( PARTITION p0 VALUES LESS THAN (2021), PARTITION p1 VALUES LESS THAN (2022), PARTITION p2 VALUES LESS THAN (2023) );
SQLAlchemy
# 수동으로 파티셔닝 테이블 생성 metadata = MetaData() log = Table('log', metadata, Column('id', Integer, primary_key=True), Column('created_at', DateTime, server_default=func.now()), mysql_partition_by='RANGE (YEAR(created_at))', mysql_partitions=[ 'PARTITION p0 VALUES LESS THAN (2021)', 'PARTITION p1 VALUES LESS THAN (2022)', 'PARTITION p2 VALUES LESS THAN (2023)' ] ) metadata.create_all(engine)
SQLAlchemy는 파이썬 객체를 통해 데이터베이스에서 데이터를 삽입, 조회, 수정, 삭제할 수 있는 기능을 제공한다
# 세션 생성
session = db.get_session()
...
# 세션 종료
session.close()
아래 예제에서 ORM 부분은 위 코드가 항상 데코되어있다고 가정
SQL
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
SQLAlchemy
new_user = User(name='Alice', email='alice@example.com') session.add(new_user) session.commit()
SQL
SELECT * FROM users WHERE name = 'Alice';
SQLAlchemy
user = session.query(User).filter_by(name='Alice').first() print(user)
SQL
UPDATE users SET email = 'newalice@example.com' WHERE name = 'Alice';
SQLAlchemy
user = session.query(User).filter_by(name='Alice').first() user.email = 'newalice@example.com' session.commit()
SQL
DELETE FROM users WHERE name = 'Alice';
SQLAlchemy
user = session.query(User).filter_by(name='Alice').first() session.delete(user) session.commit()
SQL
SELECT employees.name, departments.name FROM employees JOIN departments ON employees.department_id = departments.id WHERE departments.name = 'Engineering' ORDER BY employees.name;
SQLAlchemy
results = session.query(Employee.name, Department.name)\ .join(Department, Employee.department_id == Department.id)\ .filter(Department.name == 'Engineering')\ .order_by(Employee.name).all() for employee_name, department_name in results: print(f"{employee_name} works in {department_name}")
SQL
SELECT department_id, COUNT(*) FROM employees GROUP BY department_id;
SQLAlchemy
results = session.query(Employee.department_id, func.count(Employee.id))\ .group_by(Employee.department_id).all() for department_id, count in results: print(f"Department {department_id} has {count} employees")
SQL
SELECT name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100);
SQLAlchemy
subq = session.query(Order.user_id).filter(Order.total > 100).subquery() users = session.query(User.name).filter(User.id.in_(subq)).all() for user in users: print(user.name)
SQL
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com'), ('Charlie', 'charlie@example.com');
SQLAlchemy
session.bulk_save_objects([ User(name='Bob', email='bob@example.com'), User(name='Charlie', email='charlie@example.com') ]) session.commit()
SQL
START TRANSACTION; INSERT INTO users (name, email) VALUES ('Dave', 'dave@example.com'); UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; COMMIT;
SQLAlchemy
try: new_user = User(name='Dave', email='dave@example.com') session.add(new_user) account1 = session.query(Account).filter_by(user_id=1).first() account2 = session.query(Account).filter_by(user_id=2).first() account1.balance -= 100 account2.balance += 100 session.commit() except: session.rollback() raise
SQL에는 없고 SQLAlchemy에서 제공하는 이벤트 리스너 기능을 통해 특정 이벤트에 반응하여 커스텀 동작을 수행할 수 있음
SQLAlchemy
from sqlalchemy import event @event.listens_for(User, "before_insert") def generate_user_uuid(mapper, connection, target): target.user_uuid = str(uuid.uuid4())
이벤트 리스너 종류
before_insert: 새로운 레코드가 데이터베이스에 삽입되기 전
after_insert: 새로운 레코드가 데이터베이스에 삽입된 후
before_update: 기존 레코드가 업데이트되기 전
after_update: 기존 레코드가 업데이트된 후
before_delete: 레코드가 삭제되기 전
after_delete: 레코드가 삭제된 후