c# - 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 个查询需要时间,性能低且代码太长。有没有更好的方法来编写更好的代码?
解决方案
简短的回答:如果你直到最后都不做ToList
and 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 Provider
。Expression
表示必须以某种通用格式获取哪些数据的公式;知道谁必须提供数据(通常是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
将数据传输到您的本地进程,因此您的本地进程必须使用Where
和Contains
.
唉,您忘记向我们描述您的要求(“从所有帖子中,只给我那些......”),我分析您的所有查询有点太多了,但是如果您获得最大效率你尽量保持一切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 a
和b = 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)
这对您的序列没有任何作用,只会使其变慢。
推荐阅读
- vb.net - 如何检查列表是否包含VB.Net中的另一个列表项
- symfony - 如何解决 CSRF 令牌无效
- c++ - 在运行时,std 库何时完全初始化以便在不破坏代码的情况下使用它?
- linux - 安装一个使用 wget 下载的 RPM 文件
- perl - 在 Perl 中使用 system() 命令时,如何将 STDERR 重定向到 STDOUT?2>&1 解决方案不起作用
- java - 在另一个类中创建一个类的对象?
- c# - 为什么 List.All(a.this != that) 优于 !List.Any(a.this = that)
- python - 如何允许 python 应用程序更改系统时间
- excel - 编写宏来组织大量信息
- javascript - 单击时切换导航按钮