Skip to content

Instantly share code, notes, and snippets.

@br3nt
Created April 24, 2014 07:16
Show Gist options
  • Save br3nt/d249e1935d3e7432fa4a to your computer and use it in GitHub Desktop.
Save br3nt/d249e1935d3e7432fa4a to your computer and use it in GitHub Desktop.
Advanced Search utilising scopes (problem with match on any)
class Appointment < ActiveRecord::Base
belongs_to :patient
belongs_to :doctor
scope :between, ->(time_range) {
where(arel_table[:start_time].lt(time_range.first).and(arel_table[:end_time].gt(time_range.first)))
}
end
class Doctor < ActiveRecord::Base
has_one :person
has_many :appointments
has_many :patients, :through => :appointments
end
class Patient < ActiveRecord::Base
has_one :person
has_many :appointments
has_many :doctors, :through => :appointments
scope :with_gender, ->(gender) { joins(:person).merge( Person.with_gender(gender) ) }
scope :with_dob, ->(dob) { joins(:person).merge( Person.with_dob(dob)) }
scope :older_than, ->(date) { joins(:person).merge( Person.older_than(date)) }
scope :younger_than, ->(date) { joins(:person).merge( Person.younger_than(date)) }
scope :with_name_like, ->(name) { joins(:person).merge( Person.with_name_like(name)) }
scope :with_appointment_between, ->(time_range) { joins(:appointments).merge( Appointment.between(time_range) ) }
end
class PatientSearch
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :match_criteria, :gender, :dob, :name, :age_start_time, :age_end_time, :appt_start_time, :appt_end_time
def initialize(params = {})
# default values
@match_type = 'any'
params.each do |attribute, value|
instance_variable_set("@#{attribute}", value) if respond_to?(attribute)
end
end
def results
queries = {}
queries[:gender_query] = PatientSearch.gender_query(gender)
queries[:dob_query] = PatientSearch.dob_query(dob)
queries[:name_query] = PatientSearch.name_query(name)
queries[:age_query] = PatientSearch.age_query(age_start_time, age_end_time)
queries[:appointment_query] = PatientSearch.appointment_query(appt_start_time, appt_end_time)
results = Patients.scoped
queries.each do |query|
next if !query # skip if no query
if match_criteria == 'all'
# each search gets ANDed
results = results.where(query)
elsif match_criteria == 'any'
# each search gets ORed
#
# this is the part I'm not sure of :(
#
end
end
end
#
# Query methods
#
def self.gender_query(gender)
Patient.with_gender(gender) if gender
end
def self.dob_query(dob)
Patient.with_dob(dob) if dob
end
def self.name_query(name)
Patient.with_name_like(name) if name
end
def self.age_query(start_time, end_time)
if start_time && end_time
Patient.where(:dob => start_time..end_time)
elsif start_time
Patient.older_than(start_time)
elsif end_time
Patient.younger_than(end_time)
end
end
def self.appointment_query(start_time, end_time)
Patient.with_appointment_between(start_time..end_time)
end
end
class Person < ActiveRecord::Base
belongs_to :patient
belongs_to :doctor
scope :with_gender, ->(gender) { where(:gender => gender) }
scope :with_dob, ->(dob) {where(:dob => dob) }
scope :older_than, ->(date) { where(arel_table[:dob].lt(date)) }
scope :younger_than, ->(date) { where(arel_table[:dob].gt(date)) }
scope :with_name_like, ->(name) { where("lower(name) LIKE ?", name.downcase) }
end
@danshultz
Copy link

With something this complicated as a search like this I would create a custom query object as you have and leverage a bit of raw arel to manage everything. While you can do some clever things to reach deep down into AR and pull out the AREL nodes of your where clauses, it can become brittle, etc.

I would do the following bits with you your patient_search.rb class

class PatientSearch
  def person
    Person.arel_table
  end

  def results

    predicates = [
      (PatientSearch.gender_query(gender) if gender),
      (PatientSearch.dob_query(dob) if dob),
      (PatientSearch.name_query(name) if name),
      PatientSearch.age_query(age_start_time, age_end_time),
      PatientSearch.appointment_query(appt_start_time, appt_end_time)
    ].flatten

    where_statement = predicates.reduce(predicates.shift) { |clause, predicate|
      conjunction = (match_criteria == all ? 'and' : 'or')

      clause.send(conjunction, predicate) # does your and/or
    }

    # This will handle where_statement == nil fine.
    # your where_statement will be nil if there are no predicates
    Patient.joins(:person).where(where_statement);
  end

  def self.gender_query(gender)
    person[:gender].eq(gender)
  end

  def self.dob_query(dob)
    person[:dob].eq(dob)
  end

  def self.name_query(name)
    person[:name].matches(name)
  end

  def self.age_query(start_time, end_time)
    if start_time && end_time
      person[:dob].in(start_time..end_time)
    elsif start_time
      person[:dob].gt(start_time)
    elsif end_time
      person[:dob].lt(end_time)
    end
  end

  def self.appointment_query(start_time, end_time)
    Patient.arel_table[:appointment].in(start_time..end_time)
  end

end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment