在一个数据库中查找基于多个属性的许多重复项

时间:2017-03-30 13:27:43

标签: c# entity-framework

我尝试对数据库执行检查,以查看数据库检查中是否存在两个属性的组合(预先唯一约束检查​​以获得更好的UX)。无论您尝试检查多少,都可以轻松地使用单一属性进行检查。我无法找到如何使用可枚举的多个属性来执行此操作。

public class Foo
{
    public int Id { get; set; }
    public int Bar { get; set; }
    public int Bat { get; set; }
    public string Name { get; set; }
    //...
}

public class FooDupeCheckModel
{
    public int Bar { get; set; }
    public int Bat { get; set; }
}

public IEnumerable<FooDupeCheckModel> MatchExists(IEnumerable<FooDupeCheckModel> matches)
{
    return _db.Foos
              .Where(f => matches.Any(m => m.BarId == f.BarId &&
                                           m.BatId == f.BatId))
              .Select(f => new FooDupeCheckModel
              {
                  BarId = f.BarId,
                  BatId = f.BatId
              });
}

不幸的是,这会产生异常,因为无法将复杂属性matches转换为SQL脚本。只有原始类型才能包含在查询中。

我还尝试将matches转换为多维数组,然后在查询中使用它,但查询中不支持索引。 .First()不允许使用它们。

public IEnumerable<FooDupeCheckModel> MatchExists(IEnumerable<FooDupeCheckModel> matches)
{
    var matchArray = matches.Select(m => new [] {m.BarId, m.BatId})
                            .ToArray();

    return _db.Foos
              .Where(f => matchArray.Any(m => m[0] == f.BarId &&
                                              m[1] == f.BatId))
              .Select(f => new FooDupeCheckModel
              {
                  BarId = f.BarId,
                  BatId = f.BatId
              });
}

这可能是这种利基案例中的一种情况,或者需要一个对于Entity Framework来说过于复杂的SQL查询而不可能。如果 可能,那么如果其他人遇到同样的问题,这将非常有用。

我通过循环并为matches中的每个元素调用数据库来解决这个问题,但是如果我可以在一个数据库调用中执行此操作,那就更快了。

2 个答案:

答案 0 :(得分:1)

SQL Server不支持比较元组。但是,您可以通过OR-ing比较来比较两个+属性:

SELECT *
FROM Foo f
WHERE
(
    (f.Bar = 1 AND f.Bat = 1)
    OR
    (f.Bar = 3 AND f.Bat = 2)
)

不幸的是,没有简单的方法可以建立一个涉及OR的IQueryable<T>。但是,您可以使用表达式树构建器来构建它:

var models = new FooDupeCheckModel[]
{
    new FooDupeCheckModel() { Bar = 1, Bat = 2 },
    new FooDupeCheckModel() { Bar = 1, Bat = 3 }
};
var comparison = getComparison(models);

IQueryable<Foo> foos = new Foo[]
{
    new Foo() { Bar = 1, Bat = 1 },
    new Foo() { Bar = 1, Bat = 2 },
    new Foo() { Bar = 1, Bat = 3 },
    new Foo() { Bar = 1, Bat = 4 }
}.AsQueryable();
var results = foos.Where(comparison).ToArray();

...

private static Expression<Func<Foo, bool>> getComparison(IEnumerable<FooDupeCheckModel> models)
{
    ParameterExpression pe = Expression.Parameter(typeof(Foo), "f");
    var ands = models.Select(m =>
    {
        // Compare Bars
        Expression pBarId = Expression.Property(pe, "Bar");
        Expression vBarId = Expression.Constant(m.Bar);
        Expression bar = Expression.Equal(pBarId, vBarId);

        // Compare Bats
        Expression pBatId = Expression.Property(pe, "Bat");
        Expression vBatId = Expression.Constant(m.Bat);
        Expression bat = Expression.Equal(pBatId, vBatId);

        Expression and = Expression.And(bar, bat);
        return and;
    }).ToArray();
    if (ands.Length == 0)
    {
        return Expression.Lambda<Func<Foo, bool>>(Expression.Constant(true), pe);
    }
    else
    {
        Expression ors = ands.First();
        foreach (Expression and in ands.Skip(1))
        {
            ors = Expression.OrElse(ors, and);
        }
        return Expression.Lambda<Func<Foo, bool>>(ors, pe);
    }
}

