ASP.NET Core中使用GraphQL - 最終章 Data Loader
ASP.NET Core中使用GraphQL - 目錄
- ASP.NET Core中使用GraphQL - 第一章 Hello World
- ASP.NET Core中使用GraphQL - 第二章 中介軟體
- ASP.NET Core中使用GraphQL - 第三章 依賴注入
- ASP.NET Core中使用GraphQL - 第四章 GrahpiQL
- ASP.NET Core中使用GraphQL - 第五章 欄位, 引數, 變數
- ASP.NET Core中使用GraphQL - 第六章 使用EF Core作為持久化倉儲
- ASP.NET Core中使用GraphQL - 第七章 Mutation
- ASP.NET Core中使用GraphQL - 第八章 在GraphQL中處理一對多關係
- ASP.NET Core中使用GraphQL - 第九章 在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
。
DataLoader
是GraphQL
中的一個重要功能,它為GraphtQL
查詢提供了批處理和快取的功能。
為了使用DataLoader
, 我們首先需要在Startup.cs
中註冊2個新服務IDataLoaderContextAccessor
和DataLoaderDocumentListener
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