大家好,我正在开发一个API,它从一个数据库中返回一个包含餐厅详细信息的菜品,其中包含了餐馆和他们的菜肴。我想知道,通过将第一个查询转换为第二个查询,以下内容是否提高了查询的效率:
from res in _context.Restaurant
join resdish in _context.RestaurantDish
on res.Id equals resdish.RestaurantId
where resdish.RestaurantDishId == dishId第二:
from resdish in _context.RestaurantDish
where resdish.RestaurantDishId == dishId
join res in _context.Restaurant
on resdish.RestaurantId equals res.Id我之所以争论这件事,是因为我觉得自己就像第二版的过滤器一样,过滤到单一餐厅的菜上,然后加入它,而不是加入所有的菜,然后过滤。这是正确的吗?
发布于 2022-11-24 21:38:54
您可以在数据库中使用分析器来捕获这两种情况下的SQL,或者检查EF生成的SQL,您可能会发现这两种情况下的SQL实际上是相同的。归根结底,读者(开发人员)是如何解释逻辑意图的。
就用EF构建高效查询而言,EF是一种ORM,这意味着它提供了在面向对象模型和关系数据模型之间的映射。它不仅仅是一个API,可以将Linq转换成SQL。编写简单高效查询的部分功能是通过使用导航属性和投影。餐点将被视为某一特定餐厅的属性,而餐厅的菜单上则有许多菜肴。这在数据库中形成一对多的关系,导航属性可以在对象模型中映射这种关系:
public class Restaurant
{
[Key]
public int RestaurantId { get; set; }
// ... other fields
public virtual ICollection<Dish> Dishes { get; set; } = new List<Dish>();
}
public class Dish
{
[Key]
public int DishId { get; set; }
//[ForeignKey(nameof(Restaurant))]
//public int RestaurantId { get; set; }
public virtual Restaurant Restaurant { get; set; }
}Restaurant ID的FK属性是可选的,可以配置为使用Shadow属性。(其中一个EF知道并生成,但不在实体中公开)我建议对FKs使用阴影属性,主要是为了避免关系中的两个真相来源。(dish.RestaurantId和dish.Restaurant.RestaurantId)除非重新加载实体,否则更改FK不会自动更新关系,而更新关系在调用SaveChanges之前不会自动更新FK。
现在,如果你想要一种特别的菜,而且它是与之相关的餐厅:
var dish = _context.Dishes
.Include(d => d.Restaurant)
.Single(d => d.DishId == dishId);这将获取两个实体。注意,现在不需要像使用SQL那样手动编写Joins了。EF支持联接,但它只适用于模式未正确标准化/关系且需要映射松散连接的实体/表的非常罕见的情况。(例如使用"OwnerId“的表,该表可以连接到"This”或基于OwnerType之类的鉴别器的“那个”表。)
如果您关闭了.Include(d => d.Restaurant)并在DbContext上启用了延迟加载,那么EF将尝试在代码第一次尝试访问dish.Restaurant时自动加载Restaurant。这提供了一个安全网,但在许多情况下可能会招致一些严重的性能损失,因此应该避免或将其视为安全网,而不是拐杖。
在处理单个实体及其相关数据时,热切加载工作良好,在这些数据中,您需要处理这些关系。例如,如果我想加载一家餐厅并检查,添加/移除盘子,或加载一个Dish,并可能更改该餐厅。然而,在EF和SQL如何在幕后提供相关数据时,急切的加载可能会付出很大的代价。
默认情况下,当您使用Include时,EF将在关联表之间添加一个内连接或左连接。这在涉及的表之间创建了笛卡儿积。如果您有100家餐馆,平均每个餐厅有30道菜,并选择所有100家渴望加载其菜肴的餐馆,则结果查询为3000行。现在,如果一个Dish有类似于Review的内容,并且每个菜平均有5个评论,并且您急切地加载菜品和评论,那么这将是包含15000行的所有三个表的每个列的结果集。希望你能体会到这是如何迅速地失去控制的。然后,EF通过笛卡儿,填充对象图中的相关实体。这可能会导致一些问题,为什么“我的查询在SSMS中运行得很快,而在EF中运行得很慢”,因为EF有很多工作要做,特别是如果它一直在跟踪来自餐馆、菜肴和/或评论的引用来扫描和提供。稍后版本的EF可以通过使用查询拆分来稍微减轻这一点,因此,EF可以使用多个单独的SELECT语句来获取相关数据,这些语句可以执行和处理相当快的速度,但仍然需要大量的数据通过连接,并且需要内存来处理。
但是,大多数情况下,对于每个相关实体,您不需要所有行,也不需要所有列。这就是投影出现的地方,比如使用Select。当我们撤回我们的餐馆名单,我们可能想列出在一个特定城市的餐厅连同他们的前5菜根据用户的评论。我们只需要在这些结果中显示RestaurantId & Name,以及Dish名称和正面评论的#。与从每个表中加载每一列不同,我们可以为此摘要视图为餐馆和菜肴定义视图模型,并将实体投影到这些视图模型中:
public class RestaurantSummaryViewModel
{
public int RestaurantId { get; set; }
public string Name { get; set; }
public ICollection<DishSummaryViewModel> Top5Dishes { get; set; } = new List<DishSummaryViewModel>();
}
public class DishSummaryViewModel
{
public string Name { get; set; }
public int PositiveReviewCount {get; set; }
}
var restaurants = _context.Restaurants
.Where(r => r.City.CityId == cityId)
.OrderBy(r => r.Name)
.Select(r => new RestaurantSummaryViewModel
{
RestaurantId = r.RestaurantId,
Name = r.Name,
Top5Dishes = r.Dishes
.OrderByDescending(d => d.Reviews.Where(rv => rv.Score > 3).Count())
.Select(d => new DishSummaryViewModel
{
Name = d.Name,
PositiveReviewCount = d.Reviews.Where(rv => rv.Score > 3).Count()
}).Take(5)
.ToList();
}).ToList();注意,上面的Linq示例不使用Join,甚至不使用Include。如果您遵循一组基本规则,以确保EF能够计算出您想要到SQL中的项目,那么您就可以完成一段公平的操作,从而产生更高效的查询。上面的语句将生成跨相关表运行的SQL,但只返回填充所需的视图模型所需的字段。这使您可以根据最需要的数据对索引进行优化,还可以减少跨越线路的数据量,以及DB和app服务器上的内存使用量。像Automapper和它的ProjectTo方法这样的库可以进一步简化上述语句,配置如何一次选择到所需的视图模型,然后将整个Select( ... )替换为一个ProjectTo<RestaurantSummaryViewModel>(config),其中"config“是对Automapper配置的引用,它可以解决如何将餐馆及其关联实体转换为所需的视图模型。
在任何情况下,它都应该为您提供一些使用EF的途径,并了解它可以为表带来什么,从而产生(希望:)容易理解和高效的查询表达式。
https://stackoverflow.com/questions/74554628
复制相似问题