Azure의 GraphQL: 섹션 4 - 서버 없음 CosmosDB

몇 달 전에 저는Azure 함수에서GraphQL과CosmosDB를 어떻게 결합시켜 사용하는지 토론했기 때문에 이 글은 약간 복제판처럼 느껴질 수 있습니다. 주로 실제 조작의 측면에서 보고 싶다는 점에서 차이가 있습니다.둘 사이의 NET 통합
내가 해결하고 싶은 이유.NET GraphQL과 Azure 기능의 결합은 Function bindings을 활용할 수 있는 독특한 기회를 제공한다는 데 있다.Azure 함수에 익숙하지 않으면, 귀속은 함수를 실행할 때 읽기, 쓰기, 읽기, 쓰기 모드로 다른 서비스에 연결하는 방법입니다.함수가 메모리에 업로드된 후에 일부 메타데이터를 대기열에 기록하는 경우 유용할 수 있습니다.그러나 오늘 장면에서 우리는 HTTP 트리거 함수, 즉GraphQL 단점을 사용하고 데이터베이스 CosmosDB을 사용할 것이다.
왜 우주 데이터베이스입니까?그들이 방금 발표한 consumption plan은 Azure 함수 중의 서버가 없는 GraphQL 호스트의 생각과 잘 어울린다는 것을 감안하면, 나는 이것이 시기적절하다고 생각한다.
비록 우리는 이미 이 시리즈 앞에서 GraphQL을 어떻게 사용하는지 소개했지만, 본문에서 우리는 서로 다른 GraphQL을 사용할 것이다.NET 프레임워크, Hot Chocolate, 그래서 우리가 이전에 보여준 시연은 약간 다른 유형이 있을 수 있지만 이것은 모두 서로 다른 옵션을 탐색하기 위한 것이다.

개시하다
본고를 작성할 때 Hot Chocolate는 Azure를 호스트로 하는 기능을 정식으로 지원하지 않았지만 우리는 proof of concept from a contributor을 기점으로 하여 새로운 Functions project을 만들기 시작했다.
func init dotnet-graphql-cosmosdb --dotnet
그런 다음 프로젝트에 필요한 NuGet 패키지를 추가합니다.
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.3" />
<PackageReference Include="HotChocolate" Version="10.5.2" />
<PackageReference Include="HotChocolate.AspNetCore" Version="10.5.2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.7" />
이 버전들은 본문을 작성할 때 모두 최신이지만, 만약 새로운 버전의 소프트웨어 패키지가 있다면, 그것들을 보기를 원할 수도 있습니다.
입문 작업의 마지막 부분은 개념 검증을 도입하기 때문에 the GitHub repo에서 모든 파일을 가져와 FunctionsMiddleware이라는 프로젝트 다음 폴더에 넣는다.

GraphQL 함수 만들기
뼈대가 완성되면 Functions 프로젝트에 GraphQL 단점을 만들 때가 되었습니다. 이를 위해 HTTP 트리거 함수를 구축합니다.
func new --name GraphQL --template "HTTP trigger"
이렇게 하면 GraphQL 엔드포인트를 사용하도록 구성된 공통 함수를 생성하고 개념 검증의 한 부분을 다시 사용하게 됩니다.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using HotChocolate.AspNetCore;

namespace DotNet.GraphQL.CosmosDB
{
    public class GraphQL
    {
        private readonly IGraphQLFunctions _graphQLFunctions;

        public GraphQL(IGraphQLFunctions graphQLFunctions)
        {
            _graphQLFunctions = graphQLFunctions;
        }

        [FunctionName("graphql")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log,
            CancellationToken cancellationToken)
        {
            return await _graphQLFunctions.ExecuteFunctionsQueryAsync(
                req.HttpContext,
                cancellationToken);
        }
    }
}
이 함수는 더 이상 static이 아니라 구조 함수이며, 이 구조 함수는 매개 변수가 있다는 것을 알 수 있습니다.이를 위해서는 dependency injection for Functions을 구성해야 합니다.

의존 주입 추가
먼저 프로젝트를 위해 Startup이라는 새로운 클래스를 만듭니다.
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(DotNet.GraphQL.CosmosDB.Startup))]

