การใช้ขอบเขตเพื่อส่งคืนผลลัพธ์ภายในช่วง DateTime หลายช่วงใน ActiveRecord

ฉันมีโมเดล 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, 13/07 และ 17/07 ระหว่าง 18.30 น. และ 19.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 ดังนั้นจึงรวมวันที่ตายตัวไว้แล้ว ดังนั้นหากฉันต้องการส่งคืนเซสชันทั้งหมดที่เริ่มต้นระหว่าง 18.30 น. ถึง 19.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 << '(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 จะต้องอยู่ลำดับสุดท้าย -- การเก็บไว้ในแต่ละวงจะไม่ส่งคืนสิ่งใดเลย ขอขอบคุณทั้งสองท่านสำหรับความช่วยเหลือทั้งหมดของคุณ! ตอนนี้ฉันรู้สึกฉลาดขึ้นแล้ว


person Nate Goldman    schedule 01.08.2012    source แหล่งที่มา


คำตอบ (2)


สิ่งที่ต้องการ:

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
}
person kenichi    schedule 01.08.2012
comment
เอ้ย มันสวยงามมาก - person FloatingRock; 24.07.2014

ดังนั้น ส่วนที่ง่ายของคำถามคือต้องทำอย่างไรกับวันที่และเวลา สิ่งที่ดีเกี่ยวกับ 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 ที่กระตือรือร้นมากเกินไป

เมื่อคุณทำ a โดยที่คุณไม่ควรใช้ #{} ในแบบสอบถาม แม้ว่าคุณจะรู้ว่าข้อมูลนั้นมาจากที่ใด เพื่อนร่วมงานของคุณก็อาจจะไม่เป็นเช่นนั้น ดังนั้นตรวจสอบให้แน่ใจว่าคุณใช้ ? เหมือนอย่างที่ฉันทำข้างต้น

ประการที่สอง strptime มีราคาแพงมากในทุกภาษา คุณไม่ควรรู้สิ่งนี้ แต่มันก็เป็นเช่นนั้น หากเป็นไปได้ หลีกเลี่ยงการแยกวิเคราะห์วันที่ ในกรณีนี้ คุณก็แค่ gsub / into - ในวันนั้น แล้วทุกอย่างจะมีความสุข MySQL คาดหวังวันที่ในรูปแบบ m/d/y อยู่แล้ว หากคุณยังคงประสบปัญหาอยู่และคุณต้องการวัตถุ DateTime จริงๆ คุณสามารถทำได้ง่ายๆ เช่นกัน: Date.new(2001,2,3) โดยไม่ต้องกิน cpu ของคุณ

person Chuck Vose    schedule 01.08.2012
comment
ขอบคุณ! คำใบ้ CAST(datetime_col AS TIME) ปรากฏว่ามีประโยชน์มากจริงๆ strftimeแพงมากด้วยเหรอ? - person Nate Goldman; 01.08.2012
comment
ไม่ strftime ไม่แพงเกือบเท่าที่ควร ไม่ฟรี แต่ไม่จำเป็นต้องสร้างวัตถุที่มีกฎที่ซับซ้อนเนื่องจากวัตถุนั้นได้ถูกสร้างขึ้นแล้ว - person Chuck Vose; 03.08.2012