使用范围在 ActiveRecord 中的多个 DateTime 范围内返回结果

Posted

技术标签:

【中文标题】使用范围在 ActiveRecord 中的多个 DateTime 范围内返回结果【英文标题】:Using scope to return results within multiple DateTime ranges in ActiveRecord 【发布时间】:2012-07-29 20:42:44 【问题描述】:

我有一个Session 模型,它有一个:created_at 日期和一个:start_time 日期,两者都作为:time 存储在数据库中。我目前正在一张巨大的桌子上吐出一堆结果,并允许用户使用范围按单个日期和可选的时间范围过滤结果,如下所示:

class Session < ActiveRecord::Base
  ...

  scope :filter_by_date, lambda  |date|
    date = date.split(",")[0]
    where(:created_at =>
      DateTime.strptime(date, '%m/%d/%Y')..DateTime.strptime(date, '%m/%d/%Y').end_of_day
    )
  
  scope :filter_by_time, lambda  |date, time|
    to = time[:to]
    from = time[:from]
    where(:start_time =>
      DateTime.strptime("#date #from[:digits] #from[:meridian]", '%m/%d/%Y %r')..
      DateTime.strptime("#date #to[:digits] #to[:meridian]", '%m/%d/%Y %r')
    )
  

end

控制器看起来或多或少像这样:

class SessionController < ApplicationController

  def index
    if params.include?(:date) ||
       params.include?(:time) &&
     ( params[:time][:from][:digits].present? && params[:time][:to][:digits].present? )

      i = Session.scoped
      i = i.filter_by_date(params[:date]) unless params[:date].blank?
      i = i.filter_by_time(params[:date], params[:time]) unless params[:time].blank? || params[:time][:from][:digits].blank? || params[:time][:to][:digits].blank?

      @items = i
      @items.sort_by! &params[:sort].to_sym if params[:sort].present?
    else
      @items = Session.find(:all, :order => :created_at)
    end
  end

end

我需要允许用户使用多个日期过滤结果。我将参数作为字符串格式的逗号分隔列表接收,例如"07/12/2012,07/13/2012,07/17/2012",并且需要能够在数据库中查询几个不同的日期范围和这些日期范围内的时间范围,并合并这些结果,例如 7 月 12 日、7 月 13 日和 7 日的所有会话17 点 6:30 到 7:30。

我到处寻找并尝试了几种不同的方法,但我不知道如何实际做到这一点。这可以使用范围吗?如果不是,最好的方法是什么?

我最接近的猜测看起来像这样,但它没有返回任何内容,所以我知道这是错误的。

scope :filter_by_date, lambda  |date|
  date = date.split(",")
  date.each do |i|
    where(:created_at =>
      DateTime.strptime(i, '%m/%d/%Y')..DateTime.strptime(i, '%m/%d/%Y').end_of_day
    )
  end

scope :filter_by_time, lambda  |date, time|
  date = date.split(",")
  to = time[:to]
  from = time[:from]
  date.each do |i|
    where(:start_time =>
      DateTime.strptime("#i #from[:digits] #from[:meridian]", '%m/%d/%Y %r')..
      DateTime.strptime("#i #to[:digits] #to[:meridian]", '%m/%d/%Y %r')
    )
  end

另一个复杂之处是开始时间都存储为 DateTime 对象,因此它们已经包含一个固定日期,所以如果我想返回在任何日期在 6:30 pm 和 7:30 pm 之间开始的所有会话,我需要计算别的东西也出来了。第三方负责数据,因此我无法更改数据的结构或存储方式,我只需要弄清楚如何进行所有这些复杂的查询。请帮忙!


编辑:

以下是我结合 Kenichi 和 Chuck Vose 的建议得出的解决方案:

scope :filter_by_date, lambda  |dates|
  clauses = []
  args = []
  dates.split(',').each do |date|
    m, d, y = date.split '/'
    b = "#y-#m-#d 00:00:00"
    e = "#y-#m-#d 23:59:59"
    clauses << '(created_at >= ? AND created_at <= ?)'
    args.push b, e
  end
  where clauses.join(' OR '), *args


