GraphQL 리졸버는 얼마나 게으른가요?
GraphQL 리졸버는 얼마나 게으른가요?
일부 컨텍스트를 위해 여기 내 아키텍처의 새 눈이 있습니다. GraphQL-> Resolvers-> | Domain Boundary | -> 서비스-> 로더-> 데이터 소스 (Postgres / Redis / Elasticsearch)
도메인 경계를 지나면 GraphQL 특정 구성이 없습니다. 서비스 는 도메인의 다양한 차원을 나타내며 리졸버는 간단히 SomeQueryInput을 처리하고 적절한 서비스에 위임 한 다음 작업 결과로 적절한 SomeQueryResult를 구성합니다. 권한을 포함한 모든 비즈니스 규칙은 도메인에 있습니다. 로더 는 데이터 소스에 대한 추상화를 통해 도메인 개체에 대한 액세스를 제공하며 때로는 DataLoader 패턴을 사용하고 때로는 사용하지 않습니다.
시나리오를 사용하여 내 질문을 설명하겠습니다. 프로젝트가있는 사용자가 있고 프로젝트가 많은 문서가 있다고 가정 해 보겠습니다. 프로젝트에는 또한 많은 사용자가 있으며 일부 사용자는 모든 문서를 볼 수 없습니다.
현재 사용자가 볼 수있는 모든 문서를 검색하는 스키마와 쿼리를 구성 해 보겠습니다.
type Query {
project(id:ID!): Project
}
type Project {
id: ID!
documents: [Document!]!
}
type Document {
id: ID!
content: String!
}
{
project(id: "cool-beans") {
documents {
id
content
}
}
}
Assume the user state is processed outside of the GraphQL context and injected into the context.
그리고 일부 해당 인프라 코드 :
const QueryResolver = {
project: (parent, args, ctx) => {
return projectService.findById({ id: args.id, viewer: ctx.user });
},
}
const ProjectResolver = {
documents: (project, args, ctx) => {
return documentService.findDocumentsByProjectId({ projectId: project.id, viewer: ctx.user })
}
}
const DocumentResolver = {
content: (parent, args, ctx) => {
let document = await documentLoader.load(parent.id);
return document.content;
}
}
const documentService => {
findDocumentsByProjectId: async ({ projectId, viewer }) {
/* return a list of document ids that the viewer is eligible to view */
return getThatData(`SELECT id FROM Documents where projectId = $1 AND userCanViewEtc()`)
}
}
따라서 쿼리 실행은 다음과 같습니다. 프로젝트를 해결하고, 뷰어가 볼 수있는 문서 목록을 가져오고, 문서를 해결하고, 해당 콘텐츠를 해결합니다. DocumentLoader가 매우 일반적이고 비즈니스 규칙과 무관하다고 상상할 수 있습니다. 유일한 작업은 ID의 개체를 가능한 한 빨리 가져 오는 것입니다.
select * from Documents where id in $1
내 질문은 documentService.findDocumentsByProjectId를 중심으로합니다. 여기에는 여러 가지 접근 방식이있는 것 같습니다. 현재 서비스에는 GraphQL 지식이 구워 져 있습니다. 필요한 객체의 "스텁"을 반환하여 적절한 객체로 해석 될 것임을 알고 있습니다. 이는 GraphQL 도메인을 강화하지만 서비스 도메인을 약화시킵니다. 다른 서비스가이 서비스를 호출하면 쓸모없는 스텁이 생깁니다.
findDocumentsByProjectId가 다음을 수행하지 않는 이유는 무엇입니까?
SELECT id, name, content FROM "Documents" JOIN permisssions, etc etc
이제 서비스가 더 강력하고 전체 비즈니스 객체를 반환하지만 GraphQL 도메인은 더욱 취약 해졌습니다. GraphQL 스키마가 서비스가 예상하지 않은 방식으로 쿼리되는 더 복잡한 시나리오를 상상할 수 있습니다. 누락 된 데이터. 당신은 또한 당신이 작성한 리졸버를 지울 수 있습니다. 대부분의 서버는 이미 하이드레이트 된 객체를 간단하게 해결하기 때문입니다. REST 엔드 포인트 접근 방식으로 한 발 뒤로 물러났습니다.
또한 두 번째 방법은 특정 목적을위한 데이터 소스 인덱스를 활용할 수있는 반면, DataLoader는 WHERE IN 유형의 접근 방식을보다 무차별 적으로 사용합니다.
이러한 우려의 균형을 어떻게 잡습니까? 나는 이것이 아마도 큰 질문이라는 것을 이해하지만 그것은 내가 많이 생각하고있는 것입니다. 도메인 모델에 여기서 유용 할 수있는 개념이 누락되어 있습니까? DataLoader 쿼리는 범용 ID를 사용하는 것보다 더 구체적이어야합니까? 나는 우아한 균형을 찾기 위해 고군분투합니다.
현재 내 서비스에는 findDocumentStubs 및 findDocuments가 모두 있습니다. 첫 번째는 리졸버에서 사용하고, 두 번째는 GraphQL 해상도에 의존 할 수 없기 때문에 다른 내부 서비스에서 사용하지만, 이것 역시 옳다고 생각하지 않습니다. DataLoader 일괄 처리 및 캐싱을 사용하더라도 누군가가 불필요한 작업을하는 것처럼 느껴집니다.
답변
이와 같은 리졸버를 작성하는 경우
function resolveFullName ({ first_name, last_name }) => {
return `${first_name} ${last_name}`;
}
그럼 당신은 틀림없이 일을 잘못하고 있습니다.
이 경우 효과적으로 수행하는 작업은 도메인 논리를 도메인 계층에서 가져와 API 계층에 삽입하는 것입니다. 당신이 당신의 데이터베이스를 설계하기위한 모범 사례를 다음과 같은 경우, 다음 데이터 계층이되어 가는 직접 소비 할 수없는 정규화 엉망이 될 수 있습니다. 비즈니스 규칙을 적용하고 해당 데이터를 애플리케이션의 다른 부분에서 사용할 수있는 형태로 변환하는 것이 도메인 계층의 역할입니다.
당신은 다음과 같이 썼습니다.
당신은 또한 당신이 작성한 리졸버를 지울 수 있습니다. 대부분의 서버는 이미 하이드레이트 된 객체를 간단하게 해결하기 때문입니다. REST 엔드 포인트 접근 방식으로 한 발 뒤로 물러났습니다.
공정한 평가라고 생각하지 않습니다. 여전히 GraphQL을 활용하여 서비스에서 반환 된 다양한 도메인 객체를 단일 그래프로 결합합니다. 클라이언트 애플리케이션은 여전히 API에 단일 요청을하고 필요한 모든 데이터를 가져올 수 있습니다. 수행중인 작업에 대해 REST와 같은 것은 없습니다.
문제가 데이터베이스 쿼리를 최적화하는 것이라면 확실히 더 복잡한 DataLoader 패턴을 활용하여 해당 목표를 달성 할 수 있습니다. 서비스에 의해 노출되는 메서드는 또한 필드 배열을 인수로 허용 할 수 있으므로 도메인 개체를 "수화"할 때 어떤 열을 선택하고 어떤 조인을 만들지에 대해 더 선택적으로 선택할 수 있습니다. GraphQL 리졸버는 네 번째 매개 변수로 전달 된 GraphQLResolveInfo 객체에서이 필드 배열을 쉽게 파생 할 수 있습니다.
(일부 조사 후 @Daniel의 권장 사항 중 일부를 종합 한 후 내 질문에 답변)
몇 가지 기준에 맞는 컬렉션 가져 오기를 중심으로하는 귀하의 핵심 관심사를 해결하려고합니다. 당신이 느끼는 마찰은 문서 ID 컬렉션을 가져온 다음 그 문서의 나머지 필드를 해결하기 위해 비슷한 쿼리를 만들어서 발생합니다. 처음에는 이것이 중복 된 작업이라고 생각하는 것이 합리적이라고 생각합니다. 특히 GraphQL을 처음 접하는 경우 : 첫 번째 쿼리에서 데이터베이스에서 필요한 모든 필드를 열심히 가져 오지 않은 이유는 무엇입니까? 그럴만 한 이유가 있습니다.
우리가 필요로하는 "알고있는"문서 데이터를 열심히 가져 왔다고 가정 해 봅시다. ProjectResolver에서 ID 목록을 가져오고 DocumentResolver에서 다시 가져 와서 문서를 해결하는 대신 ProjectResolver에서 모든 것을 열심히 가져옵니다. GraphQL 서버가 문서 필드를 간단하게 해결하도록합니다. 이것은 잘 작동하는 것 같지만 문서 해결의 부담을 프로젝트 해결사로 옮겼습니다. 필드가 생성 된 사용자 유형을 추가하겠습니다 .Documents : [Document!] !.
type User {
id: ID!
name: String!
createdDocuments: [Document!]!
}
User에서 생성 된 문서를 질의하면 어떻게됩니까? UserResolver가 문서 데이터를 가져 오지 않는 한 도움이되는 것은 없습니다 . 부모가 자녀를위한 유일한 데이터 소스가되도록 허용함으로써 미래의 모든 부모가 똑같이하도록 강제합니다. 이로 인해 GraphQL API가 깨지기 쉽고 유지 및 확장이 어렵습니다. ProjectResolver를 지연시키고 최소값 만 반환 한 다음 DocumentResolver가 문서와 관련된 모든 작업을 수행하도록 강제하면이 문제가 발생하지 않습니다.
DB 로의 두 번의 왕복에서 여전히 가려운 느낌이 있습니다. DataLoader에 더 많이 의존하고 캐시 프라이밍을 사용하여 중간 경로를 취할 수 있습니다. 페이스 북 JS Dataloader에의 구현 로더의 캐시에 씨앗 데이터를 수있는 방법이라고 프라임 ()를 가지고있다. 여러 DataLoader를 사용하는 경우 서로 다른 컨텍스트에서 동일한 개체를 참조하는 여러 로더가있을 수 있습니다. (프런트 엔드 작업에 Apollo Client를 사용하는 경우 익숙 할 것입니다.) 한 컨텍스트에서 일부 객체를 가져올 때 사후 처리 단계로 다른 컨텍스트에 대해 프라이밍하십시오.
프로젝트에 대한 문서 목록을 가져올 때 내용도 열심히 가져 오되 그 결과를 사용하여 DocumentLoader를 준비하십시오. 이제 DocumentResolver가 시작되면이 모든 데이터가 준비되지만 미리 가져온 결과가없는 경우에도 자급 자족 할 수 있습니다. 애플리케이션의 요구 사항에 따라이 작업을 수행 할 때 최선의 판단을 내려야합니다. Daniel Rearden의 제안을 사용하고 GraphQLResolveInfo를 사용하여 이와 같이 사전 가져 오기를 조건부로 결정할 수도 있지만 마이크로 최적화를 수행하는 잡초에 갇히지 않도록하십시오.
ProjectDocumentsLoader와 DocumentLoader라는 두 개의 DataLoader가있는 시나리오를 상상해보십시오. ProjectDocumentsLoader는 결과를 사후 처리 단계로 사용하여 DocumentLoader를 프라임 할 수 있습니다. 저는 사전 및 사후 처리를 처리하기 위해 DataLoader를 경량 추상화로 래핑하는 것을 좋아합니다.
class Loader {
load(id) {
let results = await this.loader.load(id)
return this.postProcess(results);
}
postProcess(data) {
return data;
}
prime(key, value) {
this.dataLoader.prime(key, value);
}
}
class ProjectDocumentsLoader extends Loader {
constructor(context) {
this.context = context;
this.loader = new DataLoader(/* function to get collection of documents by project */);
}
postProcess(documents) {
documents.forEach(doc => this.context.documentLoader.prime(doc.id, doc));
return documents;
}
}
class DocumentLoader extends Loader {
constructor(context) {
this.context = context;
this.loader = new DataLoader(/* function to get documents by id */);
}
}
따라서 최종 답변 : GraphQL 리졸버는 최적화이고 진실의 출처가 아닌 한 프리 페칭 옵션과 함께 매우 게 으르어야합니다.