这适用于内存数据结构。再次测试SQL Server;它应该生成相应的SQL。

这是一个支持任意数量的具有任何名称的属性的版本:

public class Foo
{
    public int Id { get; set; }
    public int Bar { get; set; }
    public int Bat { get; set; }
    public string Name { get; set; }
}

public class FooDupeCheckModel
{
    public int Bar { get; set; }
    public int Bat { get; set; }
}

static void Main(string[] args)
{
    var models = new FooDupeCheckModel[]
    {
        new FooDupeCheckModel() { Bar = 1, Bat = 2 },
        new FooDupeCheckModel() { Bar = 1, Bat = 3 }
    };
    var comparison = getComparison<Foo, FooDupeCheckModel>(
        models,
        compare((Foo f) => f.Bar, (FooDupeCheckModel f) => f.Bar),
        compare((Foo f) => f.Bat, (FooDupeCheckModel f) => f.Bat)
    );

    IQueryable<Foo> foos = new Foo[]
    {
        new Foo() { Bar = 1, Bat = 1 },
        new Foo() { Bar = 1, Bat = 2 },
        new Foo() { Bar = 1, Bat = 3 },
        new Foo() { Bar = 1, Bat = 4 }
    }.AsQueryable();
    var query = foos.Where(comparison);
    var results = query.ToArray();
}

private class PropertyComparison
{
    public PropertyInfo FromProperty { get; set; }

    public PropertyInfo ToProperty { get; set; }
}

private static PropertyComparison compare<TFrom, TFromValue, TTo, TToValue>(
    Expression<Func<TFrom, TFromValue>> fromAccessor, 
    Expression<Func<TTo, TToValue>> toAccessor)
{
    MemberExpression fromMemberAccessor = (MemberExpression)fromAccessor.Body;
    PropertyInfo fromProperty = (PropertyInfo)fromMemberAccessor.Member;
    MemberExpression toMemberAccessor = (MemberExpression)toAccessor.Body;
    PropertyInfo toProperty = (PropertyInfo)toMemberAccessor.Member;
    return new PropertyComparison() { FromProperty = fromProperty, ToProperty = toProperty };
}

private static Expression<Func<TFrom, bool>> getComparison<TFrom, TTo>(
    IEnumerable<TTo> models, 
    params PropertyComparison[] comparisons)
{
    ParameterExpression pe = Expression.Parameter(typeof(TFrom), "f");
    if (!models.Any() || !comparisons.Any())
    {
        return Expression.Lambda<Func<TFrom, bool>>(Expression.Constant(true), pe);
    }
    var ands = models.Select(m =>
    {
        var equals = comparisons.Select(p =>
        {
            PropertyInfo fromProperty = p.FromProperty;
            PropertyInfo toProperty = p.ToProperty;
            object value = toProperty.GetValue(m);

            Expression fromValue = Expression.Property(pe, fromProperty);
            Expression toValue = Expression.Constant(value);
            Expression equal = Expression.Equal(fromValue, toValue);
            return equal;
        }).ToArray();
        var and = equals.First();
        foreach (var equal in equals.Skip(1))
        {
            and = Expression.AndAlso(and, equal);
        }
        return and;
    }).ToArray();
    Expression ors = ands.First();
    foreach (Expression and in ands.Skip(1))
    {
        ors = Expression.OrElse(ors, and);
    }
    return Expression.Lambda<Func<TFrom, bool>>(ors, pe);
}

答案 1 :(得分:0)

由于Foo是属于模型的类型,因此您可以将匹配项目投影到IEnumerable<Foo>,仅映射感兴趣的两个属性,然后发出查询。这应该有效。

public IEnumerable<FooDupeCheckModel> MatchExists(IEnumerable<FooDupeCheckModel> matches)
{
    //Convert small set to check for dups to objects recognized by the EF
    var fooMatches = matches.Select(m => new Foo() { BarId = m.BardId, BatId = m.BatId });

    //This should now work
    return _db.Foos
              .Where(f => fooMatches.Any(m => m.BarId == f.BarId &&
                                           m.BatId == f.BatId))
              .Select(f => new FooDupeCheckModel
              {
                  BarId = f.BarId,
                  BatId = f.BatId
              });
}