首页 > 解决方案 > 实体框架:检索不在日期范围内的行

问题描述

我有这个查询:

var query = _repository.GetAllIncluding(x => x.ContractRow, x => x.ContractRow.Contract)
                       .Where(x => (int)x.ContractRow.PeriodicityType == (int)CommonConsts.PeriodicityType.Yearly && 
                                   x.ContractRow.Contract.Date.AddYears(-(x.ContractRow.Period.Value / 2)) > x.DueDate
                                   || x.ContractRow.Contract.Date.AddYears(x.ContractRow.Period.Value / 2) < x.DueDate);

在哪里:

这些类型无法更改。

问题出在AddYears()功能上。

如果我使用.AddYears(-(2 / 2))它返回我期望的值,但如果我使用它,它.AddYears(-(x.ContractRow.Period.Value / 2))在哪里显示不同的结果。为什么?ContractRow.Period2

标签: c#entity-frameworklinq

解决方案


首先,鉴于您在表达式中使用 DateTime.AddYears,这表明您的存储库方法没有返回IEnumerable<Entity>IQueryable<Entity>并且正在执行 EF Linq 查询.ToList()。当您的数据库变得很大时,为了在将来为您节省很多痛苦,或者您在更大的项目中尝试类似的模式,您真的想避免这种情况。这种方法的问题在于,在您触及某个条款之前,EF 会检索所有实体及其关联的 ContractRow 和 Contract 记录。Where对于具有任何大量并发请求的任何显着大小的数据表,这绝对会杀死您的系统。

对于存储库模式,我建议在绝对需要之前返回IQueryable<Entity>并避免调用。ToList所以 GetAll 方法看起来像:

public IQueryable<Row> GetAll()
{
   var query = _context.Rows.AsQueryable();
   return query;
}

请注意,我们不需要打扰Include()语句等。Linq 查询可以愉快地引用相关实体作为表达式的一部分,EF 将自动解析这些。使用投影结果Select()也将解析相关实体。您唯一需要的时间Include()是您特别想要加载和使用整个实体结构的位置。通常这只是更新场景。在这种情况下,您可以在调用存储库方法后在查询中添加.Include()语句,而无需将表达式传递给该方法。它还使您可以灵活地执行.Count(),.Any()和分页.OrderBy().Skip(n).Take(m)等(非常简单和灵活)

至于存储库方法,上面是一个没有基本条件的简单示例。存储库为测试提供了良好的分离点,同时也是通用全局规则的良好基点,例如软删除 (IsActive) 限制和身份验证/授权检查。例如,如果您有一个软删除系统并默认为活动记录:

public IQueryable<Row> GetAll(bool includeInactive = false)
{
   var query = includeInactive 
      ? _context.Rows.AsQueryable()
      : _context.Rows.Where(x => x.IsActive);
   return query;
}

大多数实体不需要 includeInactive 选项,他们只返回Where(x => x.IsActive)

这将有助于解决未来的性能问题,但现在提出了一个您可能已经看到的问题,AddYears不能在 EF Linq 表达式中使用。这是因为 EF 试图将您的表达式转换为 SQL,而 SQL 不理解.AddYears. 幸运的是,EF 支持处理这个问题:EntityFunctions

使用IQueryable<T>存储库方法,EntityFunctions.AddYears您将拥有:

var query = _repository.GetAll()
    .Where(x => (int)x.ContractRow.PeriodicityType == (int)CommonConsts.PeriodicityType.Yearly 
        && (EntityFunctions.AddYears(x.ContractRow.Contract.Date, (x.ContractRow.Period.Value/-2)) > x.DueDate
            || EntityFunctions.AddYears(x.ContractRow.Contract.Date, x.ContractRow.Period.Value / 2)) < x.DueDate));

最后,你悲伤的可能原因:操作、混合 AND 和 OR...(你可能在上面的例子中发现了它)

criteria = A AND B OR C
vs.
criteria = A AND (B OR C)

会产生不同的结果。您需要在日期范围检查周围加上括号,因为没有它们,您会得到:

WHERE PeriodicType = Yearly AND Date > 2 years ago
   OR Date < 2 years from future (and PeriodicType can be anything it wants)

A AND B OR C == (A AND B) OR C
you want 
A AND (B OR C)

我可能已经开始使用它作为解决方案,但我真的希望首先克服潜在的性能痛点。;)


推荐阅读