非结构化数据上下文中的GraphQL

目录

介绍

GraphQL

什么是GraphQL?

为什么选择GraphQL?

GraphQL如何工作?

GraphQL的好处是什么?

中心模式

多个客户端

版本

GraphQL的缺点是什么?

查询复杂性

高速缓存

上传文件

GraphQL,REST还是OData?

ASP.NET核心上的GraphQL

RawCMS:一个GraphQL的实现示例

先决条件

模式

GraphQL类型

图解析器

GraphQL控制器

在RawCMS上注册插件

兴趣点


  • GraphQL案例历史
  • 维基

介绍

在本文中,我们将介绍GraphQL并在非结构化数据的上下文中显示其实现的示例。我们将展示如何在CMS Headless的上下文中实现GraphQL标准,如RawCMS,其中实现技术栈使用ASP.NET CoreMongoDB

同时,当您拥有结构化数据(如RDBMSNoSQL上的良好类型实体)时,很容易实现GraphQL公开,而当您处理非类型化数据时,它非常复杂。我的意思是当你有多组数据并且你希望用户没有任何限制的添加数据。但你什么时候需要它?考虑到我们的日常生活,这是一个非常不寻常的场景。它也可能看起来有点不正常。但这是我们必须要做的,实现Headless CMSRawCMS。如果您对此一无所知,只需将Headless CMS视为一种工具,它允许您在不编写任何代码的情况下保存和读取数据,就像它是REST数据库一样。是的,我们知道这非常简单,但这篇文章不是关于Headless CMS,所以我们不想深入研究它(如果您需要了解更多信息,请在下面找到一个链接)。

那么,在这个项目上,我们遇到了这个挑战:使用GraphQL公开非结构化数据。最后,我们做到了。在C GraphQL实现的逆向工程方面花费了很多时间,但我们使它工作,现在任何RawCMS用户都可以使用GraphQL查询其数据库。

这是我们与您分享的结果,并借此机会谈谈GraphQL以及如何在现实世界中使用它。

GraphQL

什么是GraphQL

简而言之,GraphQL是一种开源查询语言,Facebook创建,作为常见REST架构的替代品。它允许对特定数据的请求,使客户能够更好地控制发送的信息。

GraphQL操作只是查询(Read),改变(Write)或订阅(Continuous read)。每个操作只是一个需要根据GraphQL查询语言规范构建的string。幸运的是,GraphQL一直在不断发展,因此将来可能会有其他操作。

为什么选择GraphQL

GraphQL的诞生了解决过度获取问题。

使用RESTful架构,后端定义每个URL上每个资源可用的数据,而前端始终必须请求资源中的所有信息,即使只需要其中的一部分。在最糟糕的情况下,客户端应用程序必须通过多个网络请求读取多个资源。像服务器端的GraphQL和客户端对客户端的需求这样的查询语言通过向服务器发出单个请求来决定需要哪些数据。为了应用程序的使用,有效的数据传输减少了网络使用,主要用于移动应用程序。

GraphQL如何工作?

GraphQL背后的基础是模式(Scheme)。该方案定义了可用于前端的所有资源。

在架构上,您应该定义此主要对象:

  • Types
  • Query
  • Mutation
  • Subscription

这是架构定义的示例:

{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query"
      },
      "mutationType": null,
      "subscriptionType": null,
      "types": [
        {
          "kind": "OBJECT",
          "name": "Query",
          "description": null,
          "fields": [
            {
              "name": "country",
              "description": null,
              "args": [
                {
                  "name": "codCountry",
                  "description": null,
                  "type": {
                    "kind": "SCALAR",
                    "name": "String",
                    "ofType": null
                  },
                  "defaultValue": "null"
                }
          ................................
          ],
          "inputFields": null,
          "interfaces": [],
          "enumValues": null,
          "possibleTypes": null
        }
      ]
    }
  }
}

GraphQL自省使得从GraphQL API中检索GraphQL模式成为可能。由于模式具有通过GraphQL API提供的有关数据的所有信息,因此它非常适合自动生成API文档,它是GraphiQL的基础(GraphiQL,相当于RESTswagger)。

GraphQL的好处是什么?

正如已经提到的,GraphQL的诞生是为了解决过度获取数据并要求客户端开发人员管理必要的数据。除此之外,实现GraphQL还有其他好处。

中心模式

GraphQL模式是功能应用程序的唯一来源,并提供了描述所有数据的中心位置。

