GraphQL 도구를 사용하여 GraphQL 실시간 조회 리소스 식별자 수집

사진은 Łukasz Nieścioruk 에서 Unsplash
GraphQL 라이브 조회는 GraphQL 구독보다 우아한 방식으로 실시간 업데이트를 해결할 수 있습니다.
구독 이벤트가 아닌 주요 구독 데이터 변경 사항을 실시간으로 조회합니다.
실시간 조회는 수동으로 클라이언트 저장소를 업데이트하지 않고 신기하게 클라이언트 저장소를 업데이트합니다. 불필요한 캐시 업데이트 논리가 필요하지 않습니다.
You can learn more about the differences here
그러나 이러한 모든 장점은 서버가 반드시 상태가 있는 단점으로 바뀌어야 한다는 데 수반된다. 특히 클라이언트의 조작이 소모하는 모든 데이터를 이해하고 밑바닥 데이터가 변화할 때 특정 클라이언트를 위해 이러한 조회 조작을 다시 수행해야 한다.
GraphQL 실시간 조회를 처음 시도할 때 가장 간단한 해결 방안은 Query 대상 유형의 루트 필드를 바탕으로 실시간 조회의 재실행을 촉발하는 것이다.E, g. 실시간 조회를 통해 이벤트 발사기 Query.viewer 이벤트를 저장하고 Query.viewer 필드에서 선택 집합이 있는 조회를 다시 실행할 수 있습니다.단, 주어진 조회 조작을 사용하는 모든 클라이언트에 대해 뷰어는 완전히 다른 기록/자원일 수 있다.
더 명확한 것은 여기에 상응하는 모델이 있다는 것이다.
type User {
  id: ID!
  login: String!
}

type Query {
  """
  Returns the authenticated user. Returns null in case the user is not authenticated.
  """
  viewer: User
  """
  List of the users that are currently online.
  """
  onlineUsers: [User!]!
}

type Mutation {
  updateLogin(newLogin: String!): Boolean!
}

query viewer @live {
  viewer {
    id
    login
  }
}
이 실현이 어떤 모습인지 봅시다.
const Query = {
  viewer: (source, args, context) => {
    return context.viewer;
  },
};

const Mutation = {
  updateLogin: async (source, args, context) => {
    await context.db.updateUser(
      context.viewer.id,
      args.newLogin
    );

    context.liveQueryStore.invalidate(
      `Query.viewer`
    );
    return true;
  },
};
특정 사용자가 로그인을 업데이트하면 실시간 조회 동작을 무효화하고 다시 실행해서는 안 됩니다. 이 동작은 연결된 사용자에게 뷰어 선택을 설정합니다. 이 사용자들은 심지어 이 변경의 영향을 받지 않을 수도 있습니다!

또한 사용 가능한 모든 사용자의 목록 Query.onlineUsers 과 같은 다른 작업에서도 참조할 수 있습니다.Query.viewer 이벤트는 적용되지 않으며 이 필드를 통해 사용자의 작업 재실행을 선택할 계획입니다.

선택 집합 데이터를 고유하게 식별할 수 있는 더 좋은 해결 방안이 있어야 한다


알 수 있는 바와 같이 사용자는 id (비공id) 형식의 ID! 필드를 가지고 있습니다.이것은 클라이언트의 자원을 유일하게 표시하는 데 사용되는 일반적인 필드입니다.Apollo client는 __typename 필드와 id 필드를 결합하여 기본 자원 캐시 키(User:1로 사용합니다. Relay는 더 나아가 자원 형식이 id 내에서 인코딩되었다고 가정합니다(예: base64("User:1")참고:). 따라서 id 필드만 사용합니다.
만약 우리도 실시간 조회 저장이 실현된 서버에서 이런 식별자를 사용할 수 있다면?
나의 현재 실현은 단지 조회 조작의 AST를 두루 훑어보았을 뿐이고 루트 조회 유형의 schema coordinates을 추출했다.E, g.Query.viewer는 위의 viewer 실시간 조회 작업에 사용됩니다.

그러나 만약에 우리가 id를 통해 사용자를 식별하고 싶다면, 우리는 실시간으로 선택한 자원 집합에 유사한 User:1 내용을 추가해야 한다.이것은 패턴 지식이 필요합니다. 실시간 검색 저장소는 어떤 유형이 id 필드를 가지고 있는지 알고 선택 집합에 포함되면 해당하는 자원 식별자를 수집해야 하기 때문입니다.

위에서 말한 바와 같이, 이것은 더욱 세분화된 조회가 무효가 되도록 허용한다.

내가 생각한 첫 번째 단점은 선택 집합에 지정한 id 필드가 없으면 실시간 조회 저장소에서 자원을 추적할 수 없다는 것이다.
그러나 클라이언트에서 캐시 키를 사용할 가능성이 높기 때문에 대부분의 작업에서 id 필드를 선택할 수 있습니다.
또한 검색을 간단하게 변환하여 id 필드를 선택 집합에 추가할 수 있습니다. (apollo 클라이언트와 같이 기본적으로 모든 대상 유형에 __typename 선택을 추가합니다.
간단하게 보기 위해서, 나는 id 필드를 선택한 책임을 실시간 조회 작업을 보내는 클라이언트에게 미루기로 결정했다.기존 응용 프로그램에서 자원 선택이 없는 용례를 찾을 수 없습니다 id.👍.

리소스 식별자 컬렉터 구현


다음 장애는 ID를 추출하는 방법을 결정하는 것입니다. 두 가지 선택이 있습니다.

1. GraphQL 실행 결과 트리 반복


이것은 나에게 매우 복잡한 것 같다. 왜냐하면 나는 전체 결과를 두루 훑어보고 AST 조작과 모델에 따라 어떤 방식으로 모든 잎의 유형을 추측/검사해야 하기 때문이다.나는 곧 그 생각을 포기했다.

2. 상하문을 통해 주입된 함수를 호출하여 자원 식별자를 수동으로 등록한다


나의 실시간 조회 저장은 최소한의 작업량으로 모든 모델에 실시간 조회 지원을 추가하는 것을 목표로 한다.검색 해상도에서 라이브러리 사용자가 호출해야 하는 상하문을 전달하는 것은 잘못된 것 같습니다. 이 모든 것은 라이브러리 사용자가 관심을 가지지 말아야 할 세부 사항입니다.
만약 우리가 모든 반환 대상 유형의 해상도에 자원을 수동으로 등록해야 한다고 상상해 보세요.
const Query = {
  viewer: (source, args, context) => {
    const viewer = context.viewer;
    context.registerResource(`User:${viewer.id}`);
    return viewer;
  },
};
단일 해상도에 대해 말하자면, 이것은 매우 간단해 보일 수도 있지만, 만약 우리가 수동으로 모든 해상도에 있는 자원에 대해 이 조작을 실행해야 한다면, 그것은 곧 혼란스러워지고 오류를 초래할 수 있다.
이상적인 상황에서 라이브러리 사용자는 context.liveQueryStore.invalidate("User:1") 변이 필드 해상도에 updateLogin 줄을 추가하기만 하면 신기하게 작업의 재실행을 스케줄링할 수 있으며, 모든 해상도에 추가 함수 호출을 추가할 필요가 없다.
const Query = {
  viewer: (source, args, context) => {
    // No tracking registration code here.
    return context.viewer;
  },
};

const Mutation = {
  updateLogin: async (source, args, context) => {
    await context.db.updateUser(
      context.viewer.id,
      args.newLogin
    );

    context.liveQueryStore.invalidate(
      `User:${context.viewer.id}`
    );
    return true;
  },
};
그래서 나는 어떻게 그렇게 지루하지 않은 방식으로 이 점을 실현할 수 있을지 더 많이 생각했다.
다른 필드와 마찬가지로 id 필드에는 충돌 해결 프로그램(GraphQL이 제공하는 기본 충돌 해결 프로그램이나 사용자 정의 충돌 해결 프로그램)이 있기 때문에 함수로 각각id 필드 충돌 해결 프로그램을 포장하는 방법이 있으면 이 문제를 해결할 수 있습니다.포장기는 실제 해상도를 호출하여 자원을 등록하고 값을 되돌려줍니다.사용자는 쿼리의 선택 집합에 id 필드를 추가하는 것 외에 어떤 일에도 관심을 가질 필요가 없다.
GraphQL 모드를 변환하고 수정하는 데 가장 적합한 라이브러리는 graphql-tools 입니다.다행히도, 그것은 지금 길드에 의해 유지되고 있다. 왜냐하면 아폴로가 그것을 포기했고, 상당히 엉망으로 유지되었기 때문이다.
그래서 나는 기이한 문서를 깊이 연구했고 곧 내가 필요로 하는 것을 찾았다. @graphql-tools/wrap.
문서의 빠른 발췌문:

Schema wrapping is a method of making modified copies of GraphQLSchema objects, without changing the original schema implementation.


모드도'정상'조회/돌연변이/구독 작업에 사용되기 때문이다.비활성 조회 조작을 위해 모든 id 필드의 비용을 포장하고 싶지 않습니다.TransformObjectFields 변환을 사용하면 포장 모드 필드가 간단합니다.
import {
  GraphQLSchema,
  isScalarType,
  isNonNullType,
  GraphQLOutputType,
  GraphQLScalarType,
  execute,
} from "graphql";
import { wrapSchema, TransformObjectFields } from "@graphql-tools/wrap";

const isNonNullIDScalarType = (
  type: GraphQLOutputType
): type is GraphQLScalarType => {
  if (isNonNullType(type)) {
    return isScalarType(type.ofType) && type.ofType.name === "ID";
  }
  return false;
};

const addResourceIdentifierCollectorToSchema = (
  schema: GraphQLSchema
): GraphQLSchema =>
  wrapSchema(schema, [
    new TransformObjectFields((typename, fieldName, fieldConfig) => {
      let isIDField = fieldName === "id" && isNonNullIDScalarType(fieldConfig.type);

      let resolve = fieldConfig.resolve;
      fieldConfig.resolve = (src, args, context, info) => {
        if (!context || !context[ORIGINAL_CONTEXT_SYMBOL]) {
          return resolve(src, args, context, info);
        }

        const collectResourceIdentifier = context.collectResourceIdentifier;
        context = context[ORIGINAL_CONTEXT_SYMBOL];
        const result = resolve(src, args, context, info);
        if (isIDField) {
          if (isPromise(result)) {
            result.then(
              (value) => collectResourceIdentifier({ typename, value }),
              () => undefined
            );
          } else {
            collectResourceIdentifier({ typename, result });
          }
        }
        return result;
      };

      return fieldConfig;
    }),
  ]);
다음과 같은 작업을 수행합니다.
const newIdentifier = new Set(rootFieldIdentifier);
const collectResourceIdentifier: ResourceGatherFunction = ({ typename, id }) =>
  // for a relay spec conform server the typename could even be omitted :)
  newIdentifier.add(`${typename}:${id}`);

// You definitely wanna cache the wrapped schema as you don't want to re-create it for each operation :)
const wrappedSchema = addResourceIdentifierCollectorToSchema(schema);

const result = execute({
  schema: wrappedSchema,
  document: operationDocument,
  operationName,
  rootValue,
  contextValue: {
    [ORIGINAL_CONTEXT_SYMBOL]: contextValue,
    collectResourceIdentifier,
  },
  variableValues: operationVariables,
});
사용자 컨텍스트를 컨텍스트에 패키지화해야 합니다(컨텍스트 이상).🤯) 자원 식별자를 자원 식별자 집합에 추가하는 함수도 추가했습니다.나는apollo 서버 원본 코드의 계발을 받았다. 왜냐하면 해상도의 실행 시간을 측정하는 방법이 있기 때문에 자원 식별자 집합과 같은 요청/조작을 바탕으로 해야 한다는 것을 알고 있기 때문이다.이 방법은 실행할 때마다 새로운 함수/상하문을 사용할 수 있습니다.필드 해상도에서 정확한 사용자 상하문은 실제 (사용자) 필드 해상도로 전달됩니다.
현재 모드에 대한 조작을 실행한 후 newIdentifier 집합은 조작 실행 기간에 해석된 모든 자원에 대한 식별자를 포함해야 한다.
자원 식별자 이벤트를 보내면 실시간 조회 저장소에서 이 정보를 사용하여 조회를 다시 실행할 수 있습니다👌.

결론


검색 루트 필드가 아닌 자원을 바탕으로 자원을 식별하고 검색을 무효화시키면 더욱 효과적으로 검색을 다시 실행할 수 있고 클라이언트에게 불필요한 업데이트를 전송하는 것을 피할 수 있다.
GraphQL 도구는 매우 편리한 라이브러리로 다양한 문제를 해결하는 데 사용된다.나는 그것이 이렇게 거대한 업데이트와 좋은 문서를 얻게 되어 매우 기쁘다.
실현은 모든 용례를 덮어쓰지 않을 수도 있습니다.클라이언트가 인증을 거치지 않은 경우 Query.viewer 해상도 null 를 반환하면 어떻게 합니까?사용자가 인증을 통과하면 실시간 조회 저장 작업 상하문에는 User:ID 문자열이 사용할 수 없습니다.라이브querystore 송신기를 통해 Query.viewer 업데이트를 보내야 합니다. (이것은 선택viewer의 모든 클라이언트 작업에 영향을 줄 수 있습니다.) 클라이언트는 로그인한 후에 다시 작업을 수행해야 하거나 어떤 방식으로livequerystore에 인증서를 통과한 사용자의 모든 작업을 다시 실행해야 합니다.
구현된 소스 코드에 관심이 있는 경우 보기https://github.com/n1ru4l/graphql-live-queries/pull/94
라이브queryland에서 더 많은 내용을 발견하고 구축해야 합니다!
우리는 여전히 실시간 조회 저장소에 수동으로 통지해야 하며, 자원은 반드시 효력을 상실해야 한다.서로 다른 창고에 대해 백그라운드에서 이 조작을 실행하는 추상은 크게 다를 수 있다.
ORM/데이터베이스 메모리 층에서 이벤트를 보낼 수 있거나 에이전트는 데이터베이스 작업(예: INSERT, DELETEUPDATE을 기반으로 이벤트를 보낼 수 있습니다.
검색 작업을 다시 실행하는 것은 예쁘고 똑똑하지만 가장 효과적인 해결 방안은 아니다.만약 우리가 어떤 해상도만 다시 실행할 수 있다면?나는 이미 약간의 생각을 가지고 있다. 나는 이 점과 그에 관한 글을 쓸 수 있을 것이다.
실시간 조회나 GraphQL에 관심이 있다면 언제든지 트위터에 연락하거나 아래에 댓글을 남겨주세요🙂. 관심 있는 분들과 이 글을 공유하는 것도 고려해 주세요.😉.

좋은 웹페이지 즐겨찾기