namespace DotNet.GraphQL.CosmosDB
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
        }
    }
}
이 코드에 관해서는 두 가지 주의해야 할 것이 있다. 첫째, 우리는 [assembly: FunctionsStartup(... 프로그램 집합 속성을 가지고 있는데 이것은 Startup류를 가리킨다.이것은 함수가 실행될 때, 프로그램이 시작될 때, 일부 조작을 실행하는 클래스가 있음을 알려 준다.그 다음은 Startup에서 물려받은 FunctionsStartup류다.이 기본 클래스는 Microsoft.Azure.Functions.Extensions NuGet 패키지의 작동 방식과 ASP입니다.NET 핵심 응용 프로그램은 파이프를 시작하고 의존항에 프레임워크에 항목을 추가할 수 있는 방법을 제공합니다.
하지만 GraphQL 모드를 먼저 만들어야 하기 때문에 여기로 돌아갈 것입니다.

GraphQL 모드 만들기
이전 프레젠테이션과 마찬가지로, 우리는 자질구레한 응용 프로그램을 사용할 것이다.
우리는 코스모스DB 저장소에 존재하는 모델부터 시작할 것이다. (나는 이미 OpenTriviaDB의 덤프 파일로 코스모스DB 실례를 채웠으니, 당신은 JSON 덤프 파일 here을 찾을 것이다.)Models이라는 새 폴더를 만들고 QuestionModel.cs이라는 파일을 만듭니다.
using System.Collections.Generic;
using Newtonsoft.Json;

namespace DotNet.GraphQL.CosmosDB.Models
{
    public class QuestionModel
    {
        public string Id { get; set; }
        public string Question { get; set; }
        [JsonProperty("correct_answer")]
        public string CorrectAnswer { get; set; }
        [JsonProperty("incorrect_answers")]
        public List<string> IncorrectAnswers { get; set; }
        public string Type { get; set; }
        public string Difficulty { get; set; }
        public string Category { get; set; }
    }
}
GraphQL이나 코스모스에 특정한 내용이 없는 일반적인 데이터 클래스입니다. (서열화/반서열화를 돕는 속성이 있습니다.) GraphQL 모드를 만들어서 공개해야 합니다.Types이라는 새 폴더와 Query.cs이라는 파일을 만들겠습니다.
using DotNet.GraphQL.CosmosDB.Models;
using HotChocolate.Resolvers;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DotNet.GraphQL.CosmosDB.Types
{
    public class Query
    {
        public async Task<IEnumerable<QuestionModel>> GetQuestions(IResolverContext context)
        {
            // TODO
        }

        public async Task<QuestionModel> GetQuestion(IResolverContext context, string id)
        {
            // TODO
        }
    }
}
이 클래스도 일반적인 C# 클래스이며 Hot Chocolate는 쿼리 모드에서 공개된 유형을 가져오는 데 사용됩니다.우리는 이 클래스에서 두 가지 방법을 만들었는데 하나는 모든 문제를 얻는 데 사용되고 다른 하나는 특정한 문제를 얻는 데 사용되며 이것은GraphQL 모드에 해당할 것이다.
type QuestionModel {
    id: String
    question: String
    correctAnswer: String
    incorrectAnswers: [String]
    type: String
    difficulty: String
    category: String
}

schema {
    query: {
        questions: [QuestionModel]
        question(id: String): QuestionModel
    }
}
모든 방법은 IResolverContext을 사용하지만 모드에 나타나지 않습니다. 이것은 특수한 핫초콜릿 형식으로 해상도 함수의GraphQL 상하문에 접근할 수 있기 때문입니다.
그러나 모델에 빈 속성이 많기 때문에 우리는 이를 원하지 않는다. 그래서 이 문제를 해결하기 위해 우리는 우리가 비치는 모델에 ObjectType을 만들 것이다.QueryType이라는 클래스를 만듭니다.
using HotChocolate.Types;

namespace DotNet.GraphQL.CosmosDB.Types
{
    public class QueryType : ObjectType<Query>
    {
        protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
        {
            descriptor.Field(q => q.GetQuestions(default!))
                .Description("Get all questions in the system")
                .Type<NonNullType<ListType<NonNullType<QuestionType>>>>();

            descriptor.Field(q => q.GetQuestion(default!, default!))
                .Description("Get a question")
                .Argument("id", d => d.Type<IdType>())
                .Type<NonNullType<QuestionType>>();
        }
    }
}
여기서 우리는 IObjectTypeDescription을 사용하여 Query의 필드에 대한 정보를 정의하고 GraphQL 모드에서 내장된 GraphQL 유형 시스템의 공개 유형을 사용하고자 한다.또한 QuestionModel에서 QuestionType
using DotNet.GraphQL.CosmosDB.Models;
using HotChocolate.Types;

namespace DotNet.GraphQL.CosmosDB.Types
{
    public class QuestionType : ObjectType<QuestionModel>
    {
        protected override void Configure(IObjectTypeDescriptor<QuestionModel> descriptor)
        {
            descriptor.Field(q => q.Id)
                .Type<IdType>();
        }
    }
}

GraphQL 모드 사용
해석 프로그램을 실행하기 전에 모드를 응용 프로그램에 연결합니다. 이를 위해 Startup.cs으로 돌아가 조회와 핫초콜릿을 등록합니다.
public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.AddSingleton<Query>();

    builder.Services.AddGraphQL(sp =>
        SchemaBuilder.New()
        .AddServices(sp)
        .AddQueryType<QueryType>()
        .Create()
    );
    builder.Services.AddAzureFunctionsGraphQL();
}
우선 Query을 단일 사례로 등록하면 이를 해결할 수 있고 뜨거운 초콜릿에GraphQL을 추가할 것입니다.모드 등록에서 우리가 사용하는 리셋은 실제 SchemaBuilder을 사용하여 모드를 만들고 의존항에서 용기에 주입하여 사용 가능한 서비스를 등록하고 마지막에 QueryType을 추가하기 때문에GraphQL은 미세한 차이의 유형 시스템을 이해한다.
마지막으로, 우리는 초기에 포함된 개념 검증 코드가 제공한 확장 방법을 호출하여 함수의GraphQL 지원을 등록합니다.

실현 해상도Query 클래스의 해상도에 대해서는CosmosDB에 접근해서 데이터를 추출해야 합니다.코스모스DB 연결을 만들고 의존 주입 프레임워크에 등록할 수 있지만 함수에 입력된 연결을 이용하지 않습니다.
Azure 함수를 사용하면 input binding to CosmosDB을 설정할 수 있습니다. 특히 DocumentClient은 연결 클라이언트의 재사용과 서버가 없는 환경에서 작업할 때 발생할 수 있는 다른 성능 문제를 처리할 수 있습니다.이것이 바로 IResolverContext이 제공한 해석기 상하문이 유용하게 사용될 부분이지만, 우선 상하문에 추가할 수 있도록 개념증명을 약간 수정할 것이다.
먼저 IGraphQLFunctions 인터페이스를 수정하고 ExecuteFunctionsQueryAsync에 새 매개변수를 추가합니다.
Task<IActionResult> ExecuteFunctionsQueryAsync(
    HttpContext httpContext,
    IDictionary<string, object> context,
    CancellationToken cancellationToken);
IDictionary<string, object>은 해상도에 상하문 정보를 임의로 추가할 수 있도록 합니다.현재 우리는 GraphQLFunctions.cs의 실현을 업데이트해야 한다.
public async Task<IActionResult> ExecuteFunctionsQueryAsync(
    HttpContext httpContext,
    IDictionary<string, object> context,
    CancellationToken cancellationToken)
{
    using var stream = httpContext.Request.Body;

    var requestQuery = await _requestParser
        .ReadJsonRequestAsync(stream, cancellationToken)
        .ConfigureAwait(false);

    var builder = QueryRequestBuilder.New();

    if (requestQuery.Count > 0)
    {
        var firstQuery = requestQuery[0];

        builder
            .SetQuery(firstQuery.Query)
            .SetOperation(firstQuery.OperationName)
            .SetQueryName(firstQuery.QueryName);

        foreach (var item in context)
        {
            builder.AddProperty(item.Key, item.Value);
        }

        if (firstQuery.Variables != null
            && firstQuery.Variables.Count > 0)
        {
            builder.SetVariableValues(firstQuery.Variables);
        }
    }

    var result = await Executor.ExecuteAsync(builder.Create());
    await _jsonQueryResultSerializer.SerializeAsync((IReadOnlyQueryResult)result, httpContext.Response.Body);

    return new EmptyResult();
}
인터페이스의 서명에 일치하도록 새로운 파라미터를 추가하는 것이 첫 번째이고, 두 번째는 QueryRequestBuilder을 설정할 때, context 사전에서 순환하고, 각 항목을 해상도 상하문의 속성으로 추가합니다.
마지막으로 CosmosDB의 입력과 연결되도록 함수 자체를 업데이트한 다음 파서에 제공해야 합니다.
[FunctionName("graphql")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    ILogger log,
    [CosmosDB(
        databaseName: "trivia",
        collectionName: "questions",
        ConnectionStringSetting = "CosmosDBConnection")] DocumentClient client,
    CancellationToken cancellationToken)
{
    return await _graphQLFunctions.ExecuteFunctionsQueryAsync(
        req.HttpContext,
        new Dictionary<string, object> {
            { "client", client },
            { "log", log }
        },
        cancellationToken);
}
이렇게 하면 우리는 우리의 해석기를 실현할 수 있다.GetQuestions부터 시작해서 CosmosDB의 모든 문제점을 파악해 보겠습니다.
public async Task<IEnumerable<QuestionModel>> GetQuestions(IResolverContext context)
{
    var client = (DocumentClient)context.ContextData["client"];

    var collectionUri = UriFactory.CreateDocumentCollectionUri("trivia", "questions");
    var query = client.CreateDocumentQuery<QuestionModel>(collectionUri)
        .AsDocumentQuery();

    var quizzes = new List<QuestionModel>();

    while (query.HasMoreResults)
    {
        foreach (var result in await query.ExecuteNextAsync<QuestionModel>())
        {
            quizzes.Add(result);
        }
    }

    return quizzes;
}
IResolverContext을 사용하면 우리는 ContextData을 방문할 수 있다. 이것은 우리가 주입한 속성을 포함하는 사전인데 그 중 하나는 DocumentClient이다.여기서 우리는 CreateDocumentQuery을 사용하여CosmosDB에 대한 조회를 만들고 결과집을 교체해서 되돌아오는 집합으로 보냅니다.
질문을 받으려면 GetQuestion 파서를 사용할 수 있습니다.
public async Task<QuestionModel> GetQuestion(IResolverContext context, string id)
{
    var client = (DocumentClient)context.ContextData["client"];

    var collectionUri = UriFactory.CreateDocumentCollectionUri("trivia", "questions");
    var sql = new SqlQuerySpec("SELECT * FROM c WHERE c.id = @id");
    sql.Parameters.Add(new SqlParameter("@id", id));
    var query = client.CreateDocumentQuery<QuestionModel>(collectionUri, sql, new FeedOptions { EnableCrossPartitionQuery = true })
        .AsDocumentQuery();

    while (query.HasMoreResults)
    {
        foreach (var result in await query.ExecuteNextAsync<QuestionModel>())
        {
            return result;
        }
    }

    throw new ArgumentException("ID does not match a question in the database");
}
이번에는 제공된 ID와 일치하는 항목에 대한 매개 변수 조회를 위해 SqlQuerySpec을 만들었습니다. 또 다른 차이점은 CrossPartitionQueries에서 FeedOptions을 사용해야 한다는 것입니다. id 필드는 partitionKey이 아니기 때문에 필요없을 수도 있습니다. 이것은 CosmosDB 모델 설계에 달려 있습니다.최종적으로 검색이 완료되면 첫 번째 항목을 찾습니다. 존재하지 않으면 이상이 발생합니다. 이 이상은GraphQL에서 오류로 발생합니다.

결론
이 모든 것을 완성한 후에, 우리는Azure 함수에서 실행되는GraphQL 서버를 가지고,CosmosDB 백엔드에 연결되었다. 이 백엔드에서, 우리는 스스로 어떠한 연결 관리도 할 필요가 없다. 이것은 입력 귀속이 책임진다.
제 샘플 on GitHub의 전체 코드를 찾을 수 있습니다.
이것은 읽기 전용 예이지만 GraphQL 돌연변이를 지원하기 위해 확장할 수 있으며, 더 많은 해상도를 사용하여CosmosDB에 데이터를 쓸 수 있습니다.
검색에서 선택한 필드를 보고 CosmosDB에서만 이 데이터를 검색하는 또 다른 질문은 모든 필드를 추출할 수 있기 때문입니다. 그러나 다음 검색어를 만들면 다음과 같습니다.
{
    questions {
        id
        question
        correctAnswer
        incorrectAnswers
    }
}
CosmosDB에서 type 또는 category 필드를 반환하지 않는 것이 좋습니다.

좋은 웹페이지 즐겨찾기