이전 게시글(ORM 활용해보기)와 연결되는 내용입니다.
디렉토리 구조
해당 프로젝트의 디렉토리 구조는 다음과 같다.
tree -L 2 .
.
├── DB
│ └── database.py
├── Models
│ └── model.py
├── Validation
│ └── pydantic_models.py
├── crud.py
└── main.py
1. Model 정의
Models/model.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(30), nullable=False)
fullname = Column(String(40), nullable=False)
nickname = Column(String(20))
posts = relationship("Post", back_populates="author")
comments = relationship("Comment", back_populates="author")
def __repr__(self) -> str:
return f"<User(name='{self.name}', fullname='{self.fullname}', nickname='{self.nickname}')"
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String(40), nullable=False)
content = Column(String(1000), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, default=datetime.now())
author = relationship("User", back_populates="posts")
comments = relationship("Comment", back_populates="post")
def __repr__(self):
return f"<Post(title='{self.title}', content='{self.content}')>"
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
content = Column(String(150), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'))
post_id = Column(Integer, ForeignKey('posts.id'))
created_at = Column(DateTime, default=datetime.now())
author = relationship("User", back_populates="comments")
post = relationship("Post", back_populates="comments")
def __repr__(self):
return f"<Comment(content='{self.content}')>"
back_populates="author"
Post
모델의author
속성을 통해 양방향 관계를 설정함- 이를 통해
User
와Post
간의 양방향 참조가 가능
relationship 함수에 들어가는 인자 설명
back_populates
- 양방향 관계를 정의합니다. 한쪽 모델에서 back_populates로 정의된 속성은 반대쪽 모델에서도 동일하게 설정되어야 합니다.
- 예를 들어, User 모델의 posts와 Post 모델의 author 속성이 서로를 참조하도록 설정할 수 있습니다.
cascade
- 관계된 객체에 대한 일괄 작업을 정의합니다.
- 예를 들어, 부모 객체가 삭제될 때 자식 객체도 함께 삭제되도록 설정할 수 있습니다.
- 사용 예:
cascade="all, delete-orphan"
lazy
- 관계된 객체를 언제 로드할지를 설정합니다.
select
: 기본값으로, 관계된 객체를 사용할 때 쿼리를 실행하여 로드합니다.joined
: 부모 객체를 로드할 때 조인을 사용하여 관계된 객체를 함께 로드합니다.subquery
: 부모 객체를 로드할 때 서브쿼리를 사용하여 관계된 객체를 함께 로드합니다.dynamic
: 쿼리를 반환하여 나중에 필터링하거나 실행할 수 있게 합니다.
primaryjoin
- 관계를 정의하는 두 테이블 간의 조인 조건을 명시적으로 설정합니다.
- 기본적으로 ForeignKey를 기반으로 자동 설정되지만, 복잡한 조건이 필요할 경우 명시적으로 지정할 수 있습니다.
2. 데이터베이스 설정
DB/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from Models.model import Base
engine = create_engine(url="mysql+pymysql://orm:orm@192.168.219.113/orm")
SessionLocal = sessionmaker(bind=engine)
# 데이터베이스 테이블 생성
def init_db() -> None:
Base.metadata.create_all(bind=engine)
3. Pydantic 정의
Validation/pydantic_models.py
from pydantic import BaseModel, field_validator
class UserModel(BaseModel):
name: str
fullname: str
nickname: str | None
@field_validator('name', 'fullname')
def name_must_not_be_empty(cls, value: str) -> str:
if not value or value.strip() == '':
raise ValueError("name and fullname must not be empty")
return value
class PostModel(BaseModel):
title: str
content: str
user_id: int
@field_validator('title', 'content')
def title_and_content_must_not_be_empty(cls, value: str) -> str:
if not value or value.strip() == '':
raise ValueError('Title and content must not be empty')
return value
class CommentModel(BaseModel):
content: str
user_id: int
post_id: int
@field_validator('content')
def content_must_not_be_empty(cls, value: str) -> str:
if not value or value.strip() == '':
raise ValueError('content must not be empty')
return value
4. 데이터 삽입 및 조회 로직 구현
crud.py
from sqlalchemy.orm import Session
from Models.model import User, Post, Comment
from Validation.pydantic_models import UserModel, PostModel, CommentModel
# Create Functions
def create_user(db: Session, user_data: dict) -> User:
user = UserModel(**user_data)
db_user = User(name=user.name, fullname=user.fullname, nickname=user.nickname)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def create_post(db: Session, post_data: dict) -> Post:
post = PostModel(**post_data)
db_post = Post(title=post.title, content=post.content, user_id=post.user_id)
db.add(db_post)
db.commit()
db.refresh(db_post)
return db_post
def create_comment(db: Session, comment_data: dict) -> Comment:
comment = CommentModel(**comment_data)
db_comment = Comment(content=comment.content, user_id=comment.user_id, post_id=comment.post_id)
db.add(db_comment)
db.commit()
db.refresh(db_comment)
return db_comment
# Read Functions
def get_user_by_id(db: Session, user_id: int) -> User:
return db.query(User).filter(User.id == user_id).first()
def get_users(db: Session, skip: int = 0, limit: int = 10) -> list[User]:
return db.query(User).offset(skip).limit(limit=limit).all()
def get_post_by_id(db: Session, post_id: int) -> Post:
return db.query(Post).filter(Post.id == post_id).first()
def get_posts(db: Session, skip: int = 0, limit: int = 10) -> list[Post]:
return db.query(Post).offset(skip).limit(limit).all()
def get_comment_by_id(db: Session, comment_id: int) -> Comment:
return db.query(Comment).filter(Comment.id == comment_id).first()
def get_comments(db: Session, skip: int = 0, limit: int = 10) -> list[Comment]:
return db.query(Comment).offset(skip).limit(limit).all()
# Update functions
def update_user(db: Session, user_id: int, user_data: dict):
db_user = get_user_by_id(db, user_id)
if not db_user:
return None
for key, value in user_data.items():
setattr(db_user, key, value)
db.commit()
db.refresh(db_user)
return db_user
def update_post(db: Session, post_id: int, post_data: dict):
db_post = get_post_by_id(db, post_id)
if not db_post:
return None
for key, value in post_data.items():
setattr(db_post, key, value)
db.commit()
db.refresh(db_post)
return db_post
def update_comment(db: Session, comment_id: int, comment_data: dict):
db_comment = get_comment_by_id(db, comment_id)
if not db_comment:
return None
for key, value in comment_data.items():
setattr(db_comment, key, value)
db.commit()
db.refresh(db_comment)
return db_comment
# Delete Functions
def delete_user(db: Session, user_id: int):
db_user = get_user_by_id(db, user_id)
if not db_user:
return None
db.delete(db_user)
db.commit()
return db_user
def delete_post(db: Session, post_id: int):
db_post = get_post_by_id(db, post_id)
if not db_post:
return None
db.delete(db_post)
db.commit()
return db_post
def delete_comment(db: Session, comment_id: int):
db_comment = get_comment_by_id(db, comment_id)
if not db_comment:
return None
db.delete(db_comment)
db.commit()
return db_comment
5. 메인 애플리케이션 로직
main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from database import SessionLocal, init_db
import crud
import models
import pydantic_models
# FastAPI 인스턴스 생성
app = FastAPI()
# 데이터베이스 초기화
init_db()
# 의존성 주입
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# 사용자 관련 엔드포인트
@app.post("/users/", response_model=pydantic_models.UserModel)
def create_user(user: pydantic_models.UserModel, db: Session = Depends(get_db)):
return crud.create_user(db, user.dict())
@app.get("/users/{user_id}", response_model=pydantic_models.UserModel)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user_by_id(db, user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
@app.get("/users/", response_model=list[pydantic_models.UserModel])
def read_users(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return crud.get_users(db, skip=skip, limit=limit)
# 게시물 관련 엔드포인트
@app.post("/posts/", response_model=pydantic_models.PostModel)
def create_post(post: pydantic_models.PostModel, db: Session = Depends(get_db)):
return crud.create_post(db, post.dict())
@app.get("/posts/{post_id}", response_model=pydantic_models.PostModel)
def read_post(post_id: int, db: Session = Depends(get_db)):
db_post = crud.get_post_by_id(db, post_id)
if db_post is None:
raise HTTPException(status_code=404, detail="Post not found")
return db_post
@app.get("/posts/", response_model=list[pydantic_models.PostModel])
def read_posts(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return crud.get_posts(db, skip=skip, limit=limit)
# 댓글 관련 엔드포인트
@app.post("/comments/", response_model=pydantic_models.CommentModel)
def create_comment(comment: pydantic_models.CommentModel, db: Session = Depends(get_db)):
return crud.create_comment(db, comment.dict())
@app.get("/comments/{comment_id}", response_model=pydantic_models.CommentModel)
def read_comment(comment_id: int, db: Session = Depends(get_db)):
db_comment = crud.get_comment_by_id(db, comment_id)
if db_comment is None:
raise HTTPException(status_code=404, detail="Comment not found")
return db_comment
@app.get("/comments/", response_model=list[pydantic_models.CommentModel])
def read_comments(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return crud.get_comments(db, skip=skip, limit=limit)
마무리
유저 목록 조회해보기
curl 192.168.219.113:8000/users/1
# 출력
{"name":"jaehyo","fullname":"Jaehyo Lee","nickname":"jaehyojjang"}