KR_ORM - somaz94/python-study GitHub Wiki
ORM(Object Relational Mapping)์ ๊ฐ์ฒด์ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋งคํํ๋ ๊ธฐ์ ์ด๋ค.
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
engine = create_engine('sqlite:///example.db', echo=True) # echo=True๋ก ์คํ๋๋ SQL ์ถ๋ ฅ
Base = declarative_base()
Session = sessionmaker(bind=engine)
# ๋ชจ๋ธ ์ ์
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(100), unique=True)
def __repr__(self):
return f"<User(name='{self.name}', email='{self.email}')>"
# ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ๋ฐฉ๋ฒ
postgres_engine = create_engine('postgresql://username:password@localhost:5432/mydatabase')
mysql_engine = create_engine('mysql+mysqlconnector://username:password@localhost/mydatabase')
# ํ๊ฒฝ ๋ณ์๋ฅผ ์ฌ์ฉํ ์ฐ๊ฒฐ ๋ฌธ์์ด ๊ด๋ฆฌ
import os
from dotenv import load_dotenv
load_dotenv() # .env ํ์ผ์์ ํ๊ฒฝ ๋ณ์ ๋ก๋
db_url = os.getenv('DATABASE_URL')
secure_engine = create_engine(db_url)
# ์ฐ๊ฒฐ ํ๋ง ์ค์
pooled_engine = create_engine(
'sqlite:///example.db',
pool_size=10, # ํ์ ์ ์งํ ์ฐ๊ฒฐ ์
max_overflow=20, # ํ ํฌ๊ธฐ๋ฅผ ์ด๊ณผํ์ฌ ์์ฑํ ์ ์๋ ์ฐ๊ฒฐ ์
pool_timeout=30, # ์ฐ๊ฒฐ ๋๊ธฐ ์๊ฐ(์ด)
pool_recycle=1800 # ์ฐ๊ฒฐ ์ฌ์ฌ์ฉ ์๊ฐ(์ด)
)
โ
ํน์ง:
- ๊ฐ์ฒด-๊ด๊ณ ๋งคํ(ORM) ํจ๋ฌ๋ค์
- SQL ์ฝ๋ ์์ฑ ์์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์กฐ์
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ๋ฆฝ์ ์ธ ์ฝ๋ ์์ฑ ๊ฐ๋ฅ
- ๊ฐ์ฒด ์งํฅ์ ๋ฐ์ดํฐ ์ ๊ทผ
- ์๋ ์คํค๋ง ์์ฑ ๋ฐ ๊ด๋ฆฌ
- ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์คํ ์ง์
- ์ฐ๊ฒฐ ํ๋ง ๋ฐ ์ธ์ ๊ด๋ฆฌ
- ํ์ ์์ ์ฑ ์ ๊ณต
ORM์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ธฐ๋ณธ ์์
์ธ ์์ฑ(Create), ์กฐํ(Read), ์์ (Update), ์ญ์ (Delete) ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
# ํ
์ด๋ธ ์์ฑ
Base.metadata.create_all(engine)
# ๋ฐ์ดํฐ ์์ฑ (Create)
def create_user(name, email):
session = Session()
try:
user = User(name=name, email=email)
session.add(user)
session.commit()
return user.id
except Exception as e:
session.rollback()
print(f"์ฌ์ฉ์ ์์ฑ ์ค๋ฅ: {e}")
raise
finally:
session.close()
# ์ฌ๋ฌ ๊ฐ์ฒด ํ ๋ฒ์ ์์ฑ
def create_many_users(users_data):
session = Session()
try:
users = [User(name=data['name'], email=data['email']) for data in users_data]
session.add_all(users)
session.commit()
return [user.id for user in users]
except Exception as e:
session.rollback()
print(f"๋ค์ ์ฌ์ฉ์ ์์ฑ ์ค๋ฅ: {e}")
raise
finally:
session.close()
# ๋ฐ์ดํฐ ์กฐํ (Read)
def get_user(user_id):
session = Session()
try:
return session.query(User).filter(User.id == user_id).first()
finally:
session.close()
# ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ
def get_all_users():
session = Session()
try:
return session.query(User).all()
finally:
session.close()
# ์กฐ๊ฑด๋ถ ์กฐํ
def find_users_by_email_domain(domain):
session = Session()
try:
return session.query(User).filter(User.email.like(f'%@{domain}')).all()
finally:
session.close()
# ํ์ด์ง ์ฒ๋ฆฌ
def get_users_paginated(page=1, per_page=10):
session = Session()
try:
return session.query(User).order_by(User.id).offset((page - 1) * per_page).limit(per_page).all()
finally:
session.close()
# ๋ฐ์ดํฐ ์์ (Update)
def update_user(user_id, name=None, email=None):
session = Session()
try:
user = session.query(User).filter(User.id == user_id).first()
if user:
if name:
user.name = name
if email:
user.email = email
session.commit()
return True
return False
except Exception as e:
session.rollback()
print(f"์ฌ์ฉ์ ์
๋ฐ์ดํธ ์ค๋ฅ: {e}")
raise
finally:
session.close()
# ๋๋ ์
๋ฐ์ดํธ
def update_many_users(email_domain, new_domain):
session = Session()
try:
# filter + update๋ฅผ ์ฌ์ฉํ ๋ฒํฌ ์
๋ฐ์ดํธ
count = session.query(User).filter(
User.email.like(f'%@{email_domain}')
).update(
{User.email: User.email.op('REPLACE')(f'@{email_domain}', f'@{new_domain}')},
synchronize_session=False
)
session.commit()
return count
except Exception as e:
session.rollback()
print(f"๋ค์ ์ฌ์ฉ์ ์
๋ฐ์ดํธ ์ค๋ฅ: {e}")
raise
finally:
session.close()
# ๋ฐ์ดํฐ ์ญ์ (Delete)
def delete_user(user_id):
session = Session()
try:
user = session.query(User).filter(User.id == user_id).first()
if user:
session.delete(user)
session.commit()
return True
return False
except Exception as e:
session.rollback()
print(f"์ฌ์ฉ์ ์ญ์ ์ค๋ฅ: {e}")
raise
finally:
session.close()
# ๋๋ ์ญ์
def delete_users_by_email_domain(domain):
session = Session()
try:
count = session.query(User).filter(
User.email.like(f'%@{domain}')
).delete(synchronize_session=False)
session.commit()
return count
except Exception as e:
session.rollback()
print(f"๋ค์ ์ฌ์ฉ์ ์ญ์ ์ค๋ฅ: {e}")
raise
finally:
session.close()
# ์ปจํ
์คํธ ๋งค๋์ ๋ฅผ ์ฌ์ฉํ ์ธ์
๊ด๋ฆฌ
from contextlib import contextmanager
@contextmanager
def session_scope():
"""ํธ๋์ญ์
๊ด๋ฆฌ๋ฅผ ์ํ ์ธ์
์ปจํ
์คํธ ๋งค๋์ """
session = Session()
try:
yield session
session.commit()
except Exception as e:
session.rollback()
raise
finally:
session.close()
# ์ปจํ
์คํธ ๋งค๋์ ์ฌ์ฉ ์์
def create_user_with_context(name, email):
with session_scope() as session:
user = User(name=name, email=email)
session.add(user)
return user.id
โ
ํน์ง:
- ์์ ํ CRUD ์ฐ์ฐ ์ง์
- ์๋ ํธ๋์ญ์ ๊ด๋ฆฌ
- ์ธ์ ๊ธฐ๋ฐ ๊ฐ์ฒด ์ถ์
- ๋๋ ์์ ์ต์ ํ
- ์ธ์ ์ปจํ ์คํธ ๊ด๋ฆฌ
- ์กฐ๊ฑด๋ถ ์ฟผ๋ฆฌ ๋น๋
- ํ์ด์ง ๋ฐ ์ ๋ ฌ ๊ธฐ๋ฅ
- ์์ธ ์ฒ๋ฆฌ ๋ฐ ๋กค๋ฐฑ ์๋ํ
ORM์์ ํ
์ด๋ธ ๊ฐ์ ๊ด๊ณ๋ฅผ ๊ฐ์ฒด์งํฅ์ ์ผ๋ก ํํํ๋ ๋ฐฉ๋ฒ์ด๋ค.
from sqlalchemy import ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime
# ์ผ๋๋ค ๊ด๊ณ (One-to-Many)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(100), unique=True)
created_at = Column(DateTime, default=datetime.utcnow)
# ๊ด๊ณ ์ค์ - ์ฌ์ฉ์๊ฐ ์ญ์ ๋๋ฉด ๊ฒ์๋ฌผ๋ ํจ๊ป ์ญ์ (cascade)
posts = relationship("Post", back_populates="user", cascade="all, delete-orphan")
comments = relationship("Comment", back_populates="user", cascade="all, delete-orphan")
def __repr__(self):
return f"<User(name='{self.name}', email='{self.email}')>"
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
# ๊ด๊ณ ์ค์
user = relationship("User", back_populates="posts")
comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
tags = relationship("Tag", secondary="post_tags", back_populates="posts")
def __repr__(self):
return f"<Post(title='{self.title}')>"
# ๋ค๋๋ค ๊ด๊ณ (Many-to-Many)
from sqlalchemy import Table
# ์ฐ๊ฒฐ ํ
์ด๋ธ ์ ์
post_tags = Table('post_tags', Base.metadata,
Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True),
Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True)
)
class Tag(Base):
__tablename__ = 'tags'
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False)
# ๋ค๋๋ค ๊ด๊ณ
posts = relationship("Post", secondary="post_tags", back_populates="tags")
def __repr__(self):
return f"<Tag(name='{self.name}')>"
# ์ถ๊ฐ ๊ด๊ณ - ๋๊ธ
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
content = Column(Text, nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
post_id = Column(Integer, ForeignKey('posts.id'), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
# ๊ด๊ณ ์ค์
user = relationship("User", back_populates="comments")
post = relationship("Post", back_populates="comments")
def __repr__(self):
return f"<Comment(content='{self.content[:20]}...')>"
# ์๊ธฐ์ฐธ์กฐ ๊ด๊ณ (Self-Referential)
class Employee(Base):
__tablename__ = 'employees'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
manager_id = Column(Integer, ForeignKey('employees.id'), nullable=True)
# ์๊ธฐ์ฐธ์กฐ ๊ด๊ณ
manager = relationship("Employee", remote_side=[id], backref="subordinates")
def __repr__(self):
return f"<Employee(name='{self.name}')>"
# ๊ด๊ณ ๋ฐ์ดํฐ ์ฌ์ฉ ์์
def relation_examples():
with session_scope() as session:
# ์ฌ์ฉ์ ์์ฑ
user = User(name="ํ๊ธธ๋", email="[email protected]")
session.add(user)
# ๊ฒ์๋ฌผ ์์ฑ ๋ฐ ์ฐ๊ฒฐ
post = Post(title="ORM ๊ด๊ณ ์ค์ ", content="๊ด๊ณ ์ค์ ๋ฐฉ๋ฒ์ ์์๋ด
์๋ค.", user=user)
session.add(post)
# ํ๊ทธ ์์ฑ ๋ฐ ์ฐ๊ฒฐ
tag1 = Tag(name="ORM")
tag2 = Tag(name="SQLAlchemy")
post.tags.extend([tag1, tag2])
# ๋๊ธ ์์ฑ ๋ฐ ์ฐ๊ฒฐ
comment = Comment(content="์ข์ ๊ธ์
๋๋ค!", user=user, post=post)
session.add(comment)
# ์ง์ ๊ด๊ณ ์ค์
manager = Employee(name="๊น๊ด๋ฆฌ")
session.add(manager)
employee1 = Employee(name="์ด์ฌ์", manager=manager)
employee2 = Employee(name="๋ฐ์ง์", manager=manager)
session.add_all([employee1, employee2])
# ๊ด๊ณ ์ฟผ๋ฆฌ ์์
def query_relations():
with session_scope() as session:
# ํน์ ์ฌ์ฉ์์ ๋ชจ๋ ๊ฒ์๋ฌผ ์กฐํ
user = session.query(User).filter_by(name="ํ๊ธธ๋").first()
if user:
for post in user.posts:
print(f"๊ฒ์๋ฌผ: {post.title}")
# ๊ฒ์๋ฌผ์ ๋๊ธ ์กฐํ
for comment in post.comments:
print(f" ๋๊ธ: {comment.content} (์์ฑ์: {comment.user.name})")
# ๊ฒ์๋ฌผ์ ํ๊ทธ ์กฐํ
for tag in post.tags:
print(f" ํ๊ทธ: {tag.name}")
# ํน์ ํ๊ทธ๊ฐ ์๋ ๋ชจ๋ ๊ฒ์๋ฌผ ์กฐํ
orm_tag = session.query(Tag).filter_by(name="ORM").first()
if orm_tag:
for post in orm_tag.posts:
print(f"ORM ํ๊ทธ ๊ฒ์๋ฌผ: {post.title} (์์ฑ์: {post.user.name})")
# ๊ด๋ฆฌ์์ ๋ถํ ์ง์ ์กฐํ
manager = session.query(Employee).filter_by(name="๊น๊ด๋ฆฌ").first()
if manager:
print(f"๊ด๋ฆฌ์: {manager.name}")
for emp in manager.subordinates:
print(f" ๋ถํ์ง์: {emp.name}")
โ
ํน์ง:
- ๋ค์ํ ๊ด๊ณ ์ ํ ์ง์ (์ผ๋๋ค, ๋ค๋๋ค, ์๊ธฐ์ฐธ์กฐ)
- ์๋ฐฉํฅ ๊ด๊ณ ์ค์
- ์ธ๋ ํค ์ ์ฝ ์กฐ๊ฑด
- ๊ด๊ณ ์บ์ค์ผ์ด๋ ๋์
- ์ง์ฐ ๋ก๋ฉ(Lazy Loading)
- ์ฆ์ ๋ก๋ฉ(Eager Loading)
- ์กฐ์ธ ํ ์ด๋ธ ๋งคํ
- ๊ฐ์ฒด ๊ทธ๋ํ ํ์
ORM์ ์ฌ์ฉํ ๋ณต์กํ ์ฟผ๋ฆฌ ์์ฑ ๋ฐ ์ต์ ํ ๋ฐฉ๋ฒ์ด๋ค.
from sqlalchemy import and_, or_, not_, func, distinct, text, desc, asc
from sqlalchemy.orm import aliased, contains_eager, joinedload, selectinload
def advanced_query_examples():
with session_scope() as session:
# ๋ณตํฉ ์กฐ๊ฑด ํํฐ๋ง
users = session.query(User).filter(
and_(
User.name.like('ํ%'),
User.email.contains('@example.com'),
or_(
User.created_at > datetime(2021, 1, 1),
not_(User.email.endswith('gmail.com'))
)
)
).all()
# ์ ๋ ฌ
users = session.query(User).order_by(User.created_at.desc(), User.name).all()
# ์ง๊ณ ํจ์
user_count = session.query(func.count(User.id)).scalar()
post_stats = session.query(
func.count(Post.id).label('total_posts'),
func.avg(func.length(Post.content)).label('avg_length')
).first()
# ๊ทธ๋ฃนํ
post_counts = session.query(
User.name,
func.count(Post.id).label('post_count')
).join(Post).group_by(User.id).order_by(desc('post_count')).all()
# ์๋ธ์ฟผ๋ฆฌ
from sqlalchemy.sql import exists
active_users = session.query(User).filter(
exists().where(Post.user_id == User.id)
).all()
# ๋ณ์นญ์ ์ฌ์ฉํ ์๊ธฐ ์กฐ์ธ
mgr_alias = aliased(Employee, name='manager')
results = session.query(
Employee.name, mgr_alias.name
).join(
mgr_alias, Employee.manager_id == mgr_alias.id
).all()
# ์๋์ฐ ํจ์ (SQLAlchemy 1.4+)
from sqlalchemy.sql import functions as func
stmt = session.query(
Post,
func.row_number().over(
order_by=Post.created_at
).label('row_number'),
func.rank().over(
partition_by=Post.user_id,
order_by=Post.created_at.desc()
).label('rank')
).subquery()
ranked_posts = session.query(stmt).filter(stmt.c.rank == 1).all()
# ์์ SQL ์ฟผ๋ฆฌ
raw_results = session.query(User).from_statement(
text("SELECT * FROM users WHERE name LIKE :name")
).params(name='ํ%').all()
return {
'users': users,
'user_count': user_count,
'post_stats': post_stats,
'post_counts': post_counts,
'active_users': active_users,
'reporting_chain': results,
'ranked_posts': ranked_posts,
'raw_results': raw_results
}
# N+1 ๋ฌธ์ ํด๊ฒฐ - ์ง์ฐ ๋ก๋ฉ vs ์ฆ์ ๋ก๋ฉ
def n_plus_one_problem():
# N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์์
with session_scope() as session:
users = session.query(User).all() # 1๋ฒ ์ฟผ๋ฆฌ
# ๊ฐ ์ฌ์ฉ์๋ณ๋ก ์ถ๊ฐ ์ฟผ๋ฆฌ ์คํ (N๋ฒ์ ์ถ๊ฐ ์ฟผ๋ฆฌ)
for user in users:
print(f"{user.name}์ ๊ฒ์๋ฌผ: {len(user.posts)}")
# ํด๊ฒฐ์ฑ
1: ์ฆ์ ๋ก๋ฉ(Eager Loading) - joinedload
with session_scope() as session:
users = session.query(User).options(joinedload(User.posts)).all() # JOIN์ ํตํด 1๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ๋ก๋
for user in users:
print(f"{user.name}์ ๊ฒ์๋ฌผ: {len(user.posts)}") # ์ถ๊ฐ ์ฟผ๋ฆฌ ์์
# ํด๊ฒฐ์ฑ
2: ์ฆ์ ๋ก๋ฉ - selectinload (์ถ๊ฐ SELECT IN ์ฟผ๋ฆฌ ์ฌ์ฉ)
with session_scope() as session:
users = session.query(User).options(selectinload(User.posts)).all() # 2๋ฒ์ ํจ์จ์ ์ธ ์ฟผ๋ฆฌ๋ก ๋ก๋
for user in users:
print(f"{user.name}์ ๊ฒ์๋ฌผ: {len(user.posts)}") # ์ถ๊ฐ ์ฟผ๋ฆฌ ์์
# ํด๊ฒฐ์ฑ
3: ๋ง์ถค ์กฐ์ธ ์ฟผ๋ฆฌ
with session_scope() as session:
users_with_post_count = session.query(
User, func.count(Post.id).label('post_count')
).outerjoin(Post).group_by(User.id).all()
for user, post_count in users_with_post_count:
print(f"{user.name}์ ๊ฒ์๋ฌผ: {post_count}")
โ
ํน์ง:
- ๋ณต์กํ ์กฐ๊ฑด ์ฟผ๋ฆฌ ๊ตฌ์ฑ
- ๊ณ ๊ธ ์กฐ์ธ ๊ธฐ๋ฒ
- ์ง๊ณ ๋ฐ ๋ถ์ ํจ์
- ์๋ธ์ฟผ๋ฆฌ ๋ฐ ์ค์ฒฉ ์ฟผ๋ฆฌ
- ์๋์ฐ ํจ์ ์ง์
- ์์ SQL ์ฟผ๋ฆฌ ์คํ
- ์ฟผ๋ฆฌ ์ต์ ํ ๊ธฐ๋ฒ
- N+1 ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
ORM์ ์ฌ์ฉํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๊ด๋ฆฌ ๋ฐ ๋ณ๊ฒฝ ์ถ์ ๋ฐฉ๋ฒ์ด๋ค.
# Alembic์ ์ฌ์ฉํ ๋ง์ด๊ทธ๋ ์ด์
(SQLAlchemy์ ํจ๊ป ์ฌ์ฉ๋๋ ์ฃผ์ ๋ง์ด๊ทธ๋ ์ด์
๋๊ตฌ)
"""
์ค์น: pip install alembic
์ด๊ธฐํ: alembic init migrations
"""
# Alembic ์ค์ ์์ (alembic.ini)
"""
[alembic]
script_location = migrations
sqlalchemy.url = sqlite:///example.db
"""
# ํ๊ฒฝ ์ค์ (migrations/env.py)
"""
from alembic import context
from sqlalchemy import engine_from_config, pool
from myapp.models import Base # ์ ํ๋ฆฌ์ผ์ด์
์ ๋ชจ๋ธ์ด ์ ์๋ Base ๊ฐ์ฒด ๊ฐ์ ธ์ค๊ธฐ
target_metadata = Base.metadata # ๋ฉํ๋ฐ์ดํฐ ์ค์
def run_migrations_online():
connectable = engine_from_config(
context.config.get_section(context.config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
"""
# ๋ง์ด๊ทธ๋ ์ด์
์์ฑ ๋ฐ ์คํ ๋ช
๋ น์ด
"""
๋ง์ด๊ทธ๋ ์ด์
์์ฑ: alembic revision --autogenerate -m "Create users table"
๋ง์ด๊ทธ๋ ์ด์
์คํ: alembic upgrade head
์ด์ ๋ฒ์ ์ผ๋ก ๋กค๋ฐฑ: alembic downgrade -1
"""
# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ (migrations/versions/xxxx_create_users_table.py)
"""
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('email', sa.String(length=100), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
def downgrade():
op.drop_table('users')
"""
# ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์คํค๋ง ์กฐ์
def schema_manipulation():
from sqlalchemy import inspect, MetaData
with session_scope() as session:
# ํ
์ด๋ธ ๋ฉํ๋ฐ์ดํฐ ๊ฒ์ฌ
inspector = inspect(engine)
# ํ
์ด๋ธ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
tables = inspector.get_table_names()
print(f"๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ: {tables}")
# ํ
์ด๋ธ ์ปฌ๋ผ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
for table in tables:
columns = inspector.get_columns(table)
print(f"\n{table} ํ
์ด๋ธ ์ปฌ๋ผ:")
for column in columns:
print(f" {column['name']}: {column['type']}")
# ์ธ๋ฑ์ค ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
for table in tables:
indexes = inspector.get_indexes(table)
if indexes:
print(f"\n{table} ํ
์ด๋ธ ์ธ๋ฑ์ค:")
for index in indexes:
print(f" {index['name']} ({', '.join(index['column_names'])})")
# ์ธ๋ ํค ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
for table in tables:
fks = inspector.get_foreign_keys(table)
if fks:
print(f"\n{table} ํ
์ด๋ธ ์ธ๋ ํค:")
for fk in fks:
print(f" {fk['constrained_columns']} -> {fk['referred_table']}.{fk['referred_columns']}")
# ๋์ ๋ชจ๋ธ ์์ฑ
def create_dynamic_model(table_name, columns):
"""๋์ ์ผ๋ก ORM ๋ชจ๋ธ ํด๋์ค ์์ฑ"""
attrs = {
'__tablename__': table_name,
'__table_args__': {'extend_existing': True}
}
# ID ์ด ์ถ๊ฐ
attrs['id'] = Column(Integer, primary_key=True)
# ์ง์ ๋ ์ด ์ถ๊ฐ
for col_name, col_type in columns.items():
if col_type == 'string':
attrs[col_name] = Column(String(100))
elif col_type == 'integer':
attrs[col_name] = Column(Integer)
elif col_type == 'datetime':
attrs[col_name] = Column(DateTime)
elif col_type == 'boolean':
attrs[col_name] = Column(Boolean)
# ๋์ ๋ชจ๋ธ ํด๋์ค ์์ฑ
DynamicModel = type(table_name.capitalize(), (Base,), attrs)
# ํ
์ด๋ธ์ด ์์ผ๋ฉด ์์ฑ
if not inspect(engine).has_table(table_name):
DynamicModel.__table__.create(engine)
return DynamicModel
โ
ํน์ง:
- ์๋ํ๋ ์คํค๋ง ์์ฑ
- ๋ฒ์ ๊ด๋ฆฌ ๋ง์ด๊ทธ๋ ์ด์
- ์คํค๋ง ๋ณ๊ฒฝ ์ถ์
- ๋กค๋ฐฑ ๊ธฐ๋ฅ
- ๋ฉํ๋ฐ์ดํฐ ๊ฒ์ฌ
- ํ ์ด๋ธ ๋ฐ ์ปฌ๋ผ ์ ๋ณด ์ ๊ทผ
- ๋์ ๋ชจ๋ธ ์์ฑ
- ํ ์ด๋ธ ๋ณ๊ฒฝ ์๋ํ
ORM ์ฌ์ฉ ์ ์ฑ๋ฅ์ ํฅ์์ํค๋ ๋ค์ํ ์ต์ ํ ๊ธฐ๋ฒ์ด๋ค.
# ์ฟผ๋ฆฌ ์คํ ๊ณํ ํ์ธ
def analyze_query_performance():
with session_scope() as session:
# ์ฟผ๋ฆฌ ์คํ ์ ์ถ๋ ฅ
stmt = session.query(User).join(Post).filter(Post.title.like('%ORM%'))
print(str(stmt))
# ์ฟผ๋ฆฌ ์คํ ์๊ฐ ์ธก์
import time
start = time.time()
result = stmt.all()
end = time.time()
print(f"์ฟผ๋ฆฌ ์คํ ์๊ฐ: {end - start:.4f}์ด, ๊ฒฐ๊ณผ ์: {len(result)}")
# ๋ฒํฌ ์ฐ์ฐ ์ฌ์ฉ
def bulk_operations():
with session_scope() as session:
# ๋ฒํฌ ์ฝ์
from sqlalchemy.dialects.postgresql import insert as pg_insert
# PostgreSQL์ ๊ฒฝ์ฐ (UPSERT ๊ธฐ๋ฅ ์ง์)
users_data = [
{"name": "๊น์ฒ ์", "email": "[email protected]"},
{"name": "์ด์ํฌ", "email": "[email protected]"}
]
stmt = pg_insert(User.__table__).values(users_data)
stmt = stmt.on_conflict_do_update(
index_elements=['email'],
set_=dict(name=stmt.excluded.name)
)
session.execute(stmt)
# ๋ฒํฌ ์
๋ฐ์ดํธ - dialect ๋
๋ฆฝ์ ๋ฐฉ๋ฒ
session.query(User).filter(
User.email.like('%@example.com')
).update(
{"name": User.name + " (์์ ๋จ)"},
synchronize_session=False
)
# ๋ฒํฌ ์ญ์
deleted = session.query(Post).filter(
Post.created_at < datetime(2021, 1, 1)
).delete(synchronize_session=False)
print(f"{deleted}๊ฐ์ ๊ฒ์๋ฌผ์ด ์ญ์ ๋์์ต๋๋ค.")
# ์ธ์
์บ์ฑ ํ์ฉ
def session_caching_example():
with session_scope() as session:
# ์ฒซ ๋ฒ์งธ ์ฟผ๋ฆฌ - ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ก๋
user1 = session.query(User).filter_by(id=1).first()
# ๋ ๋ฒ์งธ ์ฟผ๋ฆฌ - ์ธ์
์บ์์์ ๋ก๋ (์ถ๊ฐ ์ฟผ๋ฆฌ ์์)
user2 = session.query(User).filter_by(id=1).first()
print(f"๋์ผ ๊ฐ์ฒด ์ฌ๋ถ: {user1 is user2}") # True
# ์ธ์
์บ์ ๋ฌดํจํ
session.expire_all()
# ์ธ ๋ฒ์งธ ์ฟผ๋ฆฌ - ์บ์๊ฐ ๋ฌดํจํ๋์ด ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๋ก๋
user3 = session.query(User).filter_by(id=1).first()
# ์ฟผ๋ฆฌ ์ต์ ํ - ํ์ํ ์ปฌ๋ผ๋ง ์กฐํ
def select_only_needed_columns():
with session_scope() as session:
# ๋ชจ๋ ์ปฌ๋ผ ์กฐํ
users_full = session.query(User).all()
# ํ์ํ ์ปฌ๋ผ๋ง ์กฐํ
users_partial = session.query(User.id, User.name).all()
# ๊ฐ๋ณ ์ํฐํฐ ๋์ ๋์
๋๋ฆฌ ํํ๋ก ์กฐํ
users_dict = session.query(
User.id, User.name, User.email
).all()
# ์ปค์คํ
์ฟผ๋ฆฌ ์ตํฐ๋ง์ด์
class QueryOptimizer:
def __init__(self, session):
self.session = session
def optimize_query(self, query, limit=None):
"""์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์ต์ ํ"""
# ๊ด๊ณ ๊ฐ์ง ๋ฐ ์๋ ์กฐ์ธ ๋ก๋ ์ถ๊ฐ
from sqlalchemy.inspection import inspect
# ์ฟผ๋ฆฌ ๋์ ์ํฐํฐ ํ์ธ
entities = []
for entity in query.column_descriptions:
if hasattr(entity['type'], '__tablename__'):
entities.append(entity['type'])
if not entities:
return query
# ์ฃผ ์ํฐํฐ ์ ํ
main_entity = entities[0]
# ๊ด๊ณ ๊ฒ์ฌ
mapper = inspect(main_entity)
relationships = list(mapper.relationships)
# ์ฟผ๋ฆฌ์์ ์ฌ์ฉ ์ค์ธ ๊ด๊ณ ๊ฐ์ง
loaded_relationships = []
for rel in relationships:
attr_name = rel.key
if hasattr(main_entity, attr_name) and attr_name in query._joinpoint.entities:
loaded_relationships.append(attr_name)
# ๊ด๊ณ๊ฐ ๋ก๋๋๋ฉด ์๋์ผ๋ก joinedload ์ถ๊ฐ
for rel_name in loaded_relationships:
query = query.options(joinedload(getattr(main_entity, rel_name)))
# ํ์ํ ๊ฒฝ์ฐ ์ ํ ์ถ๊ฐ
if limit is not None:
query = query.limit(limit)
return query
โ
ํน์ง:
- ์ฟผ๋ฆฌ ์คํ ๊ณํ ๋ถ์
- ๋ฒํฌ ์ฐ์ฐ ํ์ฉ
- ์ธ์ ์บ์ฑ ํ์ฉ
- ๋ถ๋ถ ์ปฌ๋ผ ์ ํ์ ๋ก๋
- ๊ด๊ณ ๋ก๋ฉ ์ต์ ํ
- ํธ๋์ญ์ ๊ด๋ฆฌ
- ์ธ๋ฑ์ค ํจ์จ์ ํ์ฉ
- ์ปค์คํ ์ต์ ํ ์ ๋ต
ORM์ ์ค์ ์ ํ๋ฆฌ์ผ์ด์
์ ํตํฉํ๋ ๋ชจ๋ฒ ์ฌ๋ก์ ํจํด์ด๋ค.
# ๋ฆฌํฌ์งํ ๋ฆฌ ํจํด ๊ตฌํ
class Repository:
"""๋ฐ์ดํฐ ์ก์ธ์ค ๋ก์ง์ ์บก์ํํ๋ ๋ฆฌํฌ์งํ ๋ฆฌ ํจํด"""
def __init__(self, model, session_factory):
self.model = model
self.session_factory = session_factory
def get(self, id):
with session_scope() as session:
return session.query(self.model).filter_by(id=id).first()
def get_all(self):
with session_scope() as session:
return session.query(self.model).all()
def find_by(self, **kwargs):
with session_scope() as session:
return session.query(self.model).filter_by(**kwargs).all()
def create(self, **kwargs):
with session_scope() as session:
instance = self.model(**kwargs)
session.add(instance)
session.commit()
session.refresh(instance)
return instance
def update(self, id, **kwargs):
with session_scope() as session:
instance = session.query(self.model).filter_by(id=id).first()
if instance:
for key, value in kwargs.items():
setattr(instance, key, value)
session.commit()
return instance
return None
def delete(self, id):
with session_scope() as session:
instance = session.query(self.model).filter_by(id=id).first()
if instance:
session.delete(instance)
session.commit()
return True
return False
# ์ ํ๋ฆฌ์ผ์ด์
๊ตฌ์ฑ ์์ - Flask ์น ์ ํ๋ฆฌ์ผ์ด์
"""
from flask import Flask, request, jsonify
from myapp.models import Base, User
from myapp.repository import Repository
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
app = Flask(__name__)
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์
engine = create_engine('sqlite:///app.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
# ๋ฆฌํฌ์งํ ๋ฆฌ ์์ฑ
user_repo = Repository(User, Session)
@app.route('/users', methods=['GET'])
def get_users():
users = user_repo.get_all()
return jsonify([{
'id': user.id,
'name': user.name,
'email': user.email
} for user in users])
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = user_repo.get(user_id)
if user:
return jsonify({
'id': user.id,
'name': user.name,
'email': user.email
})
return jsonify({'error': 'User not found'}), 404
@app.route('/users', methods=['POST'])
def create_user():
data = request.json
try:
user = user_repo.create(**data)
return jsonify({
'id': user.id,
'name': user.name,
'email': user.email
}), 201
except Exception as e:
return jsonify({'error': str(e)}), 400
@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
data = request.json
user = user_repo.update(user_id, **data)
if user:
return jsonify({
'id': user.id,
'name': user.name,
'email': user.email
})
return jsonify({'error': 'User not found'}), 404
@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
success = user_repo.delete(user_id)
if success:
return '', 204
return jsonify({'error': 'User not found'}), 404
if __name__ == '__main__':
app.run(debug=True)
"""
# ๋จ์ ํ
์คํธ
"""
import unittest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from myapp.models import Base, User
from myapp.repository import Repository
class TestUserRepository(unittest.TestCase):
def setUp(self):
# ๋ฉ๋ชจ๋ฆฌ ๋ด SQLite DB
self.engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(self.engine)
Session = sessionmaker(bind=self.engine)
self.session = Session()
self.repo = Repository(User, Session)
def tearDown(self):
Base.metadata.drop_all(self.engine)
def test_create_user(self):
user = self.repo.create(name='ํ
์คํธ', email='[email protected]')
self.assertIsNotNone(user.id)
self.assertEqual(user.name, 'ํ
์คํธ')
def test_get_user(self):
user = self.repo.create(name='ํ
์คํธ', email='[email protected]')
fetched_user = self.repo.get(user.id)
self.assertEqual(fetched_user.name, 'ํ
์คํธ')
def test_update_user(self):
user = self.repo.create(name='ํ
์คํธ', email='[email protected]')
updated_user = self.repo.update(user.id, name='์์ ๋จ')
self.assertEqual(updated_user.name, '์์ ๋จ')
def test_delete_user(self):
user = self.repo.create(name='ํ
์คํธ', email='[email protected]')
success = self.repo.delete(user.id)
self.assertTrue(success)
self.assertIsNone(self.repo.get(user.id))
"""
โ
ํน์ง:
- ๋ฆฌํฌ์งํ ๋ฆฌ ํจํด์ผ๋ก ๋ฐ์ดํฐ ์ ๊ทผ ์ถ์ํ
- ์น ์ ํ๋ฆฌ์ผ์ด์ ํตํฉ ์์
- ๋จ์ ํ ์คํธ ์์ฑ ๋ฐฉ๋ฒ
- ์ธ์ ๊ด๋ฆฌ ์ ๋ต
- API ์๋ํฌ์ธํธ ์ฐ๋
- ๊ฐ๋ฐ-ํ๋ก๋์ ํ๊ฒฝ ๋ถ๋ฆฌ
- ORM ๋ชจ๋ฒ ์ฌ๋ก ์ ์ฉ
- ์ฝ๋ ๊ตฌ์กฐํ ๋ฐ ๋ชจ๋ํ
โ
๋ชจ๋ฒ ์ฌ๋ก:
- ์ธ์
๊ด๋ฆฌ ์ฃผ์
- ์ธ์ ์ค์ฝํ๋ฅผ ๋ช ํํ ์ ์ํ๊ณ ํญ์ ๋ซ๊ธฐ
- ์ปจํ ์คํธ ๋งค๋์ ํจํด ํ์ฉ
- ํ๋์ ์์ฒญ/์๋ต ์ฃผ๊ธฐ์ ํ๋์ ์ธ์ ์ฌ์ฉ
- ์ฅ๊ธฐ ์คํ ์ธ์ ํผํ๊ธฐ
- ์ ์ ํ ์ธ๋ฑ์ค ์ฌ์ฉ
- ์์ฃผ ์กฐํํ๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค ์ถ๊ฐ
- ๋ณตํฉ ์ธ๋ฑ์ค ํ์ฉ
- ์ธ๋ ํค ์ปฌ๋ผ์ ์ธ๋ฑ์ค ์ ์ฉ
- ๋ถํ์ํ ์ธ๋ฑ์ค ์ ๊ฑฐ
- Lazy Loading ์ดํด
- ์ง์ฐ ๋ก๋ฉ์ ์ฅ๋จ์ ํ์
- N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ์ธ์
- ์ฆ์ ๋ก๋ฉ ์ ์ ํ ํ์ฉ
- ํ์์ ๋ฐ๋ผ ์กฐ์ธ ์ ๋ต ์ ํ
- N+1 ๋ฌธ์ ์ฃผ์
- joinedload, selectinload ํ์ฉ
- ๊ด๊ณ ์กฐํ ์ ๋ก๋ฉ ์ ๋ต ์ง์
- ๋ค์์ ๊ฐ์ฒด ์กฐํ ์ ํนํ ์ฃผ์
- ์ฟผ๋ฆฌ ์ต์ ํ๋ก ๋ถํ์ํ ๋ก๋ฉ ๋ฐฉ์ง
- ๋ฒํฌ ์ฐ์ฐ ํ์ฉ
- ๋ค์ ๋ ์ฝ๋ ์ฒ๋ฆฌ ์ ๋ฒํฌ ์ฐ์ฐ ์ฌ์ฉ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ต์ํ
- ORM ์ค๋ฒํค๋ ๊ฐ์
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ ๋ถ์ฐ
- ์บ์ฑ ์ ๋ต ์๋ฆฝ
- ์ธ์ ์บ์ ํ์ฉ
- ์ธ๋ถ ์บ์(Redis ๋ฑ) ํตํฉ
- ์ฝ๊ธฐ ๋น๋๊ฐ ๋์ ๋ฐ์ดํฐ ์บ์ฑ
- ์ ์ ํ ์บ์ ๋ฌดํจํ ์ ๋ต ๊ตฌํ
- ๋ง์ด๊ทธ๋ ์ด์
๊ด๋ฆฌ
- Alembic ๋ฑ ์ ๋ฌธ ๋๊ตฌ ํ์ฉ
- ๋ฒ์ ๊ด๋ฆฌ ์์คํ ๊ณผ ํตํฉ
- ๋ง์ด๊ทธ๋ ์ด์ ํ ์คํธ ์๋ํ
- ๋ค์ด๊ทธ๋ ์ด๋ ๊ฒฝ๋ก ์ ์ง
- ํธ๋์ญ์
์ฒ๋ฆฌ
- ์์์ ์ฐ์ฐ์ ํธ๋์ญ์ ์ฌ์ฉ
- ๋ช ์์ ์ปค๋ฐ/๋กค๋ฐฑ ๊ด๋ฆฌ
- ์์ธ ์ฒ๋ฆฌ์ ๊ฒฐํฉ
- ์ค์ฒฉ ํธ๋์ญ์ ์ดํด