首页 > 解决方案 > Linq Multiple where 基于不同的条件

问题描述

在搜索页面中,我有一些基于它们的选项搜索查询必须不同。我写了这个:

int userId = Convert.ToInt32(HttpContext.User.Identity.GetUserId());

var followings = (from f in _context.Followers
                  where f.FollowersFollowerId == userId && f.FollowersIsAccept == true
                  select f.FollowersUserId).ToList();

int value;

if (spto.Page == 0)
{
    var post = _context.Posts.AsNoTracking().Where(p => (followings.Contains(p.PostsUserId) || p.PostsUser.UserIsPublic == true || p.PostsUserId == userId) && p.PostIsAccept == true).Select(p => p).AsEnumerable();

    if(spto.MinCost != null)
    {
        post = post.Where(p => int.TryParse(p.PostCost, out value) && Convert.ToInt32(p.PostCost) >= spto.MinCost).Select(p => p);
    }

    if (spto.MaxCost != null)
    {
        post = post.Where(p => int.TryParse(p.PostCost, out value) && Convert.ToInt32(p.PostCost) <= spto.MaxCost).Select(p => p);
    }

    if (spto.TypeId != null)
    {
        post = post.Where(p => p.PostTypeId == spto.TypeId).Select(p => p);
    }

    if (spto.CityId != null)
    {
        post = post.Where(p => p.PostCityId == spto.CityId).Select(p => p);
    }

    if (spto.IsImmidiate != null)
    {
        post = post.Where(p => p.PostIsImmediate == true).Select(p => p);
    }

    var posts = post.Select(p => new
     {
         p.Id,
         Image = p.PostsImages.Select(i => i.PostImagesImage.ImageAddress).FirstOrDefault(),
         p.PostCity.CityName,
         p.PostType.TypeName
     }).AsEnumerable().Take(15).Select(p => p).ToList();

    if (posts.Count != 0)
        return Ok(posts);

    return NotFound();

在这种情况下,我有 6 个查询需要时间,性能低且代码太长。有没有更好的方法来编写更好的代码?

标签: c#performancelinq

解决方案


简短的回答:如果你直到最后都不做ToListand AsEnumerable,那么你只会在你的 dbContext 上执行一个查询。

所以保留一切IQueryable<...>,直到你创建List<...> posts

var posts = post.Select(p => new
 {
     p.Id,
     Image = p.PostsImages
              .Select(i => i.PostImagesImage.ImageAddress)
              .FirstOrDefault(),
     p.PostCity.CityName,
     p.PostType.TypeName,
 })
 .Take(15)
 .ToList();

IQueryable 和 IEnumerable

由于跳过所有 ToList / AsEnumerable 有助于提高性能,您需要了解 anIEnumerable<...>和 an之间的区别IQueryable<...>

IEnumerable

实现的类的对象IEnumerable<...>表示枚举该对象可以产生的序列的潜力。

该对象包含生成序列的所有内容。一旦您请求序列,您的本地进程将执行代码以生成序列。

在低级别,您通过使用GetEnumerator并重复调用来生成序列MoveNext。只要MoveNext返回true,序列中就有下一个元素。您可以使用 property 访问下一个元素Current

枚举序列是这样完成的:

IEnumerable<Customer> customers = ...
using (IEnumarator<Customer> customerEnumerator = customers.GetEnumerator())
{
    while (customerEnumerator.MoveNext())
    {
        // there is still a Customer in the sequence, fetch it and process it
        Customer customer = customerEnumerator.Current;
        ProcessCustomer(customer);
    }
}

好吧,这是很多代码,所以 C# 的创建者发明了foreach,它将完成大部分代码:

foreach (Customer customer in customers)
    ProcessCustomer(customer);

既然您foreach知道foreach.

重要的是要记住,anIEnumerable<...>是由您的本地进程处理的。IEnumerable<...>可以调用本地进程可以调用的每个方法。

可查询的

实现的类的对象IQueryable<...>看起来很像IEnumerable<...>,它也代表了产生类似对象的可枚举序列的潜力。然而,不同之处在于另一个进程应该提供数据。

为此,该IQueryable<...>对象包含 anExpression和 a ProviderExpression表示必须以某种通用格式获取哪些数据的公式;知道谁必须提供数据(通常是Provider数据库管理系统),以及使用什么语言与这个 DBMS 通信(通常是 SQL)。

只要您连接 LINQ 方法,或者您自己的仅返回的方法,只会IQueryable<...>更改Expression。不执行查询,不联系数据库。连接这样的语句是一种快速的方法。

仅当您开始枚举时,无论是在其最低级别 usingGetEnumerator / MoveNext / Current还是更高级别 using foreach,才会将其Expression发送到Provider,后者会将其转换为 SQL 并从数据库中获取数据。返回的数据表示为调用者的可枚举序列。

请注意,有一些 LINQ 方法不返回IQueryable<TResult>,而是返回List<TResult>TResult、布尔值或 int 等:ToList / FirstOrDefault / Any / Count / etc. Those methods will deep inside call GetEnumerator / MoveNext / Current`; 所以这些是将从数据库中获取数据的方法。

回到你的问题

数据库管理系统在处理数据方面进行了极大的优化:获取、排序、过滤等。数据库查询中较慢的部分之一是将获取的数据传输到本地进程。

因此,明智的做法是让 DBMS 尽可能多地处理数据库,并且只将数据传输到您实际计划使用的本地进程。

因此,如果您的本地进程不使用获取的数据,请尽量避免使用 ToList。在您的情况下:您将其传输followings到本地进程,只是将其传输回IQueryable.Contains方法中的数据库。

此外,(这取决于您使用的框架),AsEnumerable将数据传输到您的本地进程,因此您的本地进程必须使用WhereContains.

唉,您忘记向我们描述您的要求(“从所有帖子中,只给我那些......”),我分析您的所有查询有点太多了,但是如果您获得最大效率你尽量保持一切IQueryable<...>

可能存在一些问题Int.TryParse(...)。您的提供者可能不知道如何将其转换为 SQL。有几种可能的解决方案:

  • 显然PostCost代表一个数字。考虑将其存储为数字。如果它是一个数量(价格或其他东西,小数位数有限的东西),请考虑将其存储为小数。
  • 如果您确实无法说服项目负责人将数字存储为小数,则要么搜索他们制作适当数据库的工作,要么考虑创建一个存储过程,将 PostCost 中的字符串转换为小数/整数。
  • 如果您只使用十五个元素,请使用IQueryable.Take(15),而不是IEnumerable.Take(15).

进一步优化:

int userId = 

var followerUserIds = _context.Followers
    .Where(follower => follower.FollowersFollowerId == userId
                    && follower.FollowersIsAccept)
    .Select(follower => follower.FollowersUserId);

换句话说:使以下 IQueryable,但不要执行它:“从所有追随者中,只保留那些被接受并且追随者追随者 ID 等于用户 ID 的追随者。从剩余的追随者中,获取追随者用户 ID”。

如果页面为零,您似乎只打算使用它。如果页面不为零,为什么还要创建此查询?

顺便说一句,永远不要使用像where a == true,甚至更糟的语句:if (a == true) then b == true else b == false,这会给读者一种您难以理解布尔值的印象,只需使用:where ab = a

接下来,您决定创建一个包含零个或多个帖子的查询,并认为给它一个单数名词作为标识符是个好主意:post

var post = _context.Posts
    .Where(post => (followings.Contains(post.PostsUserId) 
                           || post.PostsUser.UserIsPublic
                           || post.PostsUserId == userId)
                && post.PostIsAccept);

Contains将导致与Followers表的联接。如果您只将已接受的帖子与关注者表一起加入,它可能会更有效。因此,在您决定加入之前,首先检查 PostIsAccept 和其他谓词:

.Where(post => post.PostIsAccept
            && (post.PostsUser.UserIsPublic || post.PostsUserId == userId
                || followings.Contains(post.PostsUserId));

所有未接受的帖子都不必与以下内容一起加入;取决于您的 Provider 是否足够聪明:它不会加入所有公共用户,也不会加入具有 userId 的用户,因为它知道它已经通过了过滤器。

考虑使用 a Contains,而不是Any

在我看来,您想要以下内容:

我有一个用户 ID;给我所有接受的帖子,要么来自这个用户,要么来自公共用户,或者有一个接受的追随者

var posts = dbContext.Posts
    .Were(post => post.IsAccepted
       && (post.PostsUser.UserIsPublic || post.PostsUserId == userId
           || dbContext.Followers
                       .Where(followers => ... // filter the followers as above)
                       .Any());

请注意:我还没有执行查询,我只是更改了表达式!

在第一次定义帖子之后,您可以根据 spto 的各种值进一步过滤帖子。你可以考虑做一个大查询,但我认为这不会加快这个过程。它只会使它更不可读。

最后:为什么使用:

.Select(post => post)

这对您的序列没有任何作用,只会使其变慢。


推荐阅读