【问题标题】:Why SELECT N + 1 with no foreign keys and LINQ?为什么 SELECT N + 1 没有外键和 LINQ?
【发布时间】:2023-12-08 15:13:01
【问题描述】:

不幸的是,我有一个没有真正外键的数据库(我计划稍后添加,但现在不希望这样做以使迁移更容易)。我已经手动编写了映射到数据库以建立关系的域对象(按照本教程http://www.codeproject.com/Articles/43025/A-LINQ-Tutorial-Mapping-Tables-to-Objects),我终于得到了正确运行的代码。但是,我注意到我现在遇到了 SELECT N + 1 问题。不是选择所有产品,而是使用以下 SQL 逐一选择它们:

SELECT [t0].[id] AS [ProductID], [t0].[Name], [t0].[info] AS [Description] 
FROM [products] AS [t0] 
WHERE [t0].[id] = @p0 
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [65] 

控制器:

    public ViewResult List(string category, int page = 1)
    {
        var cat = categoriesRepository.Categories.SelectMany(c => c.LocalizedCategories).Where(lc => lc.CountryID == 1).First(lc => lc.Name == category).Category;
        var productsToShow = cat.Products;
        var viewModel = new ProductsListViewModel
        {
            Products = productsToShow.Skip((page - 1) * PageSize).Take(PageSize).ToList(),
            PagingInfo = new PagingInfo
            {
                CurrentPage = page,
                ItemsPerPage = PageSize,
                TotalItems = productsToShow.Count()
            },
            CurrentCategory = cat
        };
        return View("List", viewModel);
    }

由于我不确定我的 LINQ 表达式是否正确,所以我尝试使用它,但仍然得到 N+1:

var cat = categoriesRepository.Categories.First();

领域对象:

[Table(Name = "products")]
public class Product
{
    [Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
    public int ProductID { get; set; }

    [Column]
    public string Name { get; set; }

    [Column(Name = "info")]
    public string Description { get; set; }

    private EntitySet<ProductCategory> _productCategories = new EntitySet<ProductCategory>();
    [System.Data.Linq.Mapping.Association(Storage = "_productCategories", OtherKey = "productId", ThisKey = "ProductID")]
    private ICollection<ProductCategory> ProductCategories
    {
        get { return _productCategories; }
        set { _productCategories.Assign(value); }
    }

    public ICollection<Category> Categories
    {
        get { return (from pc in ProductCategories select pc.Category).ToList(); }
    }
}

[Table(Name = "products_menu")]
class ProductCategory
{
    [Column(IsPrimaryKey = true, Name = "products_id")]
    private int productId;
    private EntityRef<Product> _product = new EntityRef<Product>();
    [System.Data.Linq.Mapping.Association(Storage = "_product", ThisKey = "productId")]
    public Product Product
    {
        get { return _product.Entity; }
        set { _product.Entity = value; }
    }

    [Column(IsPrimaryKey = true, Name = "products_types_id")]
    private int categoryId;
    private EntityRef<Category> _category = new EntityRef<Category>();
    [System.Data.Linq.Mapping.Association(Storage = "_category", ThisKey = "categoryId")]
    public Category Category
    {
        get { return _category.Entity; }
        set { _category.Entity = value; }
    }
}

[Table(Name = "products_types")]
public class Category
{
    [Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
    public int CategoryID { get; set; }

    private EntitySet<ProductCategory> _productCategories = new EntitySet<ProductCategory>();
    [System.Data.Linq.Mapping.Association(Storage = "_productCategories", OtherKey = "categoryId", ThisKey = "CategoryID")]
    private ICollection<ProductCategory> ProductCategories
    {
        get { return _productCategories; }
        set { _productCategories.Assign(value); }
    }

    public ICollection<Product> Products
    {
        get { return (from pc in ProductCategories select pc.Product).ToList(); }
    }

    private EntitySet<LocalizedCategory> _LocalizedCategories = new EntitySet<LocalizedCategory>();
    [System.Data.Linq.Mapping.Association(Storage = "_LocalizedCategories", OtherKey = "CategoryID")]
    public ICollection<LocalizedCategory> LocalizedCategories
    {
        get { return _LocalizedCategories; }
        set { _LocalizedCategories.Assign(value); }
    }
}

[Table(Name = "products_types_localized")]
public class LocalizedCategory
{
    [Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
    public int LocalizedCategoryID { get; set; }

    [Column(Name = "products_types_id")]
    private int CategoryID;
    private EntityRef<Category> _Category = new EntityRef<Category>();
    [System.Data.Linq.Mapping.Association(Storage = "_Category", ThisKey = "CategoryID")]
    public Category Category
    {
        get { return _Category.Entity; }
        set { _Category.Entity = value; }
    }

    [Column(Name = "country_id")]
    public int CountryID { get; set; }

    [Column]
    public string Name { get; set; }
}

我试图从我的视图中注释掉所有内容,所以似乎没有任何影响。 ViewModel 看起来很简单,所以不应该有任何东西。

阅读本文 (http://www.hookedonlinq.com/LinqToSQL5MinuteOVerview.ashx) 时,我开始怀疑这可能是因为我在数据库中没有真正的外键,并且我可能需要在我的代码中使用手动连接。那是对的吗?我该怎么办?我应该从我的域模型中删除我的映射代码还是需要添加/更改它?

注意:我已经删除了一些我认为与使其更清晰地解决这个问题无关的部分代码。如有遗漏,请告诉我。

编辑:Gert Arnold 解决了 Products 中的所有 Category 被一一查询的问题。但是,我仍然遇到页面上显示的所有Products 都被逐一查询的问题。

这发生在我的视图代码中:

列表.cshtml:

@model MaxFPS.WebUI.Models.ProductsListViewModel

@foreach(var product in Model.Products) {
    Html.RenderPartial("ProductSummary", product);
}

ProductSummary.cshtml:

@model MaxFPS.Domain.Entities.Product

<div class="item">
    <h3>@Model.Name</h3>
    @Model.Description
    @if (Model.ProductSubs.Count == 1)
    {
        using(Html.BeginForm("AddToCart", "Cart")) {
            @Html.HiddenFor(x => x.ProductSubs.First().ProductSubID);
            @Html.Hidden("returnUrl", Request.Url.PathAndQuery);
            <input type="submit" value="+ Add to cart" />
        }
    }
    else
    {
        <p>TODO: länk eller dropdown för produkter med varianter</p>
    }
    <h4>@Model.LowestPrice.ToString("c")</h4>
</div>

又是 .First() 吗?我试过 .Take(1) 但我还是无法选择 ID...

编辑:我尝试将一些代码添加到我的存储库以访问 DataContext 和此代码以创建 DataLoadOptions。但它仍然会为每个 ProductSub 生成一个查询。

var dlo = new System.Data.Linq.DataLoadOptions();
dlo.LoadWith<Product>(p => p.ProductSubs);
localizedCategoriesRepository.DataContext.LoadOptions = dlo;
var productsInCategory = localizedCategoriesRepository.LocalizedCategories.Where(lc => lc.CountryID == 1 && lc.Name == category)
    .Take(1)
    .SelectMany(lc => lc.Category.ProductCategories)
    .Select(pc => pc.Product);

生成的 SQL 略有不同,查询的顺序也不同。

对于选择 ProductSub 的查询,DataLoadOptions 代码会生成名为 @x1 的变量,没有这些变量的变量名为 @p0

SELECT [t0].[products_id] AS [ProductID], [t0].[id] AS [ProductSubID], [t0].[Name], [t0].[Price]
FROM [products_sub] AS [t0] 
WHERE [t0].[products_id] = @x1

对我的查询顺序的差异表明 DataLoadOptions 实际上正在做某事,但不是我所期望的。我期望它会生成这样的东西:

SELECT [t0].[products_id] AS [ProductID], [t0].[id] AS [ProductSubID], [t0].[Name], [t0].[Price]
FROM [products_sub] AS [t0] 
WHERE [t0].[products_id] = @x1 OR [t0].[products_id] = @x2 OR [t0].[products_id] = @x3 ... and so on

【问题讨论】:

  • “我计划稍后添加 [外键],但现在不想这样做以使迁移更容易”听起来您正在用迁移问题换取编程问题。咬紧牙关,现在添加你的外键。
  • 我可能必须这样做,但我不知道该怎么做,因为数据库当前正在使用中,所以如果我必须更改影响实时网站的内容可能会很棘手.但是,我仍然希望得到有关此问题的反馈。添加外键会解决我的问题吗?
  • 据我所知,无论 FK 是否存在,LINQ-to-SQL 的行为都没有任何不同(为此,它必须查询一堆系统表以了解它们),所以这不会有什么不同。
  • 谢谢。另一件事是它会加载该类别中的所有产品,而不仅仅是显示的 4 个。这似乎也是错误的。

标签: linq foreign-keys select-n-plus-1


【解决方案1】:

它是First()。它触发它之前的部分的执行,而它之后的部分是通过在单独的查询中的延迟加载来获取的。棘手,难以发现。

这是您可以采取的措施来防止它并一次性获取所有内容:

LocalizedCategories.Where(lc => lc.CountryID == 1 && lc.Name == category)
    .Take(1)
    .SelectMany(lc => lc.Category.ProductCategories)
    .Select (pc => pc.Product)

您应该将成员 ProductCategories 设为公开。我认为最好删除派生属性Category.ProductsProduct.Categories,因为我认为只要它们的所有者具体化或寻址,它们就会触发查询。

【讨论】:

  • 感谢您的回复。我认为你在正确的轨道上。但是,使用您的代码时,我遇到了 3 个错误:1 Category does not contain a definition for ProductCategories and no extension method ProductCategories accepting a first argument of type Category could be found (missing a using directive or an assembly reference?) 2 Category.ProductCategories is inaccessible due to its protection level 3 The property or indexer Category.ProductCategories cannot be used in this context because the get accessor is inaccessible 我尝试稍微更改代码,但仍然无法正常工作。
  • 当然。它想复制cat.Products 后面的代码,但走错了行。修改它。
  • 谢谢。该代码有效,但遗憾的是我仍然遇到同样的问题。如果我的域对象没问题(我不确定),我认为这可能是延迟加载的问题。
  • 在一个查询中获取所有内容的唯一方法是使用“真实”导航属性。像 Category.Products 这样的成员在 linq 查询中会给出一个异常,比如“成员 'Product.Categories' 没有支持的 SQL 转换。”因此,为LocalizedCategories 创建一个存储库似乎是要走的路。
  • 因为这是 linq-to-sql 你应该使用DataLoadOptions