scope :filter_by_time, lambda  |times|
  args = []
  [times[:from], times[:to]].each do |time|
    h, m, s = time[:digits].split(':')
    h = (h.to_i + 12).to_s if time[:meridian] == 'pm'
    h = '0' + h if h.length == 1
    s = '00' if s.nil?
    args.push "#h:#m:#s"
  end
  where("CAST(start_time AS TIME) >= ? AND
         CAST(start_time AS TIME) <= ?", *args)

此解决方案允许我从多个不连续的日期返回会话,或者返回一个时间范围内的任何会话,而完全不依赖日期,或者结合两个范围以按这些日期内的不连续日期和时间进行过滤。耶!

我忽略的重要一点是where 语句必须放在最后——将其保留在每个循环中不会返回任何内容。感谢你们俩的帮助!我现在感觉更聪明了。

【问题讨论】:

【参考方案1】:

类似:

scope :filter_by_date, lambda  |dates|
  clauses = []
  args = []
  dates.split(',').each do |date|
    m, d, y = date.split '/'
    b = "#y-#m-#d 00:00:00"
    e = "#y-#m-#d 23:59:59"
    clauses << '(start_time >= ? AND start_time <= ?)'
    args.push b, e
  end
  where clauses.join(' OR '), *args

scope :filter_by_time, lambda  |dates, time|
  clauses = []
  args = []
  dates.split(',').each do |date|
    m, d, y = date.split '/'
    f = time[:from] # convert to '%H:%M:%S'
    t = time[:to]   # again, same
    b = "#y-#m-#d #f"
    e = "#y-#m-#d #t"
    clauses << '(start_time >= ? AND start_time <= ?)'
    args.push b, e
  end
  where clauses.join(' OR '), *args

【讨论】:

天哪,太美了。【参考方案2】:

所以,问题的简单部分是如何处理日期时间。 DateTimes 的好处是可以通过以下方式轻松地将它们转换为时间:

CAST(datetime_col AS TIME)

所以你可以这样做:

i.where("CAST(start_time AS TIME) IN(?)", times.join(", "))

现在,更难的部分是,你为什么没有得到任何结果。首先要尝试的是使用i.to_sql 来确定作用域查询看起来是否合理。我的猜测是,当您将其打印出来时,您会发现所有这些都与 AND 链接在一起。因此,您要查找日期为 7/12、7/13 和 7/21 的对象。

这里的最后一部分是你有几件事是有关的:sql 注入和一些过分急切的 strptimes。

当你做一个你不应该在查询中使用# 的地方时。即使您知道您的同事的输入来自哪里,也可能不知道。因此,请确保您使用?,就像我在上面所做的那样。

其次,strptime 在每种语言中都非常昂贵。你不应该知道这一点,但它是。如果尽可能避免解析日期,在这种情况下,您可能只需 gsub / into - 在那个日期,一切都会很愉快。无论如何,mysql 都需要 m/d/y 形式的日期。如果你仍然遇到问题并且你真的需要一个 DateTime 对象,你可以很容易地做到:Date.new(2001,2,3) 而不会吃掉你的 CPU。

【讨论】:

谢谢! CAST(datetime_col AS TIME) 提示确实很有帮助。 strftime也很贵吗? 不,strftime 并没有那么贵。它不是免费的,但它不需要构建具有复杂规则的对象,因为该对象已经构建。

以上是关于使用范围在 ActiveRecord 中的多个 DateTime 范围内返回结果的主要内容,如果未能解决你的问题,请参考以下文章

SQLAlchemy 等效于 ActiveRecord 中的命名范围

rails 3.0中的多个范围

ActiveRecord Rails 3 范围与类方法

ActiveRecord 2 级深度范围

Rails ActiveRecord - 搜索多个属性

黑客ActiveRecord:添加全局命名范围