多个客户端

现代应用程序越来越多地使用服务器应用程序和许多类型的客户端连接(电话,PC,平板电脑......)。每种类型的客户端对数据获取都有不同的需求。使用GraphQL,可能没有必要在服务器上为每个客户端实现不同的源代码。

版本

GraphQL中,没有像以前那样在REST中使用的API版本。在REST中,提供API的多个版本是正常的(例如,api.domain.com / v1 /api.domain.com / v2 /),因为资源或资源的结构可能会随着时间的推移而发生变化。在GraphQL中,可以在字段级别上弃用API。因此,客户端在查询已弃用的字段时会收到弃用警告。一段时间后,当不再有许多客户端使用它时,可以从架构中删除不推荐使用的字段。这使得随着时间的推移可以发展GraphQL API而无需版本控制。

GraphQL的缺点是什么?

查询复杂性

人们经常将GraphQL误认为是服务器端数据库的替代品,但它只是一种查询语言。一旦需要使用服务器上的数据解析查询,GraphQL无关的实现通常会执行数据库访问。此外,当您必须在一个查询中访问多个字段时,GraphQL不会消除性能瓶颈。无论请求是在RESTful架构还是GraphQL中进行,仍然必须从数据源检索各种资源和字段。因此,当客户端一次请求太多嵌套字段时会出现问题。前端开发人员并不总是意识到服务器端应用程序必须执行的检索数据的工作,因此必须有一个机制,如最大查询深度,查询复杂性加权,避免递归,

高速缓存

GraphQL上实现缓存比在REST上实现更困难。在GraphQL上,所有都在单个动词上的单个URL上公开,而不是任何请求可以是不同的。

上传文件

GraphQL规范不提供有关文件上载的任何信息。您可以使用multipart发送变异请求的文件,但是您应该在字段解析器上处理文件。

GraphQLREST还是OData

今天所有这三个都被认为是客户端——服务器通信的标准协议,认为协议将取代另一个协议是错误的。所有协议都有其优点和缺点,选择通常取决于我们将要开发的应用程序类型。

例如,假设您应该开发一个企业应用程序,其中报表或业务逻辑是主要功能。在这种情况下,您不能要求客户端业务逻辑或数据聚合,REST解决方案可能是最佳解决方案。否则,如果您的需求是在数据库上垂直公开数据,那么CMSGraphQLOData如何才能成为正确的选择。

ASP.NET核心上的GraphQL

任何技术栈上的GraphQL服务可能包括以下内容:

  • HTTP服务的框架
  • 数据库层
  • 一个GraphQL

非结构化数据上下文中的GraphQL_第1张图片

正如我们所看到的,高度结构在更类似的经典REST结构中。最大的区别在于您有一个独特的终点,可以通过使用GraphQL库来处理所有请求。

非常感谢Joe McBrideASP.NET graphql-dotnet实现了一个很好的库。

RawCMS:一个GraphQL的实现示例

在本章中,我们将展示在CMS Headless上下文中实际实现GraphQL的示例。我们将实现一个专用的插件来扩展RawCMS的功能。

先决条件

  • Visual Studio
  • ASP.NET Core 2 SDK
  • graphql-dotnet 
  • MongoDB

模式

GraphQL是一种强类型查询语言。使用MongoDB时,您的数据通常是非结构化的。为了解决这个问题,我们选择定义一个可配置的模式来决定使用GraphQL公开哪些数据。

MongoDB上,我们将有一个特殊的集合(_schema),我们将使用此模型保存模式配置:

public class CollectionSchema
{
    public string CollectionName { get; set; }
    public bool AllowNonMappedFields { get; set; }

    public List FieldSettings { get; set; } = new List();
}

public class Field
{
    public string Name { get; set; }

    public bool Required { get; set; }

    public string Type { get; set; }

    [JsonConverter(typeof(StringEnumConverter))]
    public FieldBaseType BaseType { get; set; }

    public JObject Options { get; set; }
}

现在您已添加了集合,我们可以定义我们的模式:

