免责声明:你要问的是notEF Core 5.0 自然支持,因此提供的解决方法很可能会在未来的 EF Core 版本中崩溃。使用它需要您自担风险,或者使用什么is支持(映射到real包含所需 SQL 的数据库视图,正如其他人提到的)。
现在,问题来了。首先,您想要映射到 SQL 并在关系中使用的实体类型cannot无钥匙。原因很简单,因为目前无键实体类型 https://learn.microsoft.com/en-us/ef/core/modeling/keyless-entity-types?tabs=data-annotations#keyless-entity-types-characteristics
仅支持导航映射功能的子集,具体来说:
- 他们可能永远不会成为一段关系的主要目的。
- 他们可能没有导航到拥有的实体
- 它们只能包含指向常规实体的引用导航属性。
- 实体不能包含无键实体类型的导航属性。
就你而言,Customer
通过将导航属性定义为无键实体,违反了最后一条规则。但没有它你将无法使用Include
,这是这一切的最终目标。
该限制没有解决方法。即使使用一些黑客手段映射关系并获得正确的 SQL 翻译,导航属性仍然不会被加载,因为所有 EF Core 相关的数据加载方法都依赖于更改跟踪,并且它需要带有键的实体。
因此,该实体必须是“正常的”(带有密钥)。这没有问题,因为查询具有定义一对一关系的唯一列。然而,这遇到了当前 EF Core 的另一个限制 - 您会得到NotImplemented
在模型最终确定期间映射到 SqlQuery 的正常实体的异常。不幸的是这是在里面static关系模型终结里面很多地方都会用到这个函数,这也是一个static方法,因此实际上不可能从外部拦截和修复它。
一旦您了解了问题(支持什么、不支持什么),下面就是解决方法。支持的映射是要查看的普通实体。所以我们将使用它(ToView
而不是失败ToSqlQuery
),但将提供包含在其中的 SQL,而不是名称()
能够从关联的 EF Core 元数据中识别并提取它。请注意,EF Core 不会验证/关心您在中提供的名称是什么ToTable
and ToView
方法 - 只是它们是否是null
or not.
然后我们需要插入 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 服务之一,拦截Select
方法负责表/视图/TVF表达式映射,并将特殊视图名称转换为SQL查询。
有了这些助手,您就可以使用示例模型并DbSet
就这样。您所需要做的就是将以下内容添加到您的派生中DbContext
class:
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");
});
Now
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
财产。任务完成:)