65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (16投票s)

2019年3月25日

GPL3

8分钟阅读

viewsIcon

21022

在本文中,我们将介绍 GraphQL,并展示如何将其与非结构化数据库(MongoDB)一起使用。

引言

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

同时,当您拥有结构化数据(如关系型数据库或 NoSQL 中的强类型实体)时,实现 GraphQL 暴露是相当容易的,但在处理未类型数据时会非常复杂。我的意思是,当您拥有多个数据集并希望用户可以无限制地添加数据时。但是,您何时需要它?考虑到我们的日常生活,这是一个非常不寻常的情况。它甚至可能显得有些“变态”。但这正是我们在实现 Headless CMS RawCMS 时所需要做的。如果您对此一无所知,只需将 Headless CMS 视为一个工具,它允许您在不编写任何代码的情况下保存和读取数据,就像它是一个 REST 数据库一样。是的,我们知道这非常简化,但本文不是关于 Headless CMS,所以我们不想深入探讨(如果您需要了解更多信息,下方有链接)。

在进行这个项目时,我们遇到了一个挑战:使用 GraphQL 暴露非结构化数据。最终,我们做到了。这在逆向工程 C# GraphQL 实现方面付出了很多努力,但我们成功了,现在任何 RawCMS 用户都可以使用 GraphQL 查询其数据库。

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

GraphQL

什么是 GraphQL?

简而言之,GraphQL 是一个由 Facebook 创建的开源查询语言,作为通用 REST 架构的替代方案。它允许请求特定数据,使客户端对发送的信息拥有更多控制权。

GraphQL 操作只是一个查询(读取)、变异(写入)或订阅(持续读取)。这些操作都只是一个 string,需要根据 GraphQL 查询语言规范进行构造。幸运的是,GraphQL 始终在发展,因此未来可能还会有其他操作。

为什么选择 GraphQL?

GraphQL 的诞生是为了解决过度获取(overfetching)问题。

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

GraphQL 如何工作?

GraphQL 的基础是模式(Schema)。模式定义了前端可用的所有资源。

在模式中,您应该定义这些主要对象

  • Types
  • 查询
  • 变异
  • 订阅

这是模式定义的示例

{
  "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 内省(introspection)功能使得可以从 GraphQL API 中检索 GraphQL 模式。由于模式包含有关 GraphQL API 可用数据的所有信息,因此它非常适合自动生成 API 文档,并且是 GraphiQL(GraphiQL,REST 的 Swagger 对应物)的基础。

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 在变异请求中发送文件,但您应该在字段解析器中处理文件。

GraphQL、REST 还是 OData?

这三者目前都被认为是客户端-服务器通信的标准协议,认为一种协议会取代另一种协议是错误的。所有协议都有其优点和缺点,选择通常取决于您要开发的应用程序类型。

例如,假设您需要开发一个企业应用程序,其中报表或业务逻辑是主要功能。在这种情况下,您不能要求客户端具备业务逻辑或数据聚合能力,REST 解决方案可能是最佳选择。否则,如果您的需求是将数据库中的数据垂直暴露出来,就像 CMS 那样,GraphQL 和 OData 可以是正确的选择。

ASP.NET Core 上的 GraphQL

任何堆栈上的 GraphQL 服务通常会包含以下内容:

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

正如我们所见,其结构与经典的 REST 结构非常相似。最大的区别在于您有一个端点,该端点通过使用 GraphQL 库处理所有请求。

非常感谢 Joe McBride,他为 ASP.NET 实现了一个很好的库 graphql-dotnet

RawCMS:GraphQL 实现示例

在本章中,我们将展示在 Headless CMS 的上下文中实现 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<Field> FieldSettings { get; set; } = new List<Field>();
    }

    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<JObject>
    {
        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<object>
    {
        public QueryArguments TableArgs
        {
            get; set;
        }

        private IDictionary<FieldBaseType, Type> _fieldTypeToSystemType;

        protected IDictionary<FieldBaseType, Type> FieldTypeToSystemType
        {
            get
            {
                if (_fieldTypeToSystemType == null)
                {
                    _fieldTypeToSystemType = new Dictionary<FieldBaseType, Type>
                    {
                        { 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<string, 
        CollectionSchema> collections = null, GraphQLService graphQLService = null)
        {
            Name = collectionSchema.CollectionName;

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

        private void InitGraphField(Field field, Dictionary<string, CollectionSchema> 
        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<IntGraphType> { Name = "pageNumber" });
            TableArgs.Add(new QueryArgument<IntGraphType> { Name = "pageSize" });
            TableArgs.Add(new QueryArgument<StringGraphType> { Name = "rawQuery" });
        }
    }

CollectionType 中,我们需要定义特殊字段

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

GraphQL 解析器

在 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<List<JObject>>();
        }

        private string BuildMongoQuery(Dictionary<string, object> 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<string, object> dictionary = new Dictionary<string, object>();
                    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<object>();
            }
            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<T>(Stream s)
        {
            using (StreamReader reader = new StreamReader(s))
            using (JsonTextReader jsonReader = new JsonTextReader(reader))
            {
                JsonSerializer ser = new JsonSerializer();
                return ser.Deserialize<T>(jsonReader);
            }
        }

        [HttpPost]
        public async Task<ExecutionResult> Post([FromBody]GraphQLRequest request)
        {
            GraphQLRequest t = Deserialize<GraphQLRequest>(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<InstrumentFieldsMiddleware>();
                }
            });

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

            return result;
        }
    }

在 RawCMS 中注册插件

实现 GraphQL 的目标是在 RawCMS 上启用此功能。如此处所述,您可以像插件一样注册功能。下面的类注册了 GraphQL 插件。

    public class GraphQLPlugin : RawCMS.Library.Core.Extension.Plugin, 
                                 IConfigurablePlugin<GraphQLSettings>
    {
        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<IDependencyResolver>
                 (s => new FuncDependencyResolver(s.GetRequiredService));
            services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
            services.AddSingleton<IDocumentWriter, DocumentWriter>();
            services.AddScoped<ISchema, GraphQLSchema>();
            services.AddSingleton<GraphQLQuery>();
            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 在短短几年内已成为 API 开发的标准,就像 REST 和 OData 一样,并且很可能在未来几年,我们将在应用程序中看到它更频繁地出现,尤其是在移动领域。如今,GraphQL 已经足够成熟,可以在业务项目中使用,但我们必须记住,存在许多潜在的限制。首先,您需要处理一个经过充分原型化的数据库,您可以将整个数据库按原样或经过非常简单的自定义来暴露。

这个案例历史是一个小众案例,所以很难将本文教授的内容明天在工作中直接推广。但是,我们希望这能让您了解 GraphQL 及其内部机制,从而更容易地在我们的应用程序中集成 GraphQL,并充分利用其所有功能。

GraphQL 在非结构化数据上下文中的应用 - CodeProject - 代码之家
© . All rights reserved.