• ASP.NET Core中使用GraphQL


    ASP.NET Core中使用GraphQL - 目录


    在之前的几章中,我们的GraphQL查询是没有优化过的。下面我们以CustomerType中的orders查询为例

    CustomerType.cs
    Field<ListGraphType<OrderType>, IEnumerable<Order>>()  
        .Name("Orders")
        .ResolveAsync(ctx =>
        {
            return dataStore.GetOrdersAsync();
        }); 
    

    在这个查询中,我们获取了某个顾客中所有的订单, 这里如果你只是获取一些标量字段,那很简单。

    但是如果需要获取一些关联属性呢?例如查询系统中的所有订单,在订单信息中附带顾客信息。

    OrderType
    public OrderType(IDataStore dataStore, IDataLoaderContextAccessor accessor)  
    {
        Field(o => o.Tag);
        Field(o => o.CreatedAt);
        Field<CustomerType, Customer>()
            .Name("Customer")
            .ResolveAsync(ctx =>
            {            
                return dataStore.GetCustomerByIdAsync(ctx.Source.CustomerId);  
            });
    }
    

    这里当获取customer信息的时候,系统会另外初始化一个请求,以便从数据仓储中查询订单相关的顾客信息。

    如果你了解dotnet cli, 你可以针对以下查询,在控制台输出所有的EF查询日志

    {
      orders{
        tag
        createdAt
        customer{
          name
          billingAddress
        }
      }
    }
    

    查询结果:

    {
      "data": {
        "orders": [
          {
            "tag": "XPS 13",
            "createdAt": "2018-11-11",
            "customer": {
              "name": "Lamond Lu",
              "billingAddress": "Test Address"
            }
          },
          {
            "tag": "XPS 15",
            "createdAt": "2018-11-11",
            "customer": {
              "name": "Lamond Lu",
              "billingAddress": "Test Address"
            }
          }
        ]
      }
    }
    

    产生日志如下:

    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
          Executed DbCommand (16ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
          SELECT [o].[OrderId], [o].[CreatedAt], [o].[CustomerId], [o].[CustomerId1], [o].[Tag]
          FROM [Orders] AS [o]
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
          Executed DbCommand (6ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
          
          SELECT TOP(1) [e].[CustomerId], [e].[BillingAddress], [e].[Name]
          FROM [Customers] AS [e]
          WHERE [e].[CustomerId] = @__get_Item_0
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
          Executed DbCommand (5ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
          
          SELECT TOP(1) [e].[CustomerId], [e].[BillingAddress], [e].[Name]
          FROM [Customers] AS [e]
          WHERE [e].[CustomerId] = @__get_Item_0
    info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
          Request finished in 864.2749ms 200
    

    从日志上我们很清楚的看到,这个查询使用了3个查询语句,第一个语句查询所有的订单信息,第二个和第三个请求分别查询了2个订单的顾客信息。这里可以想象如果这里有N的订单,就会产生N+1个查询语句,这是非常不效率的。正常情况下我们其实可以通过2条语句就完成上述的查询,后面查询单个顾客信息其实可以整合成一条语句。

    为了实现这个效果,我们就需要介绍一下GraphQL中的DataLoader

    DataLoaderGraphQL中的一个重要功能,它为GraphtQL查询提供了批处理和缓存的功能。

    为了使用DataLoader, 我们首先需要在Startup.cs中注册2个新服务IDataLoaderContextAccessorDataLoaderDocumentListener

    Startup.cs
    services.AddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>();  
    services.AddSingleton<DataLoaderDocumentListener>();  
    

    如果你的某个GraphQL类型需要DataLoader, 你就可以在其构造函数中注入一个IDataLoaderContextAccessor接口对象。

    但是为了使用DataLoader, 我们还需要将它添加到我们的中间件中。

    GraphQLMiddleware.cs
    public async Task InvokeAsync(HttpContext httpContext, ISchema schema, IServiceProvider serviceProvider)  
    {
        ....
        ....
            
        var result = await _executor.ExecuteAsync(doc =>
        {
            ....
            ....
            doc.Listeners.Add(serviceProvider                                                             .GetRequiredService<DataLoaderDocumentListener>());
        }).ConfigureAwait(false);
    
        ....
        ....            
    }
    

    下一步,我们需要为我们的仓储类,添加一个新方法,这个方法可以根据顾客的id列表,返回所有的顾客信息。

    DataStore.cs
    public async Task<Dictionary<int, Customer>> GetCustomersByIdAsync(
        IEnumerable<int> customerIds,
        CancellationToken token)  
    {
        return await _context.Customers
            .Where(i => customerIds.Contains(i.CustomerId))
            .ToDictionaryAsync(x => x.CustomerId);
    }
    

    然后我们修改OrderType

    OrderType
    Field<CustomerType, Customer>()  
        .Name("Customer")
        .ResolveAsync(ctx =>
        {            
            var customersLoader = accessor.Context.GetOrAddBatchLoader<int, Customer>("GetCustomersById", dataStore.GetCustomersByIdAsync);
            return customersLoader.LoadAsync(ctx.Source.CustomerId);  
        });
    

    完成以上修改之后,我们重新运行项目, 使用相同的query, 结果如下,查询语句的数量变成了2个,效率大大提高

    info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
          Entity Framework Core 2.1.4-rtm-31024 initialized 'ApplicationDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
          Executed DbCommand (19ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
          SELECT [o].[OrderId], [o].[CreatedAt], [o].[CustomerId], [o].[CustomerId1], [o].[Tag]
          FROM [Orders] AS [o]
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
          Executed DbCommand (10ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
          SELECT [i].[CustomerId], [i].[BillingAddress], [i].[Name]
          FROM [Customers] AS [i]
          WHERE [i].[CustomerId] IN (1)
    

    DataLoader背后的原理

    GetOrAddBatchLoader方法会等到所有查询的顾客id列表准备好之后才会执行,它会一次性把所有查询id的顾客信息都收集起来。 这种技术就叫做批处理,使用了这种技术之后,无论有多少个关联的顾客信息,系统都只会发出一次请求来获取所有数据。

    本文源代码: https://github.com/lamondlu/GraphQL_Blogs/tree/master/Part%20X

  • 相关阅读:
    方法和参数
    【转】priority_queue优先队列
    【转】主席树学习
    【转】树链剖分
    【转】线段树完全版~by NotOnlySuccess
    【转】树状数组
    【转】最大流EK算法
    【转】POJ题目分类推荐 (很好很有层次感)
    【转】原根
    【转】Polya定理
  • 原文地址:https://www.cnblogs.com/lwqlun/p/9972233.html
Copyright © 2020-2023  润新知