免责声明:您要问的是 不 EF Core 5.0 自然支持,因此提供的解决方法很可能会在未来的 EF Core 版本中中断。使用它需要您自担风险,或者使用 支持的(映射到包含所需 SQL 的真实数据库视图,正如其他人所提到的)。
现在,问题。首先,您要映射到 SQL 并在关系中使用的实体类型不能是无键的。只是因为目前keyless entity types
仅支持导航映射功能的一个子集,具体来说:
- 他们可能永远不会充当关系的主要目的。
- 他们可能没有导航到拥有的实体
- 它们只能包含指向常规实体的参考导航属性。
- 实体不能包含无键实体类型的导航属性。
在您的情况下,Customer 违反了最后一条规则,将导航属性定义为无键实体。但是没有它,您将无法使用Include,这是所有这一切的最终目标。
没有解决该限制的方法。即使您使用一些技巧来映射关系并获得正确的 SQL 翻译,仍然不会加载导航属性,因为所有与 EF Core 相关的数据加载方法都依赖于更改跟踪,并且它需要带有键的实体。
因此,实体必须是“正常的”(带有键)。这没有问题,因为查询具有定义一对一关系的唯一列。但是,这遇到了另一个当前 EF Core 限制 - 在模型完成期间,您会得到 NotImplemented 异常,因为普通实体映射到 SqlQuery。不幸的是,这是关系模型终结中许多地方使用的 static 函数,这也是一个 static 方法,因此实际上不可能从外部拦截和修复它。
一旦您了解了问题(哪些支持,哪些不支持),下面是解决方法。支持的映射是要查看的正常实体。因此,我们将使用它(ToView 而不是失败的ToSqlQuery),而不是名称将提供包含在() 中的 SQL,以便能够从关联的 EF Core 元数据中识别和提取它。请注意,EF Core 不会验证/关心您在 ToTable 和 ToView 方法中提供的名称 - 只是它们是否为 null。
然后我们需要插入 EF Core 查询处理管道,并将“视图名称”替换为实际的 SQL。
以下是上述想法的实现(将其放在您的EF Core项目中的某个代码文件中):
namespace Microsoft.EntityFrameworkCore
{
using Metadata.Builders;
using Query;
public static class InlineSqlViewSupport
{
public static DbContextOptionsBuilder AddInlineSqlViewSupport(this DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ReplaceService<ISqlExpressionFactory, CustomSqlExpressionFactory>();
public static EntityTypeBuilder<TEntity> ToInlineView<TEntity>(this EntityTypeBuilder<TEntity> entityTypeBuilder, string sql)
where TEntity : class => entityTypeBuilder.ToView($"({sql})");
}
}
namespace Microsoft.EntityFrameworkCore.Query
{
using System.Linq.Expressions;
using Metadata;
using SqlExpressions;
public class CustomSqlExpressionFactory : SqlExpressionFactory
{
public override SelectExpression Select(IEntityType entityType)
{
var viewName = entityType.GetViewName();
if (viewName != null && viewName.StartsWith("(") && viewName.EndsWith(")"))
{
var sql = viewName.Substring(1, viewName.Length - 2);
return Select(entityType, new FromSqlExpression("q", sql, NoArgs));
}
return base.Select(entityType);
}
private static readonly Expression NoArgs = Expression.Constant(new object[0]);
public CustomSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) : base(dependencies) { }
}
}
前两种方法只是为了方便 - 一种用于添加必要的管道,另一种用于在名称中编码 sql。
实际工作在第三个类中,它替换了标准 EF Core 服务之一,拦截了负责表/视图/TVF 表达式映射的 Select 方法,并将特殊视图名称转换为 SQL 查询。
有了这些助手,您就可以按原样使用您的示例模型和DbSets。您只需将以下内容添加到派生的 DbContext 类中:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// ...
optionsBuilder.AddInlineSqlViewSupport(); // <--
}
并使用以下流畅的配置:
modelBuilder.Entity<MaxOrder>(builder =>
{
builder.HasKey(e => e.CustomerId);
builder.ToInlineView(
@"SELECT CustomerId, SUM(Amount) AS TotalAmount
FROM Orders O
WHERE Id = (SELECT MAX(Id)
FROM Orders
WHERE CustomerId = O.CustomerId)
GROUP BY CustomerId");
});
现在
var test = dbContext.Customers
.Include(x => x.MaxOrder)
.ToList();
将运行无错误并生成类似 SQL
SELECT [c].[Id], [c].[Name], [q].[CustomerId], [q].[TotalAmount]
FROM [Customers] AS [c]
LEFT JOIN (
SELECT CustomerId, SUM(Amount) AS TotalAmount
FROM Orders O
WHERE Id = (SELECT MAX(Id)
FROM Orders
WHERE CustomerId = O.CustomerId)
GROUP BY CustomerId
) AS [q] ON [c].[Id] = [q].[CustomerId]
更重要的是,将正确填充Customer.MaxOrder 属性。任务完成:)