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

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

 class Session  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  :created_at) end end end 

我需要允许用户使用多个日期过滤结果。 我收到了params作为字符串格式的逗号分隔列表,例如"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到晚上7:30之间开始的所有会话,我需要找出其他的东西太。 第三方负责数据,因此我无法更改其结构或存储方式,我只需要弄清楚如何进行所有这些复杂的查询。 请帮忙!


编辑:

以下是我通过结合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 <= ? AND created_at = ? AND CAST(start_time AS TIME) <= ?", *args) } 

此解决方案允许我从多个非连续日期返回会话或在一段时间内返回任何会话而不依赖于日期,或者将两个范围组合以按这些日期内的非连续日期和时间进行过滤。 好极了!

我忽略的一个重点是where语句必须最后 – 保持在每个循环内部不会返回任何内容。 感谢你们两位的帮助! 我现在感觉更聪明。

就像是:

 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 } 

因此,问题的简单部分是如何处理日期时间。 关于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。