From REST to GraphQL 정보
From REST to GraphQL본문
https://0x2a.sh/from-rest-to-graphql-b4e95e94c26b#.snl6pkgkj
From REST to GraphQL
면책 조항 : GraphQL은 여전히 새롭고 모범 사례는 여전히 나타나고 있습니다. 이 포스트는 GraphQL 백엔드 서비스 구현과 관련된 몇 가지 여정을 설명합니다. 그래서 지금까지 배운 내용을 다른 사람들에게 유용할 수 있기를 기대하면서 보여줍니다. 또한 Playlist 내부의 특정 실제 구현 세부 정보가 명백한 이유로 바꿔서(paraphrased)/단순화되거나(simplified)/익명으로(anonymized) 처리되었습니다.
이 글은 GraphQL에 대한 기본 지식을 전제로합니다. GraphQL에 익숙하지 않은 사용자:
GraphQL: A data query language
REST
Playlist에는 앱에 필요한 Rails / REST 기반 API가 있습니다. 초기에 Github의 V3 API를 사용하여 영감을 얻어 API 구조를 모델링했습니다.
트랙정보가 필요합니까?
GET /tracks/ID
재생 목록을 가져오는 것이 필요합니까?
GET /playlists/ID
재생 목록의 트랙이 필요하십니까?
GET /playlists/ID/tracks
단순성의 이점이 있습니다. 종점은 직관적으로 이름이 지정되며 쉽게 찾아 볼 수 있습니다. 처음에는 모든 객체에 URL 속성을 구현하여 클릭만으로 API를 탐색 할 수있게되었습니다. (결과적으로 더 작은 응답 페이로드를 사용하여 결국 제거되었습니다.) 문서는 각 엔드 포인트에서 반환 한 내용을 설명하므로 모바일 팀이 쉽게 통합 할 수 있습니다.
Bloat and Slowdowns
그러나 시간이 지남에 따라 요구 사항이 커짐에 따라 페이로드가 커졌습니다. 예를 들어, 다음은 단순한 재생 목록 개체 응답입니다.
{
"created_at": "2015-08-30T00:50:25.000+00:00",
"id": "e66637db-13f9-4056-abef-f731f8b1a3c7",
"like_count": 3,
"liked_count": 3,
"name": "Excuse me while I kiss these frets",
"owner": {
"avatar_url": "https://secure.gravatar.com/avatar/4ede0ad35bb796ea8f78861acc4372ca?s=300",
"bio": null,
"id": "b06e671a-b169-45e6-a645-74c31abca910",
"login": "playlistrock",
"name": "Playlist Rock",
"site_admin": false
},
"published": false,
"saved_count": 3,
"track_count": 50,
"updated_at": "2015-09-30T06:11:49.000+00:00"
}
여기에는 재생 목록에 대한 모든 기본 정보가 있지만 관련 개체는 거의 없습니다. 클라이언트로서 / 재생 목록 / ID / 트랙과 같은 다른 끝점을 호출하여 하위 리소스를 가져올 것으로 예상됩니다.
더 많은 연결이 추가되면서 더 많은 데이터가 계속 재생 목록 응답에 포함되었습니다. 특히 Rails와 ActionView 부분을 사용했기 때문에 더 많은 데이터가 필요한 재생 목록 목록으로 _playlist.json.jbuilder 부분에 더 많은 데이터가 추가되었습니다.
모바일 요구 사항은 "/users/USERNAME/playlists 을 호출하기보다는" 사용자 프로필을 표시 할 때 각 사용자 재생 목록에 처음 세 개의 태그를 표시해야합니다. "라고 말한 다음 /playlists/ID/tags가 반환 된 각 재생 목록마다 한 번씩 표시되면 태그가 재생 목록 부분에 추가됩니다.
{
"created_at": "2015-08-30T00:50:25.000+00:00",
"genres": [],
"id": "e66637db-13f9-4056-abef-f731f8b1a3c7",
"like_count": 3,
"liked_count": 3,
"name": "Excuse me while I kiss these frets",
"owner": {
"avatar_url": "https://secure.gravatar.com/avatar/4ede0ad35bb796ea8f78861acc4372ca?s=300",
"bio": null,
"id": "b06e671a-b169-45e6-a645-74c31abca910",
"login": "playlistrock",
"name": "Playlist Rock",
"site_admin": false
},
"published": false,
"saved_count": 3,
"tags": [
{
"name": "Jimi Hendrix"
},
{
"name": "Jimmy Page"
},
{
"name": "Eric Clapton"
},
{
"name": "Slash"
},
{
"name": "Stevie Ray Vaughan"
}
],
"track_count": 50,
"updated_at": "2015-09-30T06:11:49.000+00:00"
}
결국 우리는 /playlists/ID 응답을 위해 다음과 같은 것을 얻게됩니다.
{
"collaborators": [],
"created_at": "2015-08-30T00:50:25.000+00:00",
"genres": [],
"id": "e66637db-13f9-4056-abef-f731f8b1a3c7",
"like_count": 3,
"liked": true,
"liked_count": 3,
"name": "Excuse me while I kiss these frets",
"owner": {
"avatar_url": "https://secure.gravatar.com/avatar/4ede0ad35bb796ea8f78861acc4372ca?s=300",
"bio": null,
"id": "b06e671a-b169-45e6-a645-74c31abca910",
"login": "playlistrock",
"name": "Playlist Rock",
"site_admin": false
},
"published": false,
"saved": true,
"saved_count": 3,
"tags": [
{
"name": "Jimi Hendrix"
},
{
"name": "Jimmy Page"
},
{
"name": "Eric Clapton"
},
{
"name": "Slash"
},
{
"name": "Stevie Ray Vaughan"
}
],
"track_count": 50,
"tracks": [
{
"album": {
"id": "8d8223c6-284c-4aac-92bd-b31debca3237",
"title": "Toys In The Attic"
},
"artists": [
{
"id": "6c29ff27-ad20-4448-9961-f6617e393539",
"name": "Aerosmith"
}
],
"explicit": false,
"have_liked": false,
"id": "a1f9f37a-2a15-407d-82f8-e742ab5e3b81",
"title": "Walk This Way"
},
{
"album": {
"id": "21a9f63b-a38f-40f1-aaf1-8b7ed3ad1a92",
"title": "Audioslave"
},
"artists": [
{
"id": "7d600588-d073-41e9-a4f7-434501b16c45",
"name": "Audioslave"
}
],
"explicit": false,
"have_liked": false,
"id": "4cc1fc43-61e8-49a7-be42-9d7ad35c1284",
"title": "Like A Stone"
},
...
],
"updated_at": "2015-09-30T06:11:49.000+00:00"
}
여기에는 개별 재생 목록이 나타날 수있는 모든 가능한 장소를 포괄 할 수있는 충분한 데이터가 포함되어 트랙과 심지어는 연관성의 하위 집합이 포함됩니다. 그리고이 데이터는 재생 목록이 나타난 모든 장소에서 반환되었습니다.
이것은 더 많은 엔드 포인트를 추가하는 대신 응답을 늘리는 의식적인 디자인 결정이었습니다. 우리는 / playlists/ID/forProfile, /playlists/ID/forNotifications 등과 같은 것을 할 수있었습니다.
제공하는 단순성에 대해 언급해야 할 것이 있습니다. 예를 들어 트랙에 필드를 추가하려면 _track.json.jbuilder 부분을 찾고 추가 필드를 추가합니다. 그러나 조회수가 늘어남에 따라 실적은 두 가지 방식으로 신속하게 문제가되었습니다.
첫째, 응답 페이로드가 크기 때문에 JSON을 구문 분석, 역 직렬화 및 저장하는 데 소요되는 노력으로 모바일 앱이 어려움을 겪을 수있었습니다. 응답 시간은 길었고, 캐시는 더 커졌으며, 작은 부분에 대한 모든 변경 사항이 앱 전체에서 훨씬 더 큰 변화로 확장되었습니다.
둘째, 요청마다 점점 더 많은 데이터 (특히 관계)를 가져올 때 쿼리 성능이 크게 향상되었습니다. 캐싱을 사용할 수 없도록 개발할 때 재생 목록에 대한 단일 요청은 170 개 이상의 데이터베이스 쿼리를 요청하여 모든 관련 정보를 가져올 수 있습니다.
제작 과정에서 우리는 Rails "Russian Doll" 스타일 캐싱을 많이 사용 했으므로 완전히 캐싱 된 재생 목록에는 하나의 데이터베이스 쿼리 만 포함됩니다. 하지만 첫 번째로드에서는 170 개의 쿼리를 실행하여 전체 응답을 작성해야했습니다 (일반적으로 Russian Doll Caching 및 공유 된 하위 리소스 덕분에 더 적은 수).
Caching with Rails: An Overview - Ruby on Rails Guides
가장자리 위로 우리를 밀어 붙인 것은 위의 have_liked 필드였다. 이것은 현재 인증 된 사용자가 트랙을 좋아하는지 여부를 나타내는 부울 필드입니다. 제품 요구 사항에 따르면이 필드는 재생 목록 세부 정보보기에서 액세스 할 수 있어야 하므로 각 트랙의 재생 목록 응답에 포함되어야했습니다.
이것은 Russian Doll Caching을 망가 뜨렸습니다.
_track.json.jbuilder 부분은 트랙에 대한 "static"정보를 포함하는 캐시 된 부분과 current_user.have_liked?(track)에 대한 호출을 포함하는 캐시되지 않은 부분의 조합이되었습니다. 이어서, _playlist.json.jbuilder와 트랙 부분을 참조한 모든 뷰는 유사하게 변형되어 캐시 된 부분과 캐시되지 않은 부분을 포함합니다.
50 개의 트랙이있는 재생 목록 요청의 경우 50 개의 통화에서 have_liked가 더 나쁩니까? 실행되었습니다 (N+1 쿼리 버그).
개별 종단점을 위한 별도의 하위 리소스 뷰 파일, 추가 쿼리 수를 줄이기위한 사용자 지정 쿼리 캐시 관리 등 여러 가지 가능한 솔루션이 있었습니다. 그러나 두 가지 문제를 모두 해결하고 보다 강력한 제어가 가능한 솔루션을 원했습니다.
GraphQL
GraphQL로 들어오세요. 우리의 백엔드에 전력을 공급하기 위해 GraphQL을 사용하여 추가 요청없이 각각의 요청에 필요한 모바일 클라이언트를 정확하게 제공 할 수 있었고 모든 것을 매우 효율적으로 수행 할 수 있도록 데이터베이스와 캐시 계층을 최적화 할 수 있었습니다.
특정 세부 사항에 들어가기 전에, GraphQL에 대해 배우면서 자주 접하게되거나 경험 한 몇 가지 일반적인 질문 / 오해가 있습니다.
Common Questions / Misconceptions
GraphQL은 그래프처럼 들립니다. 내 데이터가 "그래프" 가되어야합니까, 아니면 "그래프" 데이터베이스가 필요합니까? 관계형 데이터베이스에서도 작동합니까?
아니요, 그래프 데이터베이스가 필요하지 않습니다. 데이터베이스가 있으면 상관 없습니다.
IMO에서는 "그래프"와 관련하여 거의 모든 "관계형"데이터베이스를 생각할 수 있습니다.
user --- OWNS --- playlist
| |
LIKES CONTAINS
| |
v |
track <---------------┘
GraphQL은 트리와 같은 데이터를 설명하고 가져옵니다.
user
┖-OWNS-> playlist
┖-CONTAINS-> track
┖-LIKED_BY-> users
그래프 데이터베이스, 관계형 데이터베이스, 메모리 내 배열, 키 - 값 저장소 등을 사용할 수 있습니다. Playlist에서 우리는 Neo4j를 전체 그래프 모드에서 작동하는 "기본"데이터베이스로 사용하고 Redis는 해시, 키 - 값 쌍 및 세트를 포함한 다양한 데이터 구조를 사용하는 캐시 계층의 역할을합니다. Redis는 본질적으로 Neo의 데이터를 유형별로 ID 및 ZSET의 키 - 값 저장소로 나타내며 Facebook의 TAO 모델을 밀접하게 반영합니다.
이를 통해 Cypher 쿼리의 모든 기능을 갖춘 Neo의 신뢰할 수있는 데이터 소스를 만들 수 있지만 모든 쿼리의 90 %에 대해 메모리 내장 키 - 값 저장소의 성능을 제공합니다.
GraphQL은 "쿼리 언어"처럼 들리지만 나는 클라이언트에서 데이터베이스에 쿼리하는 기능을 제공합니다. 이것은 위험한 것으로 들립니다. 악의적 인 클라이언트는 어떻습니까?
아니요, REST API를 사용했을 때보 다 데이터베이스 쿼리를 클라이언트에 노출시키지 않습니다. 좋아, 아마도.
GraphQL은 독자적인 백엔드 데이터 페치 로직의 최상위에있는 DSL입니다. 데이터베이스에 직접 연결하지 않습니다. 실제로 GraphQL에 노출 된 스키마는 데이터베이스를 정확히 미러링하지 않습니다. 구조화 된 데이터에 대한 요청을 설명하는 방법을 제공하지만 요청을 이행하는 것은 백엔드에 달려 있습니다.
한 가지 우려는 GraphQL이 "중첩 된"인출을 지원하기 때문에 악의적 인 클라이언트가 특정 재귀 중첩 관계를 임의로하지만 여러 번 (예 : user.followers.followers ...) 요청하면 백엔드에서 성능이 저하 될 수 있다는 것입니다. 이 위험을 완화하는 방법에 대한 몇 가지 아이디어는 마지막 섹션을 참조하십시오.
그렇다면 GraphQL은 데이터베이스에 대한 인증되지 않은 액세스를 제공하지 않습니까?
아니요. 인증은 GraphQL 외부에서 처리 될 가능성이 가장 높으며 백엔드는 이전에 REST로 수행 한 것처럼 데이터 페치 / 인증을 안전하게 처리해야합니다.
새로운 GraphQL 백엔드에서는 GraphQL 외부의 인증을 요청 헤더로 전달하고 서버가 요청을 인증 한 다음 인증 컨텍스트를 GraphQL 데이터 확인자에게 전달하도록합니다.
Playlist에서 Graphql 백엔드를 "transport-agnostic"으로 만들고 싶습니다. 따라서 일반 HTTP처럼 데이터를 포착하거나 비 HTTP 와이어 프로토콜을 통해 데이터를 요청할 수 있습니다. MQTT와 같은 실시간 데이터 변경을 위해 일종의 실시간 스트리밍 업데이트를 구현하는 것도 멋지다. 따라서 우리는 GraphQL 요청 자체에 인증 토큰 또는 사용자 이름 / 비밀번호 쌍을 포함시키는 것으로 간주했지만 아직까지는 이러한 경로를 완전히 조사하지는 못했습니다.
What about security?
다시 말하지만, 이것은 백엔드에 달려 있으며 GraphQL의 주된 관심사는 아닙니다. 아래에 인증 된 해결 프로그램 (데이터를 반입하고 반환하는 함수)이 표시됩니다. 볼 권한이없는 대상에 액세스하려는 클라이언트를 처리하는 두 가지 주요 접근 방법이있는 것 같습니다.
우선, 요구 된 필드에 대해서 null를 돌려줍니다. 이는 특정 데이터 집합을 요구할 때 실제 피해가 없으며이를 부정하는 데 실제적인 해가없는 경우에 효과적입니다.
좋은 예는 백엔드가 해당 사용자에게 사용자의 전자 메일 만 제공하는 사용자의 전자 메일을 요구하는 것입니다. 이메일 필드가 포함 된 내 사용자 개체를 요청하면 내 이메일을 받게됩니다. 다른 사용자 객체를 요청하면 전자 메일은 null이되며 해당 응용 프로그램을 코딩하여 해당 null이 될 수 있습니다.
둘째, 실제 오류를 반환합니다. 이는 데이터를 요구하는 클라이언트가 요청 된 데이터를 제공하지 않은 이유를 알아야 해당 정보에 대해 조치를 취할 수있는 경우에 가장 효과가있는 것으로 보입니다.
좋은 예가 인증이 필요한 객체에 액세스하려고 시도했지만 인증이 제공되지 않았습니다.
"404s"는 보통 null로 반환됩니다. 규칙에 따라 (Github의 API 에서처럼) 인증되지 않은 객체는 null로 반환되는 경우가 있습니다. 예를 들어 사용자가 현재 인증 된 사용자를 차단했을 때 사용자 프로필을 요청한 경우와 같습니다. null은 404를 모방하고 숨겨진 사용자가 있다는 사실을 누설하지 않습니다.
Github 저장소가 혼란스럽습니다! 어느 것이 실제로 GraphQL입니까?
facebook/graphql은 특정 언어 / 백엔드에 묶이지 않은 GraphQL 언어 및 구현 사양입니다. 특히 언어를 이해하고 개념이나 이론을 파헤쳐 가장 잘 배우는 것이 좋습니다.
graphql/graphql-js는 JS / Node로 쓰여진 페이스 북에서 제공하는 스펙의 레퍼런스 구현이다. 노드 기반 백엔드에서 GraphQL을 사용하거나 그냥 놀고 싶은 경우 시작하는 곳입니다. 필자가 아는 한, 이것은 공식 참조 구현이 다소간의 사양의 가장 완벽한 구현입니다. README를 읽으십시오.
graphql/express-graphql은 Express.js를 위한 미들웨어로서 Express와 함께 GraphQL 서버를 쉽게 생성 할 수 있습니다. 끔찍하게 오래 걸리지 않고, 이해하기 쉽고, express-graphql을 직접 사용하지 않아도 graphql-js를 사용하는 방법을 설명 할 수 있기 때문에 전체 소스 코드를 읽는 것이 좋습니다.
graphql/graphlq-relay-js는 Relay 호환 ID와 "연결"(일대 다 연관 또는 배열 필드)을 구현하는 헬퍼 세트입니다. GraphQL을 사용할 필요는 없지만 Relay 호환이 가능합니다. Relay를 사용하지 않고 ID 처리, 페이지 매김 등을 통해 우리에게 도움이되었습니다. Relay GraphQL 사양에 대한 자세한 내용은 Relay 문서를 참조하십시오.
graphql/graphiql은 GraphQL을위한 웹 기반 IDE입니다. 이 일은 대단한 괴물입니다. GraphQL은 스키마 인트로 스펙 션을 제공하며, GraphiQL은 이러한 인트로 스펙 션 기능을 사용하여 자동 완성 및 구문 검증을 제공합니다. 이 프로젝트를 직접 다운로드하거나 앱에 임베드하거나 즐겨 찾는 앱을 skevy/graphiql-app의 Electon 기반 래퍼에 독립형 앱으로 다운로드 할 수 있습니다.
facebook/dataloader는 재생 목록 백엔드에서 데이터 가져 오기에 혁명을 일으킨 유틸리티 모듈입니다. 이 기반은 매우 간단합니다. 현재 실행 프레임 (이벤트 루프 틱)에서 load() 호출 인수를 수집 한 다음 사용자 정의 제공된 논리를 사용하여 수집 된 인수를 기반으로 데이터를 일괄 적으로 패치합니다. 아래의 DataLoader 사용 방법에 대해 자세히 알아보십시오.
graphql/swapi-graphql은 기존 SWAPI를 GraphQL 서버로 노출시키는 프로젝트의 예입니다. 그것은 graphql-js, express-graphql, GraphiQL, DataLoader를 사용합니다.
chentsulin/awesome-graphql은 GraphQL 리소스, 프로젝트, 게시물 등에 대한 링크 모음입니다. 확인 해 보세요!
What is Relay? Do I need Relay too?
Relay는 GraphQL과 React를 지능적인 방법으로 연결하기위한 프레임 워크입니다. GraphQL을 이용하려면 Relay가 필요하지 않습니다. React를 사용하고 있다면 체크 아웃하십시오. 앱에서 유용 할 수도 있습니다.
Relay는 GraphQL 쿼리 디자인에서 작동을 지원하기 위해 몇 가지 특별한 규칙이 필요합니다. 우리는 Relay 자체를 사용하지 않지만 Playlist에서 Relay와 호환되도록 결정했습니다. 이것은 ID에 의한 페칭 및 연관들의 콜렉션의 표현 및 페이지 매김을위한 일관된 API를 제공합니다. 릴레이 문서에는 자세한 정보가 있습니다.
Is GraphQL only for React?
아닙니다. 이전에 HTTP / REST를 사용한 곳이라면 어디에서나 사용할 수 있습니다.
Playlists and Tracks in GraphQL
GraphQL을 사용하여 위의 재생 목록 끝점에서 성능 문제를 해결할 수있는 방법에 대해 알아 보겠습니다. 필요한 데이터 만 반환하고 데이터베이스 쿼리를 최적화하여 N+1 버그를 피할 수 있습니다.
우리의 GraphQL 쿼리는 다음과 같습니다:
query FetchPlaylist {
playlist(id: "e66637db-13f9-4056-abef-f731f8b1a3c7") {
id
name
tracks {
id
title
viewerHasLiked
}
}
}
간단하게하기 위해 재생 목록 ID가 쿼리에 포함되었지만 실제로는 ID를 쿼리에 포함하는 대신 형식화 된 매개 변수로 전달합니다. 자세한 정보는 GraphQL 문서를 참조하십시오.
우리는 인증이 GraphQL 외부에서 발생했고 우리의 해결자가 접근 할 수 있도록 GraphQL 호출의 rootValue 객체에 인증 상태가 제공되었다고 가정합니다. rootValue에 대한 자세한 내용은 graphql-js 및 express-graphql 문서를 참조하십시오.
먼저 쿼리의 진입 점인 루트 쿼리 개체를 정의해야합니다. 루트 쿼리 개체에는 playlist라는 필드가 있어야합니다. 위의 쿼리에서 제공하고있는 필드입니다.
import {
GraphQLObjectType,
GraphQLNonNull,
GraphQLString
} from 'graphql';
import playlistType from './playlistType';
export default new GraphQLObjectType({
name: 'Query',
description: 'The root query object',
fields: () => ({
playlist: {
type: playlistType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: (_, { id }, { rootValue: { ctx: { backend } } }) => (
backend.getModel('Playlist').load(id)
)
}
})
});
여기서 ES6 구문을 사용하고 있습니다. 우리는 모든 최신 ES7 자료를 활용하기 위해 0 단계로 설정된 바벨을 사용합니다.
우리는 재생 목록 유형(다른 파일에서 정의하고 여기에서 가져 오는 GraphQL 유형 정의)을 반환하는 필드를 정의하고, null이 아닌 string 유형의 id라는 단일 인수를 설정 한 다음 가장 중요한 것은 객체를 확인하는 함수를 정의한다는 것입니다.
해결할 첫 번째 인수는 현재 객체 자체입니다 (루트 수준이므로이 인수는 무시합니다). 두 번째 인수는 GraphQL 호출에 전달 된 args이므로 id 필드를 추출합니다. 세 번째 인수는 GraphQL 컨텍스트에 대한 액세스를 제공하므로 app의 다른 위치에서 rootValue에서 전달한 백엔드 인스턴스를 추출하고이를 사용하여 ID로 재생 목록을 가져옵니다.
그것은 간단합니다! 데이터베이스에서 재생 목록을로드하고 JS 객체를 반환하면이 단계에서 완료됩니다.
다음으로 재생 목록 스키마 유형을 정의 해 보겠습니다.
import {
GraphQLString,
GraphQLArray,
GraphQLObjectType,
} from 'graphql';
import trackType from './trackType';
export default new GraphQLObjectType({
name: 'Playlist',
description: 'A Playlist',
fields: () => ({
id: {
type: GraphQLString,
resolve: it => it.uuid
}
name: { type: GraphQLString },
tracks: {
type: new GraphQLArray(trackType),
resolve: it => it.tracks()
}
})
});
여기에서는 Playlist에 대한 새로운 객체 유형을 정의합니다. 루트 쿼리 분석기가 재생 목록 모델 인스턴스를 반환 했으므로이 수준 (이름이 지정된)의 해결 함수에 대한 첫 번째 인수는 해당 인스턴스입니다. 따라서 id 필드의 경우 it.uuid를 호출하여 이름 id 아래에 uuid 모델 필드를 표시하여 해결할 수 있습니다. GraphQL 스키마는 데이터베이스 스키마를 미러링 할 필요가 없다는 것을 기억하십시오.
name 필드의 경우 x라는 필드의 기본값이 model.x이므로 resolver를 제공하지 않습니다.
트랙의 경우 모델에서 it.tracks ()를 호출하여 데이터베이스에서 트랙을 로드합니다.
참고 : 모든 필드에 대해 확인 함수가 있지만 각 필드를 가져 오는 데 개별 데이터베이스 쿼리가 필요하다는 의미는 아닙니다. root.playlist에서 그다지 많거나 적게 가져올 수 없으므로 각 서브 필드 확인자는 부모가 이미 가져온 것을 반환하거나 필요에 따라 추가 쿼리를 실행할 수 있습니다.
마지막으로 트랙에 대한 GraphQL 객체 유형을 정의 해 보겠습니다.
import {
GraphQLString,
GraphQLBoolean,
GraphQLObjectType
} from 'graphql';
export default new GraphQLObjectType({
name: 'Track',
description: 'A Track',
fields: () => ({
id: {
type: GraphQLString,
resolve: it => it.uuid
}
title: { type: GraphQLString },
viewerHasLiked: {
type: GraphQLBoolean,
resolve: (it, _, { rootValue: { ctx: { auth } } }) => (
(auth.isAuthenticated) ? it.userHasLiked(auth.user) : null
)
}
})
});
이전과 마찬가지로 id 및 title 필드를 간단한 확인자로 정의합니다. 또한 필드 뷰어 HasLiked를 추가하고 인증을 확인합니다. 사용자가 인증되지 않은 경우 null을 반환합니다. 그렇지 않으면 track.userHasLiked ()를 현재 인증 된 사용자와 호출합니다. 다시, Auth 객체는 Express 미들웨어의 GraphQL 외부에서 실행됩니다.
Playlist.load ()가 재생 목록을로드하면 playlist.tracks()는 데이터베이스에서 해당 재생 목록의 트랙 배열을 로드하고 track.userHasLiked()는 데이터베이스와 사용자와 트랙 간의 연결 존재 여부를 쿼리하고, 우리의 GraphQL 쿼리는 올바르게 해석 될 것이고, 우리가 다른 필드를 정의하면 REST API의 기능을 본질적으로 복제했습니다. 여기서는 간결하게하기 위해 생략했습니다.
이는 REST API의 두 가지 문제 중 하나를 해결합니다. 이제 클라이언트는 필요한 데이터 만 요청할 수 있으며 다양한 방식으로 모바일 앱 성능에 도움이됩니다. 그러나 N+1 쿼리 문제는 여전히 남아 있습니다.이 재생 목록의 50 개 트랙 모두에 대해 viewerHasLiked를 요청하면 50 개의 검색어가 표시됩니다. 우리는 Facebook에서 DataLoader라는 매우 독창적인 npm 모듈을 사용하여 이를 해결했습니다.
DataLoader FTW
DataLoader는 실행 프레임 (이벤트 루프 틱)에서 load()에 대한 모든 호출을 통합 한 다음 콜렉션 콜을 기반으로 데이터를 일괄 적으로로드하는 API를 제공합니다. 또한 키를 사용하여 결과를 캐시하므로 같은 인수를 사용하여 load()를 호출하면 이후에 직접 캐시됩니다.
따라서 실행 프레임에서 myDataLoader.load(id)를 여러 번 호출하면 해당 프레임이 완료되면 데이터 로더에 모든 ID의 배열이 제공되고 요청 된 데이터를 일괄 적으로로드 할 수 있습니다. DataLoader의 작동을 더 잘 이해하기 위해 README를 읽는 것이 좋습니다.
우리의 경우 track.userHasLiked()는 대량의 사용자와 트랙 사이의 관계를 해결하기 위해 설계된 DataLoader 인스턴스에 대한 호출로 모델링 할 수 있습니다. 아래와 같은:
import DataLoader from 'dataloader';
import BaseModel from './BaseModel';
const likeLoader = new DataLoader((requests) => {
// requests is now a an array of [track, user] pairs.
// Batch-load the results for those requests, reorder them to match
// the order of requests and return.
})
export default class Track extends BaseModel {
userHasLiked(user) {
return likeLoader.load([this, user]);
}
}
이 코드를 사용하면 likeLoader.load()를 50 회 호출하면 일괄로드 함수가 한 번 호출됩니다. 즉, GraphQL 쿼리가 52 개가 아닌 3 개의 데이터베이스 쿼리를 실행합니다.
DataLoader README에 표시된대로 우리는 DataLoader 인스턴스를 데이터베이스 쿼리 수준까지 구성하여 한 걸음 더 나아가게됩니다.
예를 들어 사용자 이름으로 사용자를 가져 오려면 다음과 같이합니다.
- batchQueryLoader - 데이터베이스 쿼리를 수락하고 성능 향상을 위해 일괄 / 병렬 기능을 사용하여 데이터베이스에 대해이를 실행하고 결과를 반환하는 캐싱을 사용하지 않는 DataLoader입니다.
- userByIDLoader - ID를 받아들이고, batchQueryLoader를 사용하여 데이터베이스를 쿼리하고 사용자 객체를 반환하는 DataLoader입니다.
- userByUsernameLoader - 사용자 이름을 허용하고 batchQueryLoader를 사용하여 데이터베이스에 사용자 ID를 쿼리 한 다음 userByIDLoader를 호출하여 사용자 객체를 반환하는 DataLoader입니다.
이 DataLoader 컴포지션을 사용하면 다른 모든 DataLoader에서 사용하는 batchQueryLoader가 데이터베이스 작업을 일괄 처리하고 대기 시간을 줄일 수 있습니다. 그리고 userByUsernameLoader가 ID를 확인한 다음 userByIDLoader를 호출하면 userByIDLoader가 공유 캐시가 되어 전체 쿼리가 줄어 듭니다. 설정에서 우리는 파이프 라인을 사용하여 Redis 용 DataLoader를 추가하고 이를 캐싱 레이어로 다른 로더에 통합하여 쿼리 시간을 줄였습니다.
또한 앞서 언급했듯이 DataLoader는 load() 인수로 결과를 캐싱합니다. 이러한 사실 때문에 우리는 각 요청에 대해 DataLoader를 초기화하므로 단일 요청이 지속되는 동안 데이터가 캐시 된 다음 요청이 완료된 후 삭제됩니다.
이 아키텍처를 사용하면 처음부터 요청 된 전체 재생 목록 (170 개의 검색어와 15 개 정도의 렌더링)이 데이터베이스 쿼리가 3 개만 있으면 약 250ms, Redis 캐시에서는 약 17ms의 데이터를 읽습니다. 이렇게하면 두 가지 성능 문제가 해결됩니다.
Future Puzzles
앞으로 우리가 해결하고자하는 몇 가지 사항을 살펴 보겠습니다.
Mutations (Writes)
Google의 GraphQL 서버는 전체 API 표면에 대한 읽기 기능을 제공하지만 쓰기는 아직 구현되지 않았습니다. graphql-js는 GraphQL 돌연변이를 처리하기위한 쉬운 DSL을 제공하므로 곧 GraphQL 시스템에 쓰기를 통합 할 예정입니다. 이것은 간단한 작업 인 것으로 보이지만 구현에서 통찰력이나 모범 사례가 나오는지 발견하는 것은 흥미로울 것입니다.
Client-side Caching
우리는 아직 클라이언트에서 GraphQL 응답 캐싱을 해결해야합니다. 이론적으로 GraphQL 종점에서 데이터를 가져 오는 시스템은 스키마 인트로 스펙 션을 사용하여 기본 스키마를 이해하므로 하위 리소스를 지능적으로 캐시 할 수 있으므로 한 위치에서 모델을 업데이트하면 어디에서나 업데이트됩니다. TTL, 강제 업데이트 등과 같은 추가 고려 사항을 구현해야합니다.
올바르게 이해한다면 Relay는 이러한 우려 사항 중 일부를 해결할 수 있습니다. 그러나 Relay는 여전히 새롭고 현재 React Native를 지원하지 않으며 원시 코드 환경에서 실행되지 않습니다.
Real-time or Push Updates
우리 플랫폼의 여러 측면이 "실시간"이며, 이러한 측면을 GraphQL 백엔드에 통합하여 특정 데이터 집합에 실시간 "가입"을 허용하는 것이 좋습니다.
Query Performance Protection
주어진 사용자의 추종자와 같은 것을 노출 시키면 이론적으로 악의적 인 클라이언트가 서버가 응답을하기까지 사용자의 요청을 제출할 수 있습니다. 우리는 아직 풀 솔루션을 가지고 있지 않습니다. 특히 GraphQL 끝점을 향후 API에서 공개 API로 공개하기로 결정한 경우 특히 그렇습니다. 탐색 할 수있는 세 가지 경로가 마음에 듭니다.
- 스키마 AST 검사를 수행하여 쿼리가 너무 "복잡하지"않은지 확인하고 임계 값을 초과하는 쿼리를 거부합니다.
- "시간 초과" 쿼리의 일부 형식을 사용하여 해결하기에는 너무 오래 걸리는 요청을 삭제하고 단일 요청이 데이터베이스를 쿼리하는 속도를 제한합니다.
- Facebook에서 메모를 작성하고 쿼리가 캐시에 저장되고 클라이언트가 본질적으로 화이트 리스팅 쿼리 인 전체 쿼리를 전달하지 않고 프로덕션에서 ID로 참조하는 "쿼리 캐시"를 구현하십시오. 이것은 GraphQL API가 내부 클라이언트 전용 인 경우에만 작동합니다.
Conclusion
결론적으로 GraphQL은 매우 훌륭하며 Playlist에서 실제 문제를 해결해 왔습니다. 우리에게는 과대 광고 라기보다는 다른 사람들이 이해하는 데 도움이 될 수 있기를 기대하면서 일부 결과를 공유하고자했습니다. 최첨단 기술과 프로젝트는 재미 있지만 때로는 이해하고 적용하기 어려울 수 있습니다.
한 가지 더 -이 동영상을 확인하십시오. 파이낸셜 타임즈 (Financial Times)에서 GraphQL과 실제 구현의 이점을 이해하는 데 많은 도움이되었습니다.
질문, 의견, 조언 등이 있으시면 Twitter @jacobwgillespie에 또는 *** 개인정보보호를 위한 이메일주소 노출방지 ***을 통해 연락하십시오.
!-->!-->!-->!-->!-->!-->!-->!-->!-->!-->
0
댓글 0개