首页 > 解决方案 > 如何过滤相同类型的嵌套实体?

问题描述

以下是我的实际情况的简化版。假设我有这个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; }

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

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

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);
}

..但我得到一个例外,说它“不能翻译成商店表达式”

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

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

标签: c#entity-frameworklinqentity-framework-6

解决方案


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

  1. 如果父 Person 匹配过滤器或其后代之一匹配,则将包含它。

  2. 仅当满足上述条件时才包含子 Person。

如果我们颠倒我们的思维方式,并意识到没有父母或孩子实体,只有人,我们可以重写我们的标准,说如下:

  1. 如果一个人匹配过滤器,我们想要包括那个人和他的所有祖先。

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

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

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

这没有能力被 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%'

推荐阅读