【问题标题】:How to perform a date range filter on string with LINQ Query如何使用 LINQ Query 对字符串执行日期范围过滤
【发布时间】:2021-03-14 01:33:17
【问题描述】:

我正在尝试使用以下 linq 进行日期范围过滤:

IQueryable<Movies> movies= _context.Movies
            .OrderByDescending(i => i.Id).Select(i => i);

DateTime startDate = Convert.ToDateTime(searchStartDate);
DateTime endDate = Convert.ToDateTime(searchEndDate);

movies = movies.Where(i => Convert.ToDateTime(i.TransDate) >= startDate &&
                Convert.ToDateTime(i.TransDate) <= endDate)
                .OrderByDescending(j => j.Id);

但它不起作用并且给了我 InvalidOperationException: The LINQ expression '...' could not be translate.

注意:我使用的数据库具有字符串格式的 TransDate 列('YYYY-MM-DD'),因此提出了这个问题。

【问题讨论】:

  • 为什么要将日期作为字符串存储在数据库中?
  • 如果您的日期不是数据库中的字符串,则只需在查询中删除Convert.ToDateTime。但是,如果是,请更改您的数据库
  • 请向我们展示数据库表的CREATE TABLE 脚本。
  • 我希望它也可以作为日期时间存储在数据库中,但不幸的是,我不是创建数据库的人。
  • 它在数据库中的存储格式是什么?

标签: c# linq asp.net-core entity-framework-core razor-pages


【解决方案1】:

虽然将日期作为字符串存储在数据库中并不是一个好主意,但至少所选格式是可排序的。虽然 EF Core 不提供将字符串转换为日期的可翻译方法,但它允许您拥有 DateTime 类型的实体属性(应该是这样),并使用 value converter 将其映射到数据库中的字符串列。因此,您将针对DateTime 编写查询,EF Core 会将常量/参数值转换为字符串并将它们传递给 SQL 查询。

将其应用于您的案例:

型号:

public class Movie
{
    // other properties...

    public DateTime TransDate { get; set; }
}

配置:

const string DateFormat = "yyyy-MM-dd";

modelBuilder.Entity<Movie>().Property(e => e.TransDate)
    .HasConversion(
        dateValue => dateValue.ToString(DateFormat, CultureInfo.InvariantCulture),
        stringValue => DateTime.ParseExact(stringValue, DateFormat, CultureInfo.InvariantCulture)
    )
    .IsRequired()
    .IsUnicode(false) // arbitrary
    .HasMaxLength(10); // arbitrary

LINQ 查询用法:

IQueryable<Movie> movies = ...;
DateTime startDate = ...;
DateTime endDate = ...;

movies = movies
    .Where(e => e.TransDate >= startDate && e.TransDate <= endDate);

【讨论】:

    【解决方案2】:

    尝试删除一个OrderByDescending,您已经在第一个语句中订购了它。您对同一个查询进行了两次订购。

    【讨论】:

      【解决方案3】:

      要了解这个问题,您必须了解 IEnumerable 和 IQueryable 之间的区别。

      IEnumerable

      实现IEnumerable&lt;...&gt; 的类的对象表示类似对象的序列。它包含获取序列的第一个元素的所有内容,只要你有一个元素,你就可以尝试获取下一个元素。

      在最低级别,这是使用GetEnumerator 并反复调用MoveNext() / Current 完成的,如下所示:

      IEnumerable<Movie> movies = ...
      IEnumerator<Movie> enumerator = movies.GetEnumerator();
      
      // try to get the next element:
      while (enumerator.MoveNext())
      {
          // There is a next element
          Movie movie = enumerator.Current;
          ProcessMovie(movie);
      }
      

      内心深处foreach 会做这样的事情。每个不返回 IEnumerable&lt;...&gt; 的 LINQ 方法也会在内部调用 GetEnumerator / MoveNext / Current

      IEnumerable 旨在由您的进程执行,因此 IEnumerable 可以访问您的所有过程。

      可查询

      虽然 IQueryable 看起来像 IEnumerable,但它代表了获得可枚举序列的潜力

      为此,IQueryable 有一个 Expression 和一个 Provider。表达式包含必须以某种通用格式获取的数据; Provider 知道他必须从哪里获取数据(通常是数据库管理系统),以及获取数据时使用什么语言(通常是 SQL)。

      只要您连接返回 IQueryable&lt;...&gt; 的 LINQ 方法,表达式就会发生变化。查询未执行,数据库未联系。我们说 LINQ 方法使用延迟执行或延迟执行。连接这样的 LINQ 语句并不昂贵。

      不返回 IQueryable&lt;...&gt; 的 LINQ 方法,如 ToList()、ToDictionary()、FirstOrDefault()、Count()、Any(),是昂贵的,就像 foreach 一样,它们会在内部调用 GetEnumerator ()。

      当您调用GetEnumerator() 时,Expression 会发送给 Provider,Provider 会将 Expression 转换为 SQL 并执行查询。获取的数据以IEnumerator&lt;...&gt; 的形式返回,您可以调用其中的MoveNext() / Current

      有些提供商比其他提供商更聪明。例如,其中一些在您获取 Enumerator 时不会获取数据,而是在第一个 MoveNext 时获取数据。其他人不会一次获取所有请求的数据,而是“每页”获取数据,因此如果您决定仅枚举两个或三个项目,则仅获取电影的第一页,而不是全部 10000 个。

      但这与我的问题有什么关系?

      您使用了您的 Provider 不知道的方法:Convert.ToDateTime。因此它无法将其转换为 SQL。事实上,有几种 LINQ 方法是实体框架不支持的。见Supported and Unsupported LINQ methods

      您的编译器不知道 Provider 有多聪明,因此您的编译器不会抱怨。您将在运行时收到错误。

      所以你不能使用Convert.ToDateTime,也不能使用Datetime.Parse之类的方法。

      怎么办?

      您写道要转换的字符串格式为“YYYY-MM-DD”。 您可以做的是使用字符串处理例程将它们分成“YYYY”、“MM”和“DD”。是否可以使用String.SubString 取决于您的提供商。

      然后使用类DbFunctionsCreateDateTime

      .Where(x => ... && DbFunctions.CreateDateTime(
            x.TransDate.SubString(0,4),               // YYYY
            x.TransDate.SubString(5, 2),              // MM
            x.TransDate.SubString(8, 2),              // DD
            0, 0, 0) < endDate;
      

      如果您的 Provider 也不接受 SubString,请搜索其他方法来提取 YYYY、MM 和 DD。我知道Sqlite has a Date 将字符串转换为日期时间的方法。

      如果您找不到任何合适的字符串操作例程,请考虑将 endDate 转换为格式“YYYY-MM-DD”并返回比较 TransDate 字符串与 endDateSTring

      string endDateString = String.Format("{0:D04}-{1:D02}-{2:D02}", endDate.Year, endDate.Month, ...)
      

      稍微摆弄一下,直到你得到正确的格式。然后使用:

      .Where(x => ... && x.TransDate < endDateString)
      

      如果这也不起作用,请自行创建 SQL 查询。

      【讨论】:

        猜你喜欢
        • 2020-11-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-12-30
        • 1970-01-01
        • 2017-09-23
        • 2016-07-07
        • 1970-01-01
        相关资源
        最近更新 更多