【问题标题】:EF FromSql() with related entitiesEF FromSql() 与相关实体
【发布时间】:2023-03-24 21:58:01
【问题描述】:

免责声明:这些要求不是我设定的,除非这是不可能完成的任务,否则我无法说服我的老板。

假设我们有两个实体:ItemItemTranslation

public class Item
{
    public int Id { get; set; }

    public string Description { get; set; }

    public virtual ICollection<Item> Children { get; set; }
    public virtual Item Parent { get; set; }
    public virtual ICollection<ItemTranslation> Translations { get; set; }
}

public class ItemTranslation
{
    public int Id { get; set; }

    public string CultureId { get; set; }
    public string Description { get; set; }

    public virtual Item Item { get; set; }
}

要求Item.Description应根据默认选择的语言填写,但也允许根据用户的需要指定。 Item.Description 列实际上并不存在于数据库中。

在 SQL 中这很容易:您所要做的就是像这样查询两个表

SELECT [Item].[Id], [ItemTranslation].[Description], [Item].[ParentId] 
FROM [Item] 
LEFT JOIN [ItemTranslation] ON [Item].[Id] = [ItemTranslation].[ItemId]  
WHERE [CultureId] = {cultureId}

或者根据您的实现使用 OUTER APPLY。我已将此查询添加到 Entity Framework 中内置的 .FromSql() 函数中。

将所有这些放在一个 OData API 中,这对于一个 Item 来说一切正常。但是,一旦您开始使用 $expand(在幕后是一种 .Include()),它就不再起作用了。为相关实体发送到数据库的查询不再包含我在.FromSql() 中指定的 SQL。只有第一个查询可以。除此之外,当您从不同的控制器查询Item 时,例如ItemTranslation 这也将不再起作用,因为 .FromSql() 仅应用于另一个控制器。

我可以编写一个查询拦截器,它简单地将生成的 SQL 替换为实体框架,并将 FROM [Item] 替换为 FROM [Item] LEFT JOIN [ItemTranslation] ON [Item].[Id] = [ItemTranslation].[ItemId] WHERE [CultureId] = {cultureId},但我想知道是否有比这更好的实现。甚至可能对模型进行重新设计。我愿意接受建议。

【问题讨论】:

  • 该 SQL 实际上是无效的,因为 DescriptionId 在两个表之间都是不明确的。
  • 您为什么要放弃以更传统的方式使用 EF 生成的 SQL? (...或者你为什么使用FromSql 而不是 LINQ?)
  • ...您的需求中似乎没有任何内容需要回退到制作自己的 SQL。正如您所发现的那样,这样做有缺点。
  • @spender 什么意思,我必须根据用户传递的 CultureId 填写 Item.Description。如果没有 .FromSql() 并且在 OData API 上下文中,我将如何实现这一点,这意味着我必须返回 IQueryable
  • 您知道针对 EF 的 LINQ 查询也会返回 IQueryable&lt;T&gt;?

标签: c# entity-framework odata


【解决方案1】:

FromSql 有一些limitations。我怀疑这就是 Include 不起作用的原因。

但是一旦你使用了 EF,为什么还要搞 SQL 呢?该查询有哪些困难阻止您在 LINQ 中执行此操作?左加入可能吗?

from item in ctx.Items
from itemTranslation in ctx.ItemTranslations.Where(it => it.Item.Id == item.Id).DefaultIfEmpty()
where itemTranslation.CultureId == cultureId
select new { item.Id, itemTranslation.Description, ParentId = item.Parent.Id };

更新

再次讨论这个问题,我发现了另一个问题。 Include 仅适用于 IQueryable,其中 T 是其导航属性已正确映射的实体。现在,从这个角度来看,如果您使用 FromSql 或 LINQ 生成某个投影的 IQueryable 而不是实体,则无关紧要,Include em> 出于明显的原因无法正常工作。

为了能够包含 ItemTranslation 实体,您的操作方法应如下所示:

[Queryable]
public IQueryable<Item> GetItems()
{
    return db.Items;
}

因此框架可以对您返回的 IQueryable 执行 $expand。但是,这将包括所有项目翻译,而不仅仅是具有所需文化的翻译。如果我理解正确,这就是你的核心问题。

很明显,您不能将此区域性过滤器应用于 IQueryable。但您不应该这样做,因为这是通过 OData 中的 $filter 实现的:

GET https://.../Items/$expand=Translations&$filter=Translations/CultureId eq culture

【讨论】:

  • OData 需要 IQueryable 资源。它还允许传递 $expand ,它将查询相关数据并在幕后执行某种 .Include() 。这意味着可以从没有此 LINQ 的 ItemTranslation 控制器查询 Item。
  • 您的 FromSql 和我的 LINQ 代码都返回一个 IQueryable。但是,您可以对它们做些什么 - 例如关于查询组成。您是否尝试过我建议的 LINQ 而不是 FromSql?我不清楚这一切与控制器有什么关系?
  • 是的,除了 Item.Description 还应该包含从 ItemTranslation 获取的描述,其中 CultureId 等于用户传递给 API 的内容之外,这基本上几乎是要求。这是最难的部分。
  • 你为什么需要Item.Description?您在 Item.Translations[0].Description 中有该信息(当如上所示应用 CultureId 的 $expand 和 $filter 时)。问题是你试图扁平化你的对象层次结构,本质上是把你的 Item 实体变成一个 DTO。我再说一遍:$expand 需要一个正确映射的实体才能在后台发挥其 Include 的魔力。因此,您要么返回平面对象而忘记 $expand(但为什么要使用 OData 呢?),要么返回一个实体, $expand 将按预期工作。你不能同时拥有两者。
  • 您也许可以使用视图或 EF 的类似功能来解决此问题,但我认为这宁愿属于黑客攻击的范畴,并且会破坏 OData 的目的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-04
  • 1970-01-01
相关资源
最近更新 更多