Azure 移动服务器 SDK:何时评估 IQueryable?

Azure Mobile Server SDK: When the IQueryable is evaluated?

我有一个基于 Azure 移动应用服务 SDK(命名空间 Microsoft.Azure.Mobile.Server.Tables 等)的 MS Azure 后端。 它是 运行 ASP.NET MVC 在 SQL 服务器数据库上,在 C# 中。

我已经搭建了我的控制器,并且我有方法 GetAllTodoItems returns 和 IQueryable<TodoItem>

这个 IQueryable 的确切计算时间是什么时候?

我设置了性能负载测试,平均请求需要 46 秒才能完成,而我的可见代码和 SQL 查询最多需要 5 毫秒!!

我错过了什么?

编辑 ====================

这是我的 GetAllTodoItems 方法,以及依赖项:

protected IQueryable<TModelDTO> GetAllEntities()
{
    IQueryable<TModel> allEntitiesQuery = Query();
    IQueryable<string> visibleObj = context.VisibleObjs(GetUserID(), AttType);

    IQueryable<TModel> finalQuery = from item in allEntitiesQuery
                                    join visib in visibleObj on item.Id equals visib
                                    select item;
    return finalQuery.Select(Selector).AsQueryable();
}

IQueryable<string> VisibleObjs(string userID, AttachmentType type)
{
    return (from ud in UserDesktops
            join a in Attachments on ud.DesktopId equals a.ParentDesktop
            where (ud.UserId == userID) && (a.AttachmentType == type))
            select a.Id);
}

protected Func<TModel, TModelDTO> Selector { get { return d => ToDTO(d); } }

protected override TModelDTO ToDTO(TModel input)
{
    return new TModelDTO(input);
}

public TModelDTO(TModel entity)
{
    // all basic properties copied:
    Content = entity.Content;
    Width = entity.Width;
    Color = entity.Color;
    HighResImageContent = entity.HighResImageContent;
    ImageContent = entity.ImageContent;
    MaskPath = entity.MaskPath;
    MinHeight = entity.MinHeight;
    IsComment = entity.IsComment;
    IsInkNote = entity.IsInkNote;
}

对于returnsIQueryable数据在ApiController的动作,WebAPI会做ToList操作,然后序列化列表值,最后将序列化后的列表写入响应体,响应状态码为200(OK)。

只有当我们执行IQueryable的"ToList"方法时,才真正取出数据库中的数据,执行"IQueryProvider"中的"Excute"方法。 (解析表达式,然后执行得到结果)。

你说46秒完成,我猜你是用IQueryable做了一些耗时的操作,比如:先取IEnumerable再过滤数据会造成性能问题。

您可以提供更详细的代码给我们进一步研究。

希望这对您有所帮助。

Linq 是惰性的,这意味着当您尝试像在 foreach 循环或某些扩展方法(如 .ToList() 或 .ToArray())中访问枚举时,它实际上会执行。当您在方法中定义 linq 查询时,访问结果时应该做的事情 "preparation" 很简单。与执行相比,查询的准备只需要一点时间。这就是为什么您看到自己的代码在几毫秒内运行的原因。这只是一个准备。最后,当您访问结果时,实际上会执行查询,例如当 asp.net 序列化您的数据以构建请求的响应时。

在您的情况下,您尝试构建一个不区分大小写的过滤器

