如何在 EF Core 5 中配置自定义 SQL 的导航属性

2024-05-04

我有一个自定义 SQL 语句来获取客户的最大订单。我没有名为MaxOrders- 这只是一个自定义查询。

我正在使用以下方式获取客户记录和相关对象Include

dbcontext.Customers.Include(x => x.MaxOrder)

我想知道如何为这种情况配置导航属性。

客户等级

public class Customer 
{
    public int Id { get; set;}
    public string Name { get; set;}

    public MaxOrder MaxOrder { get; set;}
}

最大订单类

public class MaxOrder 
{
    public int CustomerId { get; set;}
    public decimal TotalAmount { get; set;}

    public Customer Customer { get; set;}
}

数据库上下文

public DbSet<Customer> Customers { get; set; }
public DbSet<MaxOrder> MaxOrders{ get; set; }

模型构建器

modelBuilder.Entity<MaxOrder>()
            .HasNoKey()
            .ToView(null)
            .ToSqlQuery(@"SELECT CustomerId, SUM(Amount) AS TotalAmount 
                          FROM Orders O 
                          WHERE Id = (SELECT MAX(Id) 
                                      FROM Orders 
                                      WHERE CustomerId = O.CustomerId)
                          GROUP BY CustomerId")

免责声明:你要问的是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财产。任务完成:)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 EF Core 5 中配置自定义 SQL 的导航属性 的相关文章

随机推荐