public class GraphQLQuery : ObjectGraphType
    {
        public GraphQLQuery(GraphQLService graphQLService)
        {
            Name = "Query";
            foreach (var key in graphQLService.Collections.Keys)
            {
                Library.Schema.CollectionSchema metaColl = graphQLService.Collections[key];
                CollectionType type = new CollectionType
                        (metaColl, graphQLService.Collections, graphQLService);
                ListGraphType listType = new ListGraphType(type);
                
                AddField(new FieldType
                {
                    Name = metaColl.CollectionName,
                    Type = listType.GetType(),
                    ResolvedType = listType,
                    Resolver = new JObjectFieldResolver(graphQLService),
                    Arguments = new QueryArguments(
                        type.TableArgs
                    )
                });
            }
        }
    }

    public class GraphQLSchema : SchemaQL
    {
        public GraphQLSchema(IDependencyResolver dependencyResolver, 
                             GraphQLQuery graphQLQuery) : base(dependencyResolver)
        {
            Query = graphQLQuery;
        }
    }

该模式从MongoDB集合开始构建,并为所有元素添加映射。

GraphQL类型

GraphQL需要知道我们将使用哪些类型,但RawCMS是一个动态CMS,然后你不能事先知道结构。为了解决这个问题,我们定义了一个类似的泛型类型JObject

public class CollectionType : ObjectGraphType
    {
        public QueryArguments TableArgs
        {
            get; set;
        }

        private IDictionary _fieldTypeToSystemType;

        protected IDictionary FieldTypeToSystemType
        {
            get
            {
                if (_fieldTypeToSystemType == null)
                {
                    _fieldTypeToSystemType = new Dictionary
                    {
                        { FieldBaseType.Boolean, typeof(bool) },
                        { FieldBaseType.Date, typeof(DateTime) },
                        { FieldBaseType.Float, typeof(float) },
                        { FieldBaseType.ID, typeof(Guid) },
                        { FieldBaseType.Int, typeof(int) },
                        { FieldBaseType.String, typeof(string) },
                        { FieldBaseType.Object, typeof(JObject) }
                    };
                }

                return _fieldTypeToSystemType;
            }
        }

        private Type ResolveFieldMetaType(FieldBaseType type)
        {
            if (FieldTypeToSystemType.ContainsKey(type))
            {
                return FieldTypeToSystemType[type];
            }

            return typeof(string);
        }

        public CollectionType(CollectionSchema collectionSchema, Dictionary collections = null, GraphQLService graphQLService = null)
        {
            Name = collectionSchema.CollectionName;

            foreach (Field field in collectionSchema.FieldSettings)
            {
                InitGraphField(field, collections, graphQLService);
            }
        }

        private void InitGraphField(Field field, Dictionary 
        collections = null, GraphQLService graphQLService = null)
        {
            Type graphQLType;
            if (field.BaseType == FieldBaseType.Object)
            {
                var relatedObject = collections[field.Type];
                var relatedCollection = new CollectionType(relatedObject, collections);
                var listType = new ListGraphType(relatedCollection);
                graphQLType = relatedCollection.GetType();
                FieldType columnField = Field(
                graphQLType,
                relatedObject.CollectionName);

                columnField.Resolver = new NameFieldResolver();
                columnField.Arguments = new QueryArguments(relatedCollection.TableArgs);
                foreach(var arg in columnField.Arguments.Where(x=>!(new string[] 
                { "pageNumber", "pageSize", "rawQuery", "_id" }.Contains(x.Name))).ToList())
                {
                    arg.Name = $"{relatedObject.CollectionName}_{arg.Name}";
                    TableArgs.Add(arg);
                }
            }
            else
            {
                graphQLType = 
                   (ResolveFieldMetaType(field.BaseType)).GetGraphTypeFromType(!field.Required);
                FieldType columnField = Field(
                graphQLType,
                field.Name);

                columnField.Resolver = new NameFieldResolver();
                FillArgs(field.Name, graphQLType);
            }
        }

        private void FillArgs(string name, Type graphType)
        {
            if (TableArgs == null)
            {
                TableArgs = new QueryArguments(
                    new QueryArgument(graphType)
                    {
                        Name = name
                    }
                );
            }
            else
            {
                TableArgs.Add(new QueryArgument(graphType) { Name = name });
            }

            TableArgs.Add(new QueryArgument { Name = "pageNumber" });
            TableArgs.Add(new QueryArgument { Name = "pageSize" });
            TableArgs.Add(new QueryArgument { Name = "rawQuery" });
        }
    } 
  

CollectionType中,我们必须定义特殊字段:

  • pageNumber,为分页
  • pageSize,为分页
  • rawQuery,允许在映射的集合上编写自定义的MongoDB查询

图解析器

GraphQL中,模式定义了客户端可用的对象,解析器是解释数据库结构如何映射到模式的连接。在我们的例子中,我们需要两个解析器:

JObjectFieldResolver 处理从GraphQL查询到MongoDB查询的映射:

public class JObjectFieldResolver : IFieldResolver
   {
       private readonly GraphQLService _graphQLService;

       public JObjectFieldResolver(GraphQLService graphQLService)
       {
           _graphQLService = graphQLService;
       }

       public object Resolve(ResolveFieldContext context)
       {
           ItemList result;
           if (context.Arguments != null && context.Arguments.Count > 0)
           {
               int pageNumber = 1;
               int pageSize = 1000;
               if (context.Arguments.ContainsKey("pageNumber"))
               {
                   pageNumber = int.Parse(context.Arguments["pageNumber"].ToString());
                   if (pageNumber < 1)
                   {
                       pageNumber = 1;
                   }
                   context.Arguments.Remove("pageNumber");
               }

               if (context.Arguments.ContainsKey("pageSize"))
               {
                   pageSize = int.Parse(context.Arguments["pageSize"].ToString());
                   context.Arguments.Remove("pageSize");
               }
               //Query Database
               result = _graphQLService.CrudService.Query
                        (context.FieldName.ToPascalCase(), new DataQuery()
               {
                   PageNumber = pageNumber,
                   PageSize = pageSize,
                   RawQuery = BuildMongoQuery(context.Arguments)
               });
           }
           else
           {
               //Query Database
               result = _graphQLService.CrudService.Query
                        (context.FieldName.ToPascalCase(), new DataQuery()
               {
                   PageNumber = 1,
                   PageSize = 1000,
                   RawQuery = null
               });
           }

           return result.Items.ToObject>();
       }

       private string BuildMongoQuery(Dictionary arguments)
       {
           string query = null;
           if (arguments != null)
           {
               JsonSerializerSettings jSettings = new JsonSerializerSettings
               {
                   NullValueHandling = NullValueHandling.Ignore
               };

               if (arguments.ContainsKey("rawQuery"))
               {
                   query = Convert.ToString(arguments["rawQuery"]);
               }else if (arguments.ContainsKey("_id"))
               {
                   query = "{_id: ObjectId(\"" +
                           Convert.ToString(arguments["_id"]) + "\")}";
               }
               else
               {

                   jSettings.ContractResolver = new DefaultContractResolver();
                   Dictionary dictionary = new Dictionary();
                   foreach (string key in arguments.Keys)
                   {
                       if (arguments[key] is string)
                       {

                           JObject reg = new JObject
                           {
                               ["$regex"] = $"/*{arguments[key]}/*",
                               ["$options"] = "si"
                           };
                           //"_" is added for query subobject
                           dictionary[key.ToPascalCase().Replace("_",".")] = reg;
                       }
                       else
                       {
                           dictionary[key.ToPascalCase().Replace("_", ".")] = arguments[key];
                       }
                   }
                   query = JsonConvert.SerializeObject(dictionary, jSettings);
               }
           }

           return query;
       }
   }

NameFieldResolver 处理从MongoDB查询结果到GraphQL结果的映射:

public class NameFieldResolver : IFieldResolver
{
    public object Resolve(ResolveFieldContext context)
    {
        object source = context.Source;
        if (source == null)
        {
            return null;
        }
        string name = char.ToUpperInvariant(context.FieldAst.Name[0]) +
                      context.FieldAst.Name.Substring(1);
        object value = GetPropValue(source, name);
        if (value == null)
        {
            throw new InvalidOperationException($"Expected to find property
            {context.FieldAst.Name} on {context.Source.GetType().Name} but it does not exist.");
        }
        return value;
    }

    private static object GetPropValue(object src, string propName)
    {
        JObject source = src as JObject;
        source.TryGetValue
               (propName, StringComparison.InvariantCultureIgnoreCase, out JToken value);
        if (value != null)
        {
            return value.Value();
        }
        else
        {
            return null;
        }
    }
} 
  

GraphQL控制器

作为最后一步,我们定义了一个在我们的应用程序上启用GraphQL功能的控制器: 

[AllowAnonymous]
[RawAuthentication]
[Route("api/graphql")]
public class GraphQLController : Controller
{
    private readonly IDocumentExecuter _executer;
    private readonly IDocumentWriter _writer;
    private readonly GraphQLService _service;
    private readonly ISchema _schema;

    public GraphQLController(IDocumentExecuter executer,
        IDocumentWriter writer,
        GraphQLService graphQLService,
        ISchema schema)
    {
        _executer = executer;
        _writer = writer;
        _service = graphQLService;
        _schema = schema;
    }

    public static T Deserialize(Stream s)
    {
        using (StreamReader reader = new StreamReader(s))
        using (JsonTextReader jsonReader = new JsonTextReader(reader))
        {
            JsonSerializer ser = new JsonSerializer();
            return ser.Deserialize(jsonReader);
        }
    }

    [HttpPost]
    public async Task Post([FromBody]GraphQLRequest request)
    {
        GraphQLRequest t = Deserialize(HttpContext.Request.Body);
        DateTime start = DateTime.UtcNow;

        ExecutionResult result = await _executer.ExecuteAsync(_ =>
        {
            _.Schema = _schema;
            _.Query = request.Query;
            _.OperationName = request.OperationName;
            _.Inputs = request.Variables.ToInputs();
            _.UserContext = _service.Settings.BuildUserContext?.Invoke(HttpContext);
            _.EnableMetrics = _service.Settings.EnableMetrics;
            if (_service.Settings.EnableMetrics)
            {
                _.FieldMiddleware.Use();
            }
        });

        if (_service.Settings.EnableMetrics)
        {
            result.EnrichWithApolloTracing(start);
        }

        return result;
    }
}

RawCMS上注册插件

实现GraphQL的目标是在RawCMS上下文中实现此功能。如此处所述,您可以像注册插件一样注册功能。接下来的类注册GraphQL插件。

public class GraphQLPlugin : RawCMS.Library.Core.Extension.Plugin,
                             IConfigurablePlugin
{
    public override string Name => "GraphQL";

    public override string Description => "Add GraphQL CMS capabilities";

    public override void Init()
    {
        Logger.LogInformation("GraphQL plugin loaded");
    }

    private GraphQLService graphService = new GraphQLService();

    public override void ConfigureServices(IServiceCollection services)
    {
        base.ConfigureServices(services);

        services.AddSingleton
             (s => new FuncDependencyResolver(s.GetRequiredService));
        services.AddSingleton();
        services.AddSingleton();
        services.AddScoped();
        services.AddSingleton();
        services.AddSingleton(x => graphService);
    }

    private AppEngine appEngine;

    public override void Configure(IApplicationBuilder app, AppEngine appEngine)
    {
        this.appEngine = appEngine;
        graphService.SetCRUDService(this.appEngine.Service);
        graphService.SetLogger(this.appEngine.GetLogger(this));
        graphService.SetSettings(config);
        graphService.SetAppEngine(appEngine);

        base.Configure(app, appEngine);

        app.UseGraphiQl(config.GraphiQLPath, config.Path);
    }

    private IConfigurationRoot configuration;

    public override void Setup(IConfigurationRoot configuration)
    {
        base.Setup(configuration);
        this.configuration = configuration;
    }

    public GraphQLSettings GetDefaultConfig()
    {
        return new GraphQLSettings
        {
            Path = "/api/graphql",
            EnableMetrics = false,
            GraphiQLPath = "/graphql"
        };
    }

    private GraphQLSettings config;

    public void SetActualConfig(GraphQLSettings config)
    {
        this.config = config;
    }
}

兴趣点

在本文中,我们展示了当您必须使用非结构化数据及其应用程序示例时,如何在上下文中实现GraphQL API公开。GraphQL在几年内成为REST开发和ODataAPI开发的标准,可能在未来几年,我们将有机会在应用程序上更频繁地看到它,主要是在移动环境中。如今,GraphQL已经足够成熟,可以在业务项目中加以考虑,但我们必须记住,存在许多可能的限制。首先,您需要使用一个非常好的原型数据库,您可以在其中公开整个数据库或使用非常简单的自定义。

这个案例历史是一个利基案例,所以很难把我们明天在这篇文章中所教的东西用在工作上。但是,我们希望这将教会您一些关于GraphQL及其内部的内容,因此在我们的应用程序中利用其所有功能集成GraphQL会更加简单。

 

原文地址:https://www.codeproject.com/Articles/1279833/GraphQL-on-Unstructured-Data-Context

你可能感兴趣的:(ASP.NET,CORE,CSharp.NET,架构及框架,MVC,GraphQL,MongoDB,ASP.NET,Core,RawCMS)