where (ud.UserId.Equals(userID, StringComparison.InvariantCultureIgnoreCase)

在 VisibleObjs() 方法中。 Equals(userID, StringComparison.InvariantCultureIgnoreCase) 调用似乎强制 EF 在执行时处理过滤器之前 query/return 来自 table 的所有数据。过滤器的评估是在客户端执行的,而不是在 sql 服务器上使用不区分大小写的搜索。一种可能的解决方案是使用排序规则 "SQL_Latin1_General_CP1_CI_AS" 标记数据库中的 sql 服务器列 UserDesktops.UserId,其中 CI 表示不区分大小写。之后你应该用

替换你的过滤器
where (ud.UserId == userID)

或类似的东西而不使用任何 .Net 方法允许 EF 将您的 linq 过滤器转换为普通 sql 比较。在这种情况下,不区分大小写的过滤器由 sql 服务器直接处理,无需从 sql 服务器请求完整的 table 并在客户端进行过滤。

在这种情况下,它可以在多个位置执行。将GetUserID方法拉出来,放在上面的一个变量中。

var userId = GetUserID();
IQueryable<string> visibleObj = context.VisibleObjs(userId, AttType);

这可能会立即解决您的性能问题 - 也许它正在单独执行 SQL,然后加入内存。

此外,context.VisibleObjs - context 与 Query() 使用的上下文相同吗?如果是不同的上下文,则不会使用 SQL 加入。您应该在控制器的 Initialize 方法中获取上下文并将其存储在 class 变量中。

还有,AttachmentType是什么类型的?它是枚举吗?也许需要明确地转换为 int ?那里需要更多信息。

When exactly is this IQueryable evaluated?

我们想要 SQL 到 运行 的点是它被迭代的时候。在上面的代码中,如果编写正确,它实际上不应该 运行 在此方法中。 当迭代时,finalQuery.Select(Selector)之前的所有表达式都应该运行排成SQL。 Selector 方法显然不能在数据库上 运行 ,所以当时它需要 运行 和 SQL 作为查询展开本身。

查询将在序列化期间自行展开。

这是什么意思?那么您已经将一个由 Expression Tree. The Table Service framework may add some filters or sorts as requested by the web client (see supported query operators 组成的 IQueryable 对象交还给了 API。这样做之后,Web api 框架(调用您的控制器)将枚举 IQueryable 触发执行,因为它写出 JSON(?).

我们需要知道 SQL 实际上是什么 运行ning。这是使用 Linq to SQL / EF 的关键。

在对 Linq to SQL 进行故障排除时,我经常在上下文的数据库中放置一个记录器。 context.Database.Log = Console.Write 是我使用的快速解决方案。使用 TableController,您需要在控制器的 Initialize 方法中使用 context.Database.Log = a => this.Configuration.Services.GetTraceWriter().Info(a); - 上下文已初始化。

那就简单看看日志吧。

我将 tables、模式等模拟到一个 TableController 中,然后 运行 我自己模拟了这个,然后在连接了日志记录的情况下检查了输出,所以让我们来看看看看发生了什么:

iisexpress.exe Information: 0 : Request, Method=GET, Url=http://localhost:51543/tables/TodoItem?ZUMO-API-VERSION=2.0.0, Message='http://localhost:51543/tables/TodoItem?ZUMO-API-VERSION=2.0.0'
iisexpress.exe Information: 0 : Message='TodoItem', Operation=DefaultHttpControllerSelector.SelectController
iisexpress.exe Information: 0 : Message='maqsService.Controllers.TodoItemController', Operation=DefaultHttpControllerActivator.Create
iisexpress.exe Information: 0 : Message='maqsService.Controllers.TodoItemController', Operation=HttpControllerDescriptor.CreateController
iisexpress.exe Information: 0 : Message='Selected action 'GetAllTodoItems()'', Operation=ApiControllerActionSelector.SelectAction
iisexpress.exe Information: 0 : Operation=HttpActionBinding.ExecuteBindingAsync
iisexpress.exe Information: 0 : Operation=TableQueryFilter.OnActionExecutingAsync
iisexpress.exe Information: 0 : Operation=EnableQueryAttribute.OnActionExecutingAsync
iisexpress.exe Information: 0 : Operation=TableControllerConfigAttribute.OnActionExecutingAsync
'iisexpress.exe' (CLR v4.0.30319: /LM/W3SVC/2/ROOT-1-131799606929250512): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Numerics\v4.0_4.0.0.0__b77a5c561934e089\System.Numerics.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'iisexpress.exe' (CLR v4.0.30319: /LM/W3SVC/2/ROOT-1-131799606929250512): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_32\System.Data.OracleClient\v4.0_4.0.0.0__b77a5c561934e089\System.Data.OracleClient.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.

接下来是日志中的一行,表明它返回了 IQueryable:

iisexpress.exe Information: 0 : Message='Action returned 'System.Linq.Enumerable+WhereSelectEnumerableIterator`2[maqsService.DataObjects.TodoItem,maqsService.Controllers.TodoItemDTO]'', Operation=ReflectedHttpActionDescriptor.ExecuteAsync

注意,没有SQL被执行。它现在确定要在 Json:

中序列化它
iisexpress.exe Information: 0 : Message='Will use same 'JsonMediaTypeFormatter' formatter', Operation=JsonMediaTypeFormatter.GetPerRequestFormatterInstance
iisexpress.exe Information: 0 : Message='Selected formatter='JsonMediaTypeFormatter', content-type='application/json; charset=utf-8'', Operation=DefaultContentNegotiator.Negotiate
iisexpress.exe Information: 0 : Operation=ApiControllerActionInvoker.InvokeActionAsync, Status=200 (OK)
iisexpress.exe Information: 0 : Operation=TableControllerConfigAttribute.OnActionExecutedAsync, Status=200 (OK)

现在 JsonSerializer 将序列化 IQueryable,为此需要枚举它。

iisexpress.exe Information: 0 : Message='Opened connection at 8/28/2018 4:11:48 PM -04:00
'
'iisexpress.exe' (CLR v4.0.30319: /LM/W3SVC/2/ROOT-1-131799606929250512): Loaded 'EntityFrameworkDynamicProxies-maqsService'. 
iisexpress.exe Information: 0 : Message='SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Text] AS [Text], 
    [Extent1].[Complete] AS [Complete], 
    [Extent1].[AttachmentId] AS [AttachmentId], 
    [Extent1].[Version] AS [Version], 
    [Extent1].[CreatedAt] AS [CreatedAt], 
    [Extent1].[UpdatedAt] AS [UpdatedAt], 
    [Extent1].[Deleted] AS [Deleted]
    FROM  [dbo].[TodoItems] AS [Extent1]
    INNER JOIN  (SELECT [Extent2].[UserId] AS [UserId], [Extent3].[Id] AS [Id1], [Extent3].[AttachmentType] AS [AttachmentType]
        FROM  [dbo].[UserDesktops] AS [Extent2]
        INNER JOIN [dbo].[Attachments] AS [Extent3] ON [Extent2].[DesktopId] = [Extent3].[ParentDesktop] ) AS [Join1] ON [Extent1].[Id] = [Join1].[Id1]
    WHERE (([Join1].[UserId] = @p__linq__0) OR (([Join1].[UserId] IS NULL) AND (@p__linq__0 IS NULL))) AND ([Join1].[AttachmentType] = @p__linq__1)'
iisexpress.exe Information: 0 : Message='
'
iisexpress.exe Information: 0 : Message='-- p__linq__0: 'dana' (Type = String, Size = 4000)
'
iisexpress.exe Information: 0 : Message='-- p__linq__1: '1' (Type = Int32, IsNullable = false)
'
iisexpress.exe Information: 0 : Message='-- Executing at 8/28/2018 4:11:48 PM -04:00
'
iisexpress.exe Information: 0 : Message='-- Completed in 7 ms with result: SqlDataReader
'
iisexpress.exe Information: 0 : Message='
'
iisexpress.exe Information: 0 : Message='Closed connection at 8/28/2018 4:11:48 PM -04:00
'

SQL完成。

iisexpress.exe Information: 0 : Operation=EnableQueryAttribute.OnActionExecutedAsync, Status=200 (OK)
iisexpress.exe Information: 0 : Operation=TableQueryFilter.OnActionExecutedAsync, Status=200 (OK)
iisexpress.exe Information: 0 : Operation=TodoItemController.ExecuteAsync, Status=200 (OK)
iisexpress.exe Information: 0 : Response, Status=200 (OK), Method=GET, Url=http://localhost:51543/tables/TodoItem?ZUMO-API-VERSION=2.0.0, Message='Content-type='application/json; charset=utf-8', content-length=unknown'
iisexpress.exe Information: 0 : Operation=JsonMediaTypeFormatter.WriteToStreamAsync
iisexpress.exe Information: 0 : Operation=TodoItemController.Dispose

我不确定性能问题是什么。虽然我的 table 没有数据,但我看到它正确地将所有内容汇总到一个 SQL 中。可能有所不同的东西? AttachmentType 的类型。我用了一个整数。

还有什么?

如果你想拥有堆栈并真正了解表底下发生的事情,有一系列的OData框架过滤器把这些也放在调用堆栈上。

这是调用 TModelDTO(在我的示例中为 TodoItemDTO)构造函数期间的堆栈,它在 SQL 结果的迭代期间被调用。请注意,堆栈中没有我们的控制器方法。我们早就交回了IQueryable。这回到了框架代码中,它实际使用了 IQueryable,我正在拦截它,因为我们的 Select 方法调用 DTO 以 t运行sform 它。

maqsService.dll!maqsService.Controllers.TodoItemDTO.TodoItemDTO(maqsService.DataObjects.TodoItem entity) Line 89    C#
    maqsService.dll!maqsService.Controllers.TodoItemController.ToDTO(maqsService.DataObjects.TodoItem input) Line 55    C#
    maqsService.dll!maqsService.Controllers.TodoItemController.get_Selector.AnonymousMethod__6_0(maqsService.DataObjects.TodoItem d) Line 51    C#
>   System.Core.dll!System.Linq.Enumerable.WhereSelectEnumerableIterator<maqsService.DataObjects.TodoItem, maqsService.Controllers.TodoItemDTO>.MoveNext()  Unknown
    System.Core.dll!System.Linq.Buffer<maqsService.Controllers.TodoItemDTO>.Buffer(System.Collections.Generic.IEnumerable<maqsService.Controllers.TodoItemDTO> source)  Unknown
    System.Core.dll!System.Linq.OrderedEnumerable<maqsService.Controllers.TodoItemDTO>.GetEnumerator()  Unknown
    System.Core.dll!System.Linq.Enumerable.TakeIterator<maqsService.Controllers.TodoItemDTO>(System.Collections.Generic.IEnumerable<maqsService.Controllers.TodoItemDTO> source, int count) Unknown
    mscorlib.dll!System.Collections.Generic.List<maqsService.Controllers.TodoItemDTO>.List(System.Collections.Generic.IEnumerable<maqsService.Controllers.TodoItemDTO> collection) Line 99  C#

似乎在这里,OData 评估页码的 $top 查询参数实际上确实将结果放入列表中。

System.Web.Http.OData.dll!System.Web.Http.OData.Query.TruncatedCollection<maqsService.Controllers.TodoItemDTO>.TruncatedCollection(System.Linq.IQueryable<maqsService.Controllers.TodoItemDTO> source, int pageSize)    Unknown
        System.Web.Http.OData.dll!System.Web.Http.OData.Query.ODataQueryOptions.LimitResults<maqsService.Controllers.TodoItemDTO>(System.Linq.IQueryable<maqsService.Controllers.TodoItemDTO> queryable, int limit, out bool resultsLimited)    Unknown

我相信这里的原生 t运行sition 是 SQL 相关的。不过,我无法在反编译的源代码中找到确切的内容。

    [Native to Managed Transition]  
    [Managed to Native Transition]  
System.Web.Http.OData.dll!System.Web.Http.OData.Query.ODataQueryOptions.LimitResults(System.Linq.IQueryable queryable, int limit, out bool resultsLimited)  Unknown
        System.Web.Http.OData.dll!System.Web.Http.OData.Query.ODataQueryOptions.ApplyTo(System.Linq.IQueryable query, System.Web.Http.OData.Query.ODataQuerySettings querySettings) Unknown
        System.Web.Http.OData.dll!System.Web.Http.OData.EnableQueryAttribute.ApplyQuery(System.Linq.IQueryable queryable, System.Web.Http.OData.Query.ODataQueryOptions queryOptions)   Unknown
        System.Web.Http.OData.dll!System.Web.Http.OData.EnableQueryAttribute.ExecuteQuery(object response, System.Net.Http.HttpRequestMessage request, System.Web.Http.Controllers.HttpActionDescriptor actionDescriptor)   Unknown
        System.Web.Http.OData.dll!System.Web.Http.OData.EnableQueryAttribute.OnActionExecuted(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)  Unknown

更深入地挖掘,框架无论如何都知道如何处理 IQueryable?!好吧,当它迭代它时,IQueryable 使用它的 IQueryProvider 来解析包含的表达式(请注意,这不是编译代码,这是您使用连接和 where 子句添加的方法调用和运算符的树)。它 t运行 尽其所能将该树变形为 SQL (在本例中)。当它碰到一些它不能 t运行slate 的东西时,它会抛出一个错误或找到一种解决方法。

深入了解 IQueryProvider 是一项相当复杂的计算机科学任务。您可以开始 here with a walkthrough of creating a Query Provider. Once upon a time I wrote a query provider to transform Linq expressions into Ektron CMS API calls, and you could take a look at that here。我写了一个很好的摘要,其中包含指向关键领域的链接。

希望对您有所帮助。不确定我还能深入了解什么,谢谢你今天教我一些新东西。 我不知道这个手机 table API 是什么(仍然不清楚点)