如何过滤相同类型的嵌套实体?

How to filter nested entities of the same type?

以下是我实际情况的简化版。假设我有这个 Person 实体:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    // etc.

    // Some navigation properties
    public virtual ICollection<Thing> Things { get; set; }
    // etc.
}

我写了一个扩展方法来根据一个或多个属性进行过滤:

public static IQueryable<Person> Filter(this IQueryable<Person> query,
                                        string name = null, int? thingId = null, 
                                        Foo etc = null)
{
    if (!string.IsNullOrEmpty(name))
        query = query.Where(p => p.Name.ToLower().Contains(name.ToLower()));

    if (thingId.HasValue)
        query = query.Where(p => p.Things.Count > 0 && 
                                 p.Things.Any(t => t.Id == thingId.Value));
    // etc.

    return query;
}

..我可以这样使用:

var query = context.People.Filter(name, thingId);
var filteredPeople = query.Include(p => p.Things).Include(__).OrderBy(__).ToList();

我想让 Person 成为一个嵌套实体(即每个人都有一组人)。所以,我添加了以下属性:

public virtual ICollection<Person> Children { get; set; }
[ForeignKey("Parent")]
public int? ParentId { get; set; }
public virtual Person Parent { get; set; }

现在我正在努力实现过滤逻辑。我需要的是:

对于第二个问题,我可能会尝试this answer中的解决方案,但我需要先解决第一个问题。我尝试创建如下递归表达式:

private static IQueryable<Person> FilterByName(this IQueryable<Person> query, string name)
{
    if (string.IsNullOrEmpty(name)) return query;

    Expression<Func<Person, bool>> selector = (p) => 
        p.Name.ToLower().Contains(name.ToLower()) 
        || p.Children.AsQueryable().FilterByName(name).Any();

    return query.Where(selector);
}

..但我得到一个例外,说它 "无法翻译成存储表达式".

我能想到的唯一其他解决方案是递归迭代 children 的树并尝试手动构建列表,这是低效的,因为它需要太多查询。

如何有效地过滤 Person 及其后代的集合?

我们需要创建一个实现以下目标的查询:

  1. 一个 parent 人,如果它匹配过滤器或者如果它的后代之一匹配。

  2. 一个child人只有满足以上条件才会被收录。

如果我们反过来思考,并意识到没有 parent 或 children 实体,只有人,我们可以重写我们的标准如下:

  1. 如果某个人与过滤器相匹配,我们希望包括该人及其所有祖先。

这大大简化了我们需要执行的查询并给出相同的结果。

现在,正如某些评论所指出的那样,Entity Framework 就分层查询而言并没有很多内置功能。这并不意味着我们不能使用 EF,但我们确实需要编写并执行原始 SQL 查询。

EF 仍会处理 object 映射,如果您想将实体保存到数据库,则会更改跟踪。

这是一个 non-compostable 查询,无法通过 Linq 进一步扩展,例如 .Where().Include()

public static IEnumerable<Person> FilterPeople(this DbSet<Person> people, string name)
{
    return people.FromSqlRaw(
        "WITH child AS (" +
        "    SELECT * FROM People" +
        "    WHERE Name LIKE {0}" +
        "    UNION ALL" +
        "    SELECT parent.* FROM People parent" +
        "    INNER JOIN child ON parent.Id = child.ParentId" +
        ") SELECT * FROM child",
        $"%{name}%")
        .AsEnumerable();
}

这转化为参数化查询:

exec sp_executesql N'WITH child AS (SELECT * FROM People WHERE Name LIKE @p0 UNION ALL SELECT parent.* FROM People parent INNER JOIN child ON parent.Id = child.ParentId) SELECT * FROM child',N'@p0 nvarchar(4000)',
@p0=N'%Dick%'