EF:“包含”导航属性,当使用“选择”投影创建包装对象时

Posted

技术标签:

【中文标题】EF:“包含”导航属性,当使用“选择”投影创建包装对象时【英文标题】:EF: "Include" navigation property, when creating a wrapper object with "Select" projection 【发布时间】:2016-04-27 19:34:57 【问题描述】:

我在查询中使用Include 包含导航属性,以便以后不会延迟加载。但是,当我使用 Select 投影创建匿名包装对象时,它不起作用。

让我展示一个简化的例子。 实体

public class UserEntity 
    public string Name get;set;
    public virtual ICollection<UserEntity> Friends  get; set; 

查询

var entry = _dbCtx
    .Users
    .Include(x => x.Friends)
    // Select here is simplified, but it shows the wrapping
    .Select(user => new 
        User = user
    )
    .First();

// Here we have additional lazy loaded DB call
var friends = entry.User.Friends.Select(x => x.Name).ToList();

我还从生成的 SQL 中看到,导航属性不包括在内:

SELECT 
    [Limit1].[Name] AS [Name], 
    FROM ( SELECT TOP (1) 
        [Extent1].[Name] AS [Name]
        FROM [dbo].[Users] AS [Extent1]
    )  AS [Limit1]

在这种情况下是否可以Include导航属性Friends,以便User在不延迟加载的情况下获取数据?

我希望这也能起作用:

var entry = _dbCtx
    .Users
    .Select(user => new 
        User = user
    )
    .Include(x => x.User.Friends)
    .First();

但遇到异常:

InvalidOperationException:查询的结果类型既不是 EntityType 也不是具有实体元素类型的 CollectionType。只能为具有这些结果类型之一的查询指定包含路径。

我想到了一些解决方法,但它们有点棘手:

    Select 中为我们的匿名对象添加附加属性:

    var entry = _dbCtx
        .Users
        .Select(user => new 
            User = user,
            UsersFriends = user.Friends
        )
        .First();
    
    // manually copy the navigation property
    entry.User.Friends = user.UsersFriends;
    
    // Now we don't have any addition queries
    var friends = entry.User.Friends.Select(x => x.Name).ToList();
    

    还将 User 映射到 DB 级别的匿名对象,然后将属性映射到 C# 中的 UserEntity

    var entry = _dbCtx
        .Users
        .Select(user => new 
            User = new 
                Name = user.Name,
                Friends = user.Friends
            
        )
        .Take(1)
        // Fetch the DB
        .ToList()
        .Select(x => new 
            User = new UserEntity 
                Name = x.Name,
                Friends = x.Friends
            
        )
        .First();
    
    // Now we don't have any addition queries
    var friends = entry.User.Friends.Select(x => x.Name).ToList();
    

所以现在,Friends 有一个 LEFT OUTER JOIN,但两种解决方法都不是很好:

1) 附加属性和副本不是一种干净的方式。

2) 我的 UserEntity 有更多其他属性。此外,每次我们添加新属性时,我们也应该修改这里的选择器。

有没有办法实现导航属性,包括第一个示例?

感谢您的阅读,我希望有人对此有所了解。

编辑:

我将扩展实体和查询以显示一个真实的用例。

实体

public class UserEntity 
    public string Name get;set;
    public int Score get;set;
    public virtual ICollection<UserEntity> Friends  get; set; 

查询

var entry = _dbCtx
    .Users
    .Include(x => x.Friends)
    .Select(user => new 
        User = user,
        Position = _dbCtx.Users.Count(y => y.Score > user.Score)
    )
    .First();

【问题讨论】:

【参考方案1】:

不是关于_为什么_的答案,而是想要更好的代码格式...

我真的很惊讶它以这种方式工作。也许 EF 检测到您没有直接在投影中使用 Friends 属性,因此忽略了它。如果您将对象封装在 EF 查询的外部

var entry = _dbCtx
    .Users
    .Include(x => x.Friends)
    .Take(1);  // replicate "First" inside the EF query to reduce traffic
    .AsEnumerable()  // shift to linq-to-objects
    // Select here is simplified, but it shows the wrapping
    .Select(user => new 
        User = user
    )
    .First()

【讨论】:

当我们在c# 端(查询后)而不是sql 创建包装器对象时,您的示例将起作用。但我在db 级别上需要它。原因:我使用Select 执行附加子查询。有趣的事情,看我的第一个解决方法,虽然我们在查询中使用了user.Friends,但是我们必须在实现后将属性复制回实体,否则我们仍然会有延迟加载。 为什么需要在db查询中投影到匿名类型?据我所知,您最终会得到相同的结果。或者您正在计划要投影到 SQL 的 其他 方法(WhereOrderBy 等)? 查看我的帖子编辑,我为用户执行子查询,它应该在 DB 中完成,因为它遍历每个用户。因此,由于性能原因,我无法使用 c# 执行此操作。

以上是关于EF:“包含”导航属性,当使用“选择”投影创建包装对象时的主要内容,如果未能解决你的问题,请参考以下文章

EF导航属性

即使在显式包含之后,EF Core“InvalidOperationException:包含已用于非实体可查询”的导航属性也是如此

EF的导航属性

EF 6 代码首先,在导航属性上使用包含更改外键 ID 会导致“发生引用完整性约束违规”错误

无法使用自动映射器映射内部导航属性。 EF 核心

如何使用实体框架的导航属性来构建视图模型