Overview
I want to do an OR search with one that is AND search in ActiveRecord
Detail
I want to change the table to be searched by conditional branching depending on the data stored in the record (whether the status value of the item model is accepted this time).
So, I tried conditional branching as shown below, but it ended up being an AND search.
What kind of description should I write if I want to search here by OR?
Code
items_controller.rb
#items.each do |item|
if item.status != 'accepted'
# search for Item model
#items = #items.where(name: search_params[:name])
else
# search for Master model
#items = #items.merge(Master.where(name: search_params[:name]))
end
end
def search_params
params.permit(
:name
)
end
↓
Generated SQL
AND `items`.`name` = 'test'
AND `masters`.`name` = 'test'
I want this SQL
`items`.`name` = 'test'
OR `masters`.`name` = 'test'
Environment
rails 6.0
Support for "or" was added in Rails 5 you can read more about it here. The syntax works like...
#items = Item.joins(:masters) # or some way to join in the other table
#items.where(name: search_params[:name]).or(Master.where(name: search_params[:name]))
Related
I'm trying to build a simple search form in Ruby on Rails, my form is simple enough basically you select fields from a series of options and then all the events matching the fields are shown. The problem comes when I leave any field blank.
Here is the code responsible for filtering the parameters
Event.joins(:eventdates).joins(:categories).where
("eventdates.start_date = ? AND city = ? AND categories.name = ?",
params[:event][:date], params[:event][:city], params[:event][:category]).all
From what I get it's that it looks for events with any empty field, but since all of them have them not empty, it wont match unless all 3 are filled, another problem arises when I try to say, look events inside a range or array of dates, I'm clueless on how to pass multiple days into the search.
I'm pretty new to making search forms in general, so I don't even know if this is the best approach, also I'm trying to keep the searches without the need of a secialized model.
Below is probably what you are looking for. (Note: If all fields all blank, it shows all data in the events table linkable with eventdates and categories.)
events = Event.joins(:eventdates).joins(:categories)
if params[:event]
# includes below where condition to query only if params[:event][:date] has a value
events = events.where("eventdates.start_date = ?", params[:event][:date]) if params[:event][:date].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("city = ?", params[:event][:city]) if params[:event][:city].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("categories.name = ?", params[:event][:category]) if params[:event][:category].present?
end
To search using multiple days:
# params[:event][:dates] is expected to be array of dates.
# Below query gets converted into an 'IN' operation in SQL, something like "where eventdates.start_date IN ['date1', 'date2']"
events = events.where("eventdates.start_date = ?", params[:event][:dates]) if params[:event][:dates].present?
It will be more easy and optimised . If you use concern for filter data.
Make one concern in Model.
filterable.rb
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
if column_type(key) == :date || column_type(key) ==
:datetime
results = results.where("DATE(#{column(key)}) = ?",
Date.strptime(value, "%m/%d/%Y")) if
value.present?
else
results = results.where("#{column(key)} Like ? ", "%#{value}%") if
value.present?
end
end
results
end
def resource_name
self.table_name
end
def column(key)
return key if key.split(".").count > 1
return "#{resource_name}.#{key}"
end
def column_type(key)
self.columns_hash[key].type
end
end
end
Include this concern in model file that you want to filter.
Model.rb
include Filterable
In your controller Add this methods
def search
#resources = Model.filter(class_search_params)
render 'index'
end
def class_search_params
params.slice(:id,:name) #Your field names
end
So, It is global solution. You dont need to use query for filter. just add this concern in your model file.
That's it.
I created a simple web form where users can enter some search criteria to look for venues e.g. a price range. When a user clicks "find" I use active record to query the database. This all works very well if all fields are filled in. Problems occur when one or more fields are left open and therefore have a value of null.
How can I work around this in my controller? Should I first check whether a value is null and create a query based on that? I can imagine I end up with many different queries and a lot of code. There must be a quicker way to achieve this?
Controller:
def search
#venues = Venue.where("price >= ? AND price <= ? AND romance = ? AND firstdate = ?", params[:minPrice], params[:maxPrice], params[:romance], params[:firstdate])
end
You may want to filter out all of the blank parameters that were sent with the request.
Here is a quick and DRY solution for filtering out blank values, triggers only one query of the database, and builds the where clause with Rails' ActiveRecord ORM.
This approach safeguards against SQL-injection, as pointed out by #DanBrooking. Rails 4.0+ provides "strong parameters." You should use the feature.
class VenuesController < ActiveRecord::Base
def search
# Pass a hash to your query
#venues = Venue.where(search_params)
end
private
def search_params
params.
# Optionally, whitelist your search parameters with permit
permit(:min_price, :max_price, :romance, :first_date).
# Delete any passed params that are nil or empty string
delete_if {|key, value| value.blank? }
end
end
I would recommend to make method in Venue
def self.find_by_price(min_price, max_price)
if min_price && max_price
where("price between ? and ?", min_price, max_price)
else
all
end
end
def self.find_by_romance(romance)
if romance
where("romance = ?", romance)
else
all
end
end
def self.find_by_firstdate(firstdate)
if firstdate
where("firstdate = ?", firstdate)
else
all
end
end
And use it in your controller
Venue
.find_by_price(params[:minPrice], params[:maxPrice])
.find_by_romance(params[:romance])
.find_by_firstdate(params[:firstdate])
Another solution to this problem, and I think a more elegant one, is using scopes with conditions.
You could do something like
class Venue < ActiveRecord::Base
scope :romance, ->(genre) { where("romance = ?", genre) if genre.present? }
end
You can then chain those, which would work as an AND if there is no argument present, then it is not part of the chain.
http://guides.rubyonrails.org/active_record_querying.html#scopes
Try below code, it will ignore parameters those are not present
conditions = []
conditions << "price >= '#{params[:minPrice]}'" if params[:minPrice].present?
conditions << "price <= '#{params[:maxPrice]}'" if params[:maxPrice].present?
conditions << "romance = '#{params[:romance]}'" if params[:romance].present?
conditions << "firstdate = '#{params[:firstdate]}'" if params[:firstdate].present?
#venues = Venue.where(conditions.join(" AND "))
I am updating an old ruby\rails application that has an ActiveAdmin component (ActiveAdmin 0.6, Ruby 1.9.3 and Rails 3.2). The user has requested a filter that searches all fields in a given model. I don't think this is practical because you can't search a date or numeric value for "a" so I have compromised on just searching text with the filter.
Having looked at the ActiveAdmin documentation this states that you can create a filter for several attributes using "or" between the attributes. So if I wanted to search the "circumstances" or "accident_type" attributes I would use the filter below:
filter :circumstances_or_accident_type, :as => :string, label: "Search All Text Fields"
If I use this syntax the filter works as expected.
I now want to find all the string\text attributes to create by filter attributes which I did using this code (there are probably neater ways of doing this but it works):
xfilter_text = ""
Notification.columns.each do |xfield|
if xfield.type == :string or xfield.type == :text
if xfilter_text.length == 0
xfilter_text = xfield.name
else
xfilter_text << "_or_"
xfilter_text << xfield.name
end
end
end
I used the result to hard-code the values into the filter which gave me the following (yes there are a few attributes in the model):
filter :circumstances_or_accident_type_or_author_type_or_location_or_immediate_action_or_injury_details_or_outcome_type_or_investigation_findings_or_action_to_prevent_recurrence_or_building_or_classification_or_manager_email_or_manager_name_or_current_stage_or_injured_last_name_or_injured_first_name_or_injured_gender_or_injured_address_or_injured_home_telephone_or_injured_work_status_or_injured_job_title_or_injured_working_pattern_or_injured_email_or_riddor_document_or_body_part_or_kind_of_accident_or_injury_type_or_service_or_team_or_defects_or_witness_details_or_location_details_or_hse_reference_number_or_riddor_category_or_address_or_details_of_treatment_or_processor_actions_or_business_unit_or_other_author_type_or_lost_time_details_or_changed_by_or_details_of_hospital_treatment, :as => :string, label: "Search All Text Fields"
I tested this and it worked. All good so far. I could just leave it here but I wanted to ensure the code is self maintaining so any changes in the model would not require changes to the custom filter. This is the part I am having trouble with. I would like to change the hardcoded attributes to use the results of the code that creates the filter attributes somehow. Something like this:
filter :get_filter, :as => :string, label: "Search All Text Fields"
def get_filter
xfilter_text = ""
Notification.columns.each do |xfield|
if xfield.type == :string or xfield.type == :text
if xfilter_text.length == 0
xfilter_text = xfield.name
else
xfilter_text << "_or_"
xfilter_text << xfield.name
end
end
return xfilter
end
end
I expect that I would need something that checks that attributes are returned otherwise the filter would fail. I can add that once I get the code working.
Appreciate any help or suggestions.
I'd be inclined to take the messy business of generating the query and delegate it to the model, using its own scope/class method. Then you just need to inform MetaSearch/Ransack (depending on your ActiveAdmin version) that it can search that scope, and you can add it as a filter.
For bonus points, you could drop the search method into a concern that you can include into any model.
app/admin/notifications.rb
filter :containing_text, as: :string, label: 'Text Search:'
app/models/notification.rb
# for MetaSearch
# search_methods :containing_text
# for Ransack
def self.ransackable_scopes(_opts)
[:containing_text]
end
# this could be dropped into a concern as-is
def self.containing_text(query)
# select text-type columns
cols = columns.select { |c| [:string, :text].include?(c.type) }
# generate query fragment
fragment = cols.map { |c| "#{ table_name }.#{ c.name } LIKE ?" }
.join(' OR ')
# execute sanitized query
where(fragment, *Array.new(cols.size, "%#{ query }%"))
end
### EDIT by OP ###
I had never used concerns before so eventually worked out how to get it working:
1) Add the concern path to your application.rb
config/application.rb
class Application < Rails::Application
config.autoload_paths += %W(#{config.root}/app/models/concerns)
end
2) Add the include to the Searchable concern and method call into the notifcation model
app/models/notification.rb
include Searchable
search_methods :containing_text
3) Created the concern:
/app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
module ClassMethods
def self.containing_text(query)
# select text-type columns (string and text)
cols = columns.select { |c| [:string, :text].include?(c.type) }
# generate query fragment
fragment = cols.map { |c| "#{ table_name }.#{ c.name } LIKE ?" }
.join(' OR ')
# execute sanitized query
where(fragment, *Array.new(cols.size, "%#{ query }%"))
end
end
end
That then seemed to work. I probably should rename the searchable into something better but it works.
I'm implementing a search system that uses name, tags, and location. There is a has_and_belongs_to_many relationship between Server and Tag. Here's what my search method currently looks like:
def self.search(params)
#servers = Server.all
if params[:name]
#servers = #servers.where "name ILIKE ?", "%#{params[:name]}%"
end
if params[:tags]
#tags = Tag.find params[:tags].split(",")
# How do I eliminate servers that do not have these tags?
end
# TODO: Eliminate those that do not have the location specified in params.
end
The tags parameter is just a comma-separated list of IDs. My question is stated in a comment in the if params[:tags] conditional block. How can I eliminate servers that do not have the tags specified?
Bonus question: any way to speed this up? All fields are optional, and I am using Postgres exclusively.
EDIT
I found a way to do this, but I have reason to believe it will be extremely slow to run. Is there any way that's faster than what I've done? Perhaps a way to make the database do the work?
tags = Tag.find tokens
servers = servers.reject do |server|
missing_a_tag = false
tags.each do |tag|
if server.tags.find_by_id(tag.id).nil?
missing_a_tag = true
end
end
missing_a_tag
end
Retrieve the servers with all the given tags with
if params[:tags]
tags_ids = params[:tags].split(',')
#tags = Tag.find(tags_ids)
#servers = #servers.joins(:tags).where(tags: {id: tags_ids}).group('servers.id').having("count(*) = #{tags_ids.count}")
end
The group(...).having(...) part selects the servers with all requested tags. If you're looking for servers which have at least one of the tags, remove it.
With this solution, the search is done in a single SQL request, so it will be better than your solution.
Prior to Rails 3.1, we could update the self.columns method of ActiveRecord::Base.
But that doesn't seem to work now.
Now it seems if I remove a column from a table, I am forced to restart the Rails server. If I don't I keep getting errors when INSERTs to the table happen. Rails still thinks the old column exists, even though it's not in the database anymore.
Active Record does not support this out of the box, because it queries the database to get the columns of a model (unlike Merb's ORM tool, Datamapper).
Nonetheless, you can patch this feature on Rails with (assuming, for instance, you want to ignore columns starting with "deprecated" string):
module ActiveRecord
module ConnectionAdapters
class SchemaCache
def initialize(conn)
#connection = conn
#tables = {}
#columns = Hash.new do |h, table_name|
columns = conn.columns(table_name, "#{table_name} Columns").reject { |c| c.name.start_with? "deprecated"}
h[table_name] = columns
end
#columns_hash = Hash.new do |h, table_name|
h[table_name] = Hash[columns[table_name].map { |col|
[col.name, col]
}]
end
#primary_keys = Hash.new do |h, table_name|
h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil
end
end
end
end
end
You can clear the ActiveRecord schema cache:
ActiveRecord::Base.connection.schema_cache.clear_table_cache(:table_name)!
Then it'll be reloaded the next time you reference a model that uses that table.