Menggunakan cakupan untuk mengembalikan hasil dalam beberapa rentang DateTime di ActiveRecord

Saya memiliki model Session yang memiliki tanggal :created_at dan tanggal :start_time, keduanya disimpan dalam database sebagai :time. Saat ini saya mengeluarkan banyak hasil pada tabel besar dan memungkinkan pengguna memfilter hasil berdasarkan satu tanggal dan rentang waktu opsional menggunakan cakupan, seperti:

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

Pengontrolnya kurang lebih terlihat seperti ini:

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

Saya perlu mengizinkan pengguna memfilter hasil menggunakan beberapa tanggal. Saya menerima params sebagai daftar yang dipisahkan koma dalam format string, mis. "07/12/2012,07/13/2012,07/17/2012", dan harus dapat menanyakan database untuk beberapa rentang tanggal yang berbeda, dan rentang waktu dalam rentang tanggal tersebut, dan menggabungkan hasilnya, jadi misalnya semua sesi pada 12/7, 13/7, dan 17/7 antara 18:30 dan 19:30.

Saya telah mencari ke mana-mana dan mencoba beberapa hal berbeda tetapi saya tidak tahu bagaimana cara melakukannya. Apakah ini mungkin menggunakan cakupan? Jika tidak, apa cara terbaik untuk melakukan ini?

Tebakan terdekat saya terlihat seperti ini tetapi tidak menghasilkan apa pun jadi saya tahu itu salah.

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
}

Kerumitan lainnya adalah semua waktu mulai disimpan sebagai objek DateTime sehingga sudah menyertakan tanggal tetap, jadi jika saya ingin mengembalikan semua sesi yang dimulai antara pukul 18:30 dan 19:30 pada tanggal mana pun, saya perlu memikirkan hal lain. juga. Pihak ketiga bertanggung jawab atas data tersebut, jadi saya tidak dapat mengubah struktur atau penyimpanannya, saya hanya perlu memikirkan cara melakukan semua pertanyaan rumit ini. Tolong bantu!


EDIT:

Inilah solusi yang saya temukan dengan menggabungkan saran Kenichi dan Chuck Vose di bawah ini:

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)
}

Solusi ini memungkinkan saya untuk mengembalikan sesi dari beberapa tanggal yang tidak berturut-turut ATAU mengembalikan sesi apa pun dalam rentang waktu tanpa bergantung pada tanggal sama sekali, ATAU menggabungkan dua cakupan untuk memfilter berdasarkan tanggal dan waktu yang tidak berturut-turut dalam tanggal tersebut. Hore!

Poin penting yang saya abaikan adalah bahwa pernyataan where harus berada di urutan terakhir -- menyimpannya di dalam setiap loop tidak akan menghasilkan apa-apa. Terima kasih kepada Anda berdua atas semua bantuan Anda! Saya merasa lebih pintar sekarang.


person Nate Goldman    schedule 01.08.2012    source sumber


Jawaban (2)


sesuatu seperti:

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
}

Dan

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
Astaga, itu indah. - person FloatingRock; 24.07.2014

Jadi, bagian mudah dari pertanyaannya adalah apa yang harus dilakukan terhadap waktu tanggal. Hal yang menyenangkan tentang DateTimes adalah mereka dapat dipindahkan ke waktu dengan sangat mudah dengan ini:

CAST(datetime_col AS TIME)

Jadi Anda dapat melakukan hal-hal seperti:

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

Sekarang, bagian tersulitnya, mengapa Anda tidak mendapatkan hasil apa pun. Hal pertama yang harus dicoba adalah menggunakan i.to_sql untuk memutuskan apakah cakupan kueri terlihat masuk akal. Dugaan saya adalah ketika Anda mencetaknya, Anda akan menemukan bahwa semua tempat tersebut dirangkai bersama dengan AND. Jadi Anda meminta objek dengan tanggal 12/7, 13/7, dan 21/7.

Bagian terakhir di sini adalah Anda memiliki beberapa hal yang mengkhawatirkan: injeksi sql dan beberapa waktu strp yang berlebihan.

Saat Anda melakukan di mana Anda tidak boleh menggunakan #{} dalam kueri. Meskipun Anda tahu dari mana masukan itu berasal, rekan kerja Anda mungkin tidak tahu. Jadi pastikan Anda menggunakan ? seperti yang saya lakukan di atas.

Kedua, strptime sangat mahal dalam setiap bahasa. Anda seharusnya tidak mengetahui hal ini, tetapi memang demikian adanya. Jika memungkinkan hindari penguraian tanggal, dalam hal ini Anda mungkin bisa langsung gsub / ke - pada tanggal itu dan semuanya akan bahagia. MySQL mengharapkan tanggal dalam bentuk m/d/y. Jika Anda masih mengalami masalah dengannya dan Anda benar-benar membutuhkan objek DateTime, Anda dapat melakukannya dengan mudah: Date.new(2001,2,3) tanpa memakan CPU Anda.

person Chuck Vose    schedule 01.08.2012
comment
Terima kasih! Petunjuk CAST(datetime_col AS TIME) ternyata memang sangat membantu. Apakah strftime juga sangat mahal? - person Nate Goldman; 01.08.2012
comment
tidak, strftime tidak semahal itu. Memang tidak gratis, namun tidak perlu membangun objek dengan aturan yang rumit karena objek tersebut sudah dibuat. - person Chuck Vose; 03.08.2012