2020/04/13 - [Back-end/Python] - [GraphQL] 무작정 시작하기 (1) - Schema & Query
2020/04/14 - [Back-end/Python] - [GraphQL] 무작정 시작하기 (2) - Mutation
0. 서론
일반적으로 데이터 목록을 조회할 때, 목록 전체를 조회하는 것이 아니라 페이지 단위로 조회한다. GraphQL에서는 Cursor기반의 Connection으로 강력한 Pagination 기능을 제공하는데, 필자처럼 GraphQL을 시작한지 얼마안된 초보자라면 거부감이 들것이다. 그래서, ConnectionField대신 기본적인 ObjectField를 이용하여 Pagination을 구현해보았다.
그래서 이번 포스트에서는 ObjectField를 이용한 Pagination을 다루어볼 계획이다. 추가로, Pagination을 처리하기 위해서는 Query와 함께 Variables를 받아서 처리해야하므로, 변수를 입력받고 처리하는 방법에 대해서도 함께 알아보도록 하겠다.
1. 프로젝트 준비
1-1. 프로젝트 구조
1) [ app.py ]
: Flask Web Application 실행 파일.
2) [ api/__init__.py ]
: Flask Web Application 설정 파일.
3) [ api/database.py ]
: MongoDB 연결 및 기초 데이터 셋팅 파일.
4) [ api/models.py ]
: MongoDB의 Document 구조를 정의하는 파일.
5) [ api/query.py ]
: GraphQL에서 데이터를 조회하기위한 Field들을 정의하는 파일.
6) [ api/schema.py ]
: GraphQL의 구조(Schema)를 정의하는 파일.
7) [ api/types.py ]
: GraphQL에서 Database에 접근하는 객체를 정의하는 파일.
1-2. 디버깅 도구 설치
1) Altair GraphQL Client
: https://altair.sirmuel.design/
: GraphQL Server 디버깅 도구인데, graphene에서 기본적으로 제공하는 것보다 UI가 깔끔하고 여러개의 Widnow를 사용할 수 있어서 편함.
: 직접 설치하는 것보단 Chrome이나 Safari의 확장 프로그램으로 사용하는 것을 추천.
2. 기본 소스 작성
- 이번 포스트에서 중요하게 봐야할 부분은 'query.py' 파일이다. 나머지 파일들은 첫번째 포스트에서 작성한 내용을 그대로 사용하므로 자세한 설명은 해당 포스트를 참고하길 바란다.
2-1. api/database.py
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
|
# api/datdabase.py
import datetime
from mongoengine import connect
from api.models import RankModel
MONGO_DTATBASE="graphql-example"
MONGO_HOST="mongomock://localhost"
# Database 연결
conn = connect(MONGO_DTATBASE, host=MONGO_HOST, alias="default")
print( conn.server_info() )
# 기초 데이터 Insert 함수
def init_db():
# 1000개의 데이터 생성
for idx in range(1000):
RankModel(
name="heo",
mode="4x4",
score=idx,
is_mobile=False,
reg_dttm=datetime.datetime.now().strftime("%Y%m%d%H%M%S")
).save()
|
cs |
2-2. api/models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# api/models.py
from mongoengine import Document
from mongoengine.fields import (
StringField, IntField, BooleanField
)
# MongoDB Document 객체 정의
class RankModel(Document):
meta = {
'collection': 'rank_list'
}
mode = StringField(description='2048 grame ranking.')
name = StringField()
score = IntField()
is_mobile = BooleanField()
reg_dttm = StringField()
upd_dttm = StringField()
|
cs |
2-3. api/types.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# api/types.py
import datetime
from graphene_mongo import MongoengineObjectType
from .models import RankModel
# MongoDB에서 데이터를 조회하는 GraphQL 객체 정의
class RankType(MongoengineObjectType):
class Meta:
model = RankModel
# reg_dttm을 출력할 때, 처리하는 로직
def resolve_reg_dttm(parent, info, **input):
return datetime.datetime.strptime(parent.reg_dttm, "%Y%m%d%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
# upd_dttm을 출력할 때, 처리하는 로직
def resolve_upd_dttm(parent, info, **input):
if parent.upd_dttm is not None:
return datetime.datetime.strptime(parent.upd_dttm, "%Y%m%d%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
else:
return parent.upd_dttm
|
cs |
2-4. api/schema.py
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# api/schema.py
import graphene
from .types import RankType
from .query import Query
# Schema 생성
schema = graphene.Schema(
query=Query,
types=[
RankType
]
)
|
cs |
2-5. api/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# api/__init__.py
from flask import Flask
from flask_graphql import GraphQLView
from api.schema import schema
def create_app():
# Flask Application 생성
app = Flask(__name__)
# /graphql EndPoint 설정
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view(
'graphql',
schema=schema,
graphiql=True # GraphQL UI 제공
)
)
return app
|
cs |
2-6. app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# app.py
import dotenv
from api import create_app
from api.database import init_db
if __name__ == '__main__':
# 환경변수 설정
dotenv.load_dotenv(dotenv_path=".env")
# MongoDB 접속 및 기초 데이터 입력
init_db()
# Flask App 실행
app = create_app()
app.run(host="localhost", port=3000)
|
cs |
2-7. .env
1
2
3
|
# .env
FLASK_ENV=devlopment
FLASK_DEBUG=true
|
cs |
3. Query 소스 작성
3-1. api/query.py
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
|
# api/query.py
import graphene
from .models import RankModel
from .types import RankType
# 조회 조건의 인자값을 Json 형태로 받기위한 객체 정의
class InputPagination(graphene.InputObjectType):
page = graphene.Int(default_value=1)
count_for_rows = graphene.Int(default_value=10)
# Query Field 객체 정의
class Query(graphene.ObjectType):
# 전체 랭킹 목록 필드 설정, 반환 결과( List )
rank_list = graphene.List(
RankType,
mode = graphene.String(required=True, default_value=None),
name = graphene.String(),
order=graphene.List(graphene.String),
pagination=InputPagination()
)
# 전체 랭킹 목록
def resolve_rank_list(parent, info, mode=None, name=None, pagination=None, **input):
# 선택 조건 처리
cond = dict()
if name is not None:
code["name"] = name
# 정렬 처리
order = input.get("order") if "order" in input else list()
# 페이징 처리
if pagination is not None:
page = pagination.page if pagination.page > 0 else 1
count_for_rows = pagination.count_for_rows if pagination.count_for_rows > 0 else 10
skip = (page-1) * count_for_rows
return RankModel.objects(mode=mode, **cond).order_by(*order).skip(skip).limit(count_for_rows)
return RankModel.objects(mode=mode, **cond).order_by(*order)
|
cs |
1) [ 8~10 ln ] - 조회 조건의 인자값을 Json형태로 받기위한 객체 정의.
: 현재 페이지를 의미하는 'page' 정수형 변수.
: 페이지당 보여줄 목록의 수를 의미하는 'count_for_rows' 정수형 변수.
2) [ 14~49 ln ] - 데이터 조회를 위한 Query Field 정의.
: [ 16~22 ln ] - 반환 결과가 List인 'rank_list' 필드를 정의.
: [ 17 ln ] - 리스트 안의 데이터 타입 설정.
: [ 18 ln ] - 필수 조건 Field 설정.
: [ 19~20 ln ] - 선택 조건 Field 설정.
: [ 21 ln ] - Json 형태로 입력받을 조건 Field 설정.
: [ 25~42 ln ] - 'rank_list'에 대한 Resolver 정의.
: [ 27~29 ln ] - 선택 조건은 값이 'None'인 경우, 조회 조건에서 제외.
: [ 32 ln ] - 선택 조건은 Keyword Arguments에서 가져와서 사용 가능.
: [ 36~39 ln ] - 페이징 처리를 위한 연산.
: [ 40 ln ] - MongoDB에서 skip과 limit를 이용하여 페이징처리된 조회된 결과를 반환.
: [ 42 ln ] - 페이징처리가 되지않은 결과를 반환.
* Mongoengine에서 'sort_by'함수는 인자값으로 컬럼명을 나열하며 기본적으로 오름차순(Ascending, ASC)으로 정렬하고, 컬럼명 앞에 '-'를 붙일경우 내림차순(Descending, DESC)으로 정렬함.
예) .order_by("-score")
4. Pagination Query 테스트
4-1. Query 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
query PaginationQuery(
$mode: String!
$name: String
$order: [String]
$pagination: InputPagination
){
rankList(
mode: $mode
name: $name
order: $order
pagination: $pagination
) {
mode
name
score
}
}
|
cs |
1) [ 1~6 ln ] - Query Operation 설정.
: [ 1 ln ] - Operation Name 설정.
: [ 2~5 ln ] - 변수 설정( $변수명:데이터타입[필수여부] )
: [ 2 ln ] - 변수명 mode는 데이터타입이 문자열인 필수조건으로 설정.
: [ 3 ln ] - 변수명 name은 데이터타입이 문자열인 선택조건으로 설정.
: [ 4 ln ] - 변수명 order는 데이터타입이 문자열 배열인 선택조건으로 설정.
: [ 5 ln ] - 변수명 pagination은 데이터타입이 'InputPagination'인 선택조건으로 설정.
: [ 7~12 ln ] - 'rank_list' 필드 조회 및 조건 적용.
: GraphQL은 기본적으로 모든 필드에 대해서 Camel-Case 기법이 적용됨.
: [ 13~15 ln ] - 출력 필드 설정.
4-2. Variables 작성
1
2
3
4
5
6
7
8
9
10
|
{
"mode": "4x4",
"order": [
"-score"
],
"pagination": {
"page": 1,
"countForRows": 3
}
}
|
cs |
1) [ 3~5 ln ] - 정렬 변수 설정.
: 'order'는 반환타입이 문자열 배열( [String] )임.
: [ -score ]는 score를 기준으로 내림차순 정렬을 의미.
2) [ 6~9 ln ] - 페이징 변수 설정.
: pagination은 반환타입이 'api/query.py'에서 정의한 객체( InputPagination )임.
: 사전에 정의한 변수 외에 다른 것을 사용할 경우 오류가 발생함.
마치며
- 처음에는 Relay를 이용한 Pagination에 비해서 성능이 어떨지는 모르겠지만, GraphQL을 처음 접했을 때는 ConnectionField보다 이렇게 구현하는 좀 더 접근하기가 쉬울 것 같다.
- 필자도 처음에 이렇게 직접 구현을 해보고나서 Connection을 사용해보았는데, Edge, Node, Connection, Relay 등 공부해야하는 범위가 넓어졌지만, 알고나면 Pagination에 있어서는 Connection를 이용하는 방법이 편하다. 다만, Pagination 이외에 정렬이나 좀 더 세부적인 데이터 조작을 원할 경우에는 다소 복잡해질 수 있다.
- 다음 포스트에서는 Relay와 Connection에 대해서 알아보고 이용한 Connection을 이용한 Pagination을 구현해보도록 하겠다.
'Back-end > Python' 카테고리의 다른 글
[PyQt5] 무작정 시작하기 (1) - 설치 및 실행 (0) | 2020.04.29 |
---|---|
[GraphQL] 무작정 시작하기 (4) - Relay와 Connection이란? (0) | 2020.04.21 |
[GraphQL] 무작정 시작하기 (2) - Mutation (0) | 2020.04.14 |
[GraphQL] 무작정 시작하기 (1) - Schema & Query (0) | 2020.04.13 |
[SMTP] Python으로 메일 발송 하기. (With. 첨부파일 ) (1) | 2020.02.22 |
댓글