ASP.NET Core中使用GraphQL - 最終章 Data Loader

ASP.NET Core中使用GraphQL - 目錄html


在以前的幾章中,咱們的GraphQL查詢是沒有優化過的。下面咱們以CustomerType中的orders查詢爲例git

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

在這個查詢中,咱們獲取了某個顧客中全部的訂單, 這裏若是你只是獲取一些標量字段,那很簡單。github

可是若是須要獲取一些關聯屬性呢?例如查詢系統中的全部訂單,在訂單信息中附帶顧客信息。json

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信息的時候,系統會另外初始化一個請求,以便從數據倉儲中查詢訂單相關的顧客信息。c#

若是你瞭解dotnet cli, 你能夠針對如下查詢,在控制檯輸出全部的EF查詢日誌緩存

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

查詢結果:async

{
  "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"
        }
      }
    ]
  }
}

產生日誌以下:ide

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

相關文章
相關標籤/搜索