포스트

GraphQL - ORM

목차

  1. GraphQL과 관계형 데이터베이스의 조합이 중요한 이유
  2. GrpahQL + ORM 예시

GraphQL과 관계형 데이터베이스의 조합이 중요한 이유

1) 데이터 요청 효율성 향상

  • GraphQL을 사용하면 클라이언트가 필요한 데이터만 정확히 요청할 수 있어 관계형 데이터베이스에서 과도한 데이터 조회나 여러 번의 API 요청을 방지할 수 있습니다.
  • 이는 “오버페칭”과 “언더페칭” 문제를 해결합니다.

2) 스키마 정의와 타입 시스템

  • GraphQL의 강력한 타입 시스템은 관계형 데이터베이스의 스키마와 자연스럽게 매핑됩니다.
  • 이는 데이터 일관성을 유지하고 개발 시 타입 안전성을 제공합니다.

3) 성능 최적화 가능성

  • N+1 문제와 같은 일반적인 GraphQL 성능 이슈는 DataLoader 패턴, 관계형 데이터베이스의 조인 최적화 등을 통해 효과적으로 해결할 수 있습니다.

4) 트랜잭션 지원

  • 관계형 데이터베이스의 트랜잭션 기능은 GraphQL 뮤테이션에서 여러 데이터 변경 작업의 원자성을 보장하는 데 필수적입니다.

GrpahQL + ORM 예시

1) Database 연결

1
2
3
4
5
6
from app.config.database_config import POSTGRESQL_USER, POSTGRESQL_PASSWORD, POSTGRESQL_HOST, POSTGRESQL_PORT, POSTGRESQL_DB
from sqlalchemy import create_engine, orm

DATABASE_URL = f"postgresql+psycopg2://{POSTGRESQL_USER}:{POSTGRESQL_PASSWORD}@{POSTGRESQL_HOST}:{POSTGRESQL_PORT}/{POSTGRESQL_DB}"
engine = create_engine(DATABASE_URL, echo=True)
SessionLocal = orm.sessionmaker(bind=engine)

2) ORM을 이용한 Model 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from sqlalchemy import Column, Integer, String, ForeignKey, Text, Table
from sqlalchemy.orm import declarative_base, relationship
from app.database.database import engine
Base = declarative_base()

book_tags = Table("book_tags",
                  Base.metadata,
                  Column("book_id", Integer, ForeignKey("books.id")),
                  Column("tag_id", Integer, ForeignKey("tags.id")))    

class PublisherModel(Base):
    __tablename__ = "publishers"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String)
    location = Column(String)
    published_year = Column(Integer)
    books = relationship("BookModel", back_populates="publishers")
    
class BookModel(Base):
    __tablename__ = "books"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String)
    author = Column(String)
    publisher_id = Column(Integer, ForeignKey("publishers.id"))
    publishers = relationship("PublisherModel", back_populates="books")
    reviews = relationship("ReviewModel", back_populates="books")
    tags = relationship("TagModel", secondary=book_tags, back_populates="books")
    
class ReviewModel(Base):
    __tablename__ = "reviews"
    id = Column(Integer, primary_key=True, index=True)
    content = Column(Text)
    rating = Column(Integer)
    book_id = Column(Integer, ForeignKey("books.id"))
    books = relationship("BookModel", back_populates="reviews")

class TagModel(Base):
    __tablename__ = "tags"
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)
    books = relationship("BookModel", secondary=book_tags, back_populates="tags")


# 테이블이 없으면 생성
Base.metadata.create_all(bind=engine)

3) graphQL 스키마 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@strawberry.type
class Review:
    id: int
    content: str
    rating: int
    book_id: int

@strawberry.type
class Tag:
    id: int
    name: str
    
@strawberry.type
class Book:
    id: int
    title: str
    author: str
    publisher_id: int
    
    @strawberry.field
    async def reviews(self) -> List[Review]:
        try:
            db = SessionLocal()
            db_reviews = db.query(ReviewModel).filter(ReviewModel.book_id == self.id).all()
            return [Review(id=r.id, content=r.content, rating=r.rating, book_id=r.book_id) for r in db_reviews]
        finally:
            db.close()
    
    @strawberry.field
    async def tags(self) -> List[Tag]:
        try:
            db = SessionLocal()
            book = db.query(BookModel).filter(BookModel.id == self.id).first()
            return [Tag(id=tag.id, name=tag.name) for tag in book.tags] if book else []
        finally:
            db.close()
        
@strawberry.type
class Publisher:
    id: int
    name: str
    location: str
    published_year: int

    @strawberry.field
    async def books(self) -> List[Book]:
        try:
            db = SessionLocal()
            db_books = db.query(BookModel).filter(BookModel.publisher_id == self.id).all()
            return [Book(id=b.id, title=b.title, author=b.author, publisher_id=b.publisher_id) for b in db_books]
        finally:
            db.close()

4) Query 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@strawberry.type
class Query:
    @strawberry.field
    async def books(self) -> List[Book]:
        db = SessionLocal()
        books = db.query(BookModel).all()
        db.close()
        return [Book(id=b.id, title=b.title, author=b.author, publisher_id=b.publisher_id) for b in books]

    @strawberry.field
    async def book(self, title: str) -> Optional[Book]:
        db = SessionLocal()
        find_book = db.query(BookModel).filter(BookModel.title == title).first()
        db.close()
        if find_book:
            return Book(id=find_book.id, title=find_book.title, author=find_book.author, publisher_id=find_book.publisher_id)
        return None

5) Mutation 작성 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@strawberry.type
class Mutation:
    @strawberry.mutation
    async def create_book(self, book_input: BookInput) -> Book:
        db = SessionLocal()
        publisher_model = db.query(PublisherModel).filter(PublisherModel.id == book_input.publisher_id).first()
        if not publisher_model:
            db.close()
            raise ValueError("유효하지 않은 출판사 ID입니다.")
        
        new_book = BookModel(title=book_input.title,
                             author=book_input.author,
                             publisher_id=book_input.publisher_id)
        db.add(new_book)
        db.commit()
        db.refresh(new_book)
    
        new_book = Book(id=new_book.id,
                        title=new_book.title,
                        author=new_book.author,
                        publisher_id=new_book.publisher_id)
        
        await change_event_queue.put(ChangeEvent(entity_type=EntityType.BOOK,
                                                 event_type=EventType.CREATE,
                                                 book=new_book))
        db.close()
        return new_book

    @strawberry.mutation
    async def update_book(self, id: int, update: BookUpdate) -> Optional[Book]:
        db = SessionLocal()
        book_model = db.query(BookModel).filter(BookModel.id == id).first()
        if not book_model:
            db.close()
            return None
        
        if update.title is not None:
            book_model.title = update.title
        if update.author is not None:
            book_model.author = update.author
        if update.publisher_id is not None:
            publisher_model = db.query(PublisherModel).filter(PublisherModel.id == update.publisher_id).first()
            if not publisher_model:
                db.close()
                raise ValueError("유효하지 않은 출판사 ID입니다.")
            book_model.publisher_id = update.publisher_id
            
        db.commit()
        db.refresh(book_model)
        update_book = Book(id=book_model.id,
                           title=book_model.title,
                           author=book_model.author,
                           publisher_id=book_model.publisher_id)
        await change_event_queue.put(ChangeEvent(entity_type=EntityType.BOOK,
                                                 event_type=EventType.UPDATE,
                                                 book=update_book))
        db.close()
        return update_book




다음글에는 만들어진 GraphQL 서버에 Query 하는 방법에 대해서 자세히 설명하겠습니다.