Init query (.where / .order) with empty query - ruby-on-rails

I'm trying to do something like:
if filter_1
#field = #field.where()
else
#field = #field.where()
end
if filter_2
#field = #field.order()
end
etc.
But how do I init #field with an empty query? I tried #field = Field.all but that gives an array so not allowing chaining.

Try scopedon Model class e.g.
#fields = Field.scoped
#fields = #fields.where("your conditions") if filter_1
#fields = #fiels.order("your conditions") if filter_2

The first time you are initializing the #field instance variable, Please try referring to the class Field, i.e.
Filter1: #field = Field.where(...)
Afterwards if you need to keep adding further filters you can refer to your variable field as many times as you want to.
Filter2 onward: #field = #field.where(...)
As Filter1 would return an active Record relation, you can nest more condition clauses onto it. Also do not worry about performance issues as the SQL will only be generated and processed once it is actually needed.(lazy loading)
If you to #field.to_sql at the end of your filters, you'll be able to see that all of your where clauses have conveniently been nested together into one SQL statement.
Also, I'd recommend you to read Active Record Query Interface
EDIT
Create a method get_field. And use that to add filter results.
def get_field(field)
#field.is_a?(ActiveRecord::Relation) ? Field : field
end
get_field(#field).where(....)
get_field(#field).where(....)

Related

Apply where condition if it exists in rails

is there a way in rails activerecord to apply the where condition based on a condition ?
For example:
Say I want to apply the filter if it is present in the input parameters.
Assume #name is a mandatory field and #salary is an optional field.
#name = "test_name"
#salary = 2000
I want to do something like below:
Employee.where(name: #name)
.where(salary: #salary) if #salary.present?
I know to make it in multiple db calls, but I'm looking for an option to make this happen in a single db call.
You can assign the result of the 1st where to a variable and invoke the 2nd where conditionally:
#employees = Employee.where(name: #name)
#employees = #employees.where(salary: #salary) if #salary.present?
You can query only by parameters that are present:
args = { name: #name, salary: #salary.presence }
Employee.where(args.compact)
You can just add all possible arguments into one hash and remove the ones that are nil with Hash#compact:
Employee.where({ name: #name, salary: #salary }.compact)

Chaining active record queries together

I have a search form which contains parameters such as city,building_type,min_price,max_price.
What is the best way to chain all those queries together. If the user doesn't set a parameter, I want it to just return all records.
Something like
city=params[:city] || "all"
building_type=params[:building_type] || "all"
min_price=params[:min_price] || "all"
#results= Property.where(city: city,building_type: building_type,min_price: min_price)
Is it possible to do something like this? Seeing that there is no "all" keyword.
You can chain active record queries by assigning the results of all the queries you want to do in a single variable
properties = Property.all
properties = properties.where(city: params[:city]) if params[:city].present?
properties = properties.where(building_type: params[:building_type]) if params[:building_type].present?
Moreover, if the parameter keys are the same as the columns in your database, you can just place all of them in an array and loop through it like
properties = Property.all
%i[city building_type min_price].each do |column_name|
next if params[column_name].blank?
properties = properties.where(column_name => params[column_name])
end
NOTE
The first line, properties = Property.all assumes that it returns an ActiveRecord::Relation object which is the default in Rails 4 (not sure). If you are using Rails 3, just use Property.

Equivalent of find_each for foo_ids?

Given this model:
class User < ActiveRecord::Base
has_many :things
end
Then we can do this::
#user = User.find(123)
#user.things.find_each{ |t| print t.name }
#user.thing_ids.each{ |id| print id }
There are a large number of #user.things and I want to iterate through only their ids in batches, like with find_each. Is there a handy way to do this?
The goal is to:
not load the entire thing_ids array into memory at once
still only load arrays of thing_ids, and not instantiate a Thing for each id
Rails 5 introduced in_batches method, which yields a relation and uses pluck(primary_key) internally. And we can make use of the where_values_hash method of the relation in order to retrieve already-plucked ids:
#user.things.in_batches { |batch_rel| p batch_rel.where_values_hash['id'] }
Note that in_batches has order and limit restrictions similar to find_each.
This approach is a bit hacky since it depends on the internal implementation of in_batches and will fail if in_batches stops plucking ids in the future. A non-hacky method would be batch_rel.pluck(:id), but this runs the same pluck query twice.
You can try something like below, the each slice will take 4 elements at a time and them you can loop around the 4
#user.thing_ids.each_slice(4) do |batch|
batch.each do |id|
puts id
end
end
It is, unfortunately, not a one-liner or helper that will allow you to do this, so instead:
limit = 1000
offset = 0
loop do
batch = #user.things.limit(limit).offset(offset).pluck(:id)
batch.each { |id| puts id }
break if batch.count < limit
offset += limit
end
UPDATE Final EDIT:
I have updated my answer after reviewing your updated question (not sure why you would downvote after I backed up my answer with source code to prove it...but I don't hold grudges :)
Here is my solution, tested and working, so you can accept this as the answer if it pleases you.
Below, I have extended ActiveRecord::Relation, overriding the find_in_batches method to accept one additional option, :relation. When set to true, it will return the activerecord relation to your block, so you can then use your desired method 'pluck' to get only the ids of the target query.
#put this file in your lib directory:
#active_record_extension.rb
module ARAExtension
extend ActiveSupport::Concern
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size, :relation)
relation = self
start = options[:start]
batch_size = options[:batch_size] || 1000
unless block_given?
return to_enum(:find_in_batches, options) do
total = start ? where(table[primary_key].gteq(start)).size : size
(total - 1).div(batch_size) + 1
end
end
if logger && (arel.orders.present? || arel.taken.present?)
logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
relation = relation.reorder(batch_order).limit(batch_size)
records = start ? relation.where(table[primary_key].gteq(start)) : relation
records = records.to_a unless options[:relation]
while records.any?
records_size = records.size
primary_key_offset = records.last.id
raise "Primary key not included in the custom select clause" unless primary_key_offset
yield records
break if records_size < batch_size
records = relation.where(table[primary_key].gt(primary_key_offset))
records = records.to_a unless options[:relation]
end
end
end
ActiveRecord::Relation.send(:include, ARAExtension)
here is the initializer
#put this file in config/initializers directory:
#extensions.rb
require "active_record_extension"
Originally, this method forced a conversion of the relation to an array of activrecord objects and returned it to you. Now, I optionally allow you to return the query before the conversion to the array happens. Here is an example of how to use it:
#user.things.find_in_batches(:batch_size=>10, :relation=>true).each do |batch_query|
# do any kind of further querying/filtering/mapping that you want
# show that this is actually an activerecord relation, not an array of AR objects
puts batch_query.to_sql
# add more conditions to this query, this is just an example
batch_query = batch_query.where(:color=>"blue")
# pluck just the ids
puts batch_query.pluck(:id)
end
Ultimately, if you don't like any of the answers given on an SO post, you can roll-your-own solution. Consider only downvoting when an answer is either way off topic or not helpful in any way. We are all just trying to help. Downvoting an answer that has source code to prove it will only deter others from trying to help you.
Previous EDIT
In response to your comment (because my comment would not fit):
calling
thing_ids
internally uses
pluck
pluck internally uses
select_all
...which instantiates an activerecord Result
Previous 2nd EDIT:
This line of code within pluck returns an activerecord Result:
....
result = klass.connection.select_all(relation.arel, nil, bound_attributes)
...
I just stepped through the source code for you. Using select_all will save you some memory, but in the end, an activerecord Result was still created and mapped over even when you are using the pluck method.
I would use something like this:
User.things.find_each(batch_size: 1000).map(&:id)
This will give you an array of the ids.

Rails - dynamic method creation

I have two methods that are identical apart from the ActiveRecord class they are referencing:
def category_id_find(category_name)
category = Category.find_by_name(category_name)
if category != nil
return category.id
else
return nil
end
end
def brand_id_find(brand)
brand = Brand.find_by_name(brand)
if brand != nil
return brand.id
else
return nil
end
end
Now, I just know there must be a more Railsy/Ruby way to combine this into some kind of dynamically-created method that takes two arguments, the class and the string to find, so I tried (and failed) with something like this:
def id_find(class, to_find)
thing = (class.capitalize).find_by_name(to_find)
if thing.id != nil
return thing.id
else
return nil
end
end
which means I could call id_find(category, "Sports")
I am having to populate tables during seeding from a single, monster CSV file which contains all the data. So, for example, I am having to grab all the distinct categories from the CSV, punt them in a Category table then then assign each item's category_id based on the id from the just-populated category table, if that makes sense...
class is a reserved keyword in Ruby (it's used for class declarations only), so you can't use it to name your method parameter. Developers often change it to klass, which preserves the original meaning without colliding with this restriction. However, in this case, you'll probably be passing in the name of a class as a string, so I would call it class_name.
Rails' ActiveSupport has a number of built in inflection methods that you can use to turn a string into a constant. Depending on what your CSV data looks like, you might end up with something like this:
def id_find(class_name, to_find)
thing = (class_name.camelize.constantize).find_by_name(to_find)
...
end
If using a string, you can use constantize instead of capitalize and your code should work (in theory):
thing = passed_in_class.constantize.find_by_name(to_find)
But you can also pass the actual class itself to the method, no reason not to:
thing = passed_in_class.find_by_name(to_find)

Rails and Mongoid :: Converting Strings to Arrays

I collect tags in a nice javascript UI widget. It then takes all the tags and passes them to the server as tag1,tag2,tag3,etc in one text_field input. The server receives them:
params[:event][:tags] = params[:event][:tags].split(',') # convert to array
#event = Event.find(params[:id])
Is there a better way to convert the string to the array? It seems like a code smell. I have to put this both in update and in the new actions of the controller.
you could do this in the model:
I have seldom experience on mongoid. The following would work in active record (the only difference is the write_attribute part)
class Event
def tags=(value_from_form)
value_from_form = "" unless value_from_form.respond_to(:split)
write_attribute(:tags, value_from_form.split(','))
end
end
On the other hand, for consistency, you may want to do the following:
class Event
def tags_for_form=(value_from_form)
value_from_form = "" unless value_from_form.respond_to(:split)
self.tags = value_from_form.split(',')
end
def tags_for_form
self.tags
end
# no need to change tags and tags= methods. and tags and tags= methods would return an array and accept an array respectively
end
In the first case (directly overwriting the tags= method), tags= accepts a string but tags returns an array.
In the second case, tags_for_form= and tags_for_form accepts and returns string, while tags= and tags accepts and returns array.
I just create another model attribute that wraps the tags attribute like so:
class Event
def tags_list=(tags_string)
self.tags = tags_string.split(',').map(&:strip)
end
def tags_list
self.tags.join(',')
end
end
In your form, just read/write the tags_list attribute which will always accept, or return a preformated string. (The .map(:strip) part simply removes spaces on the ends in case the tags get entered with spaces: tag1, tag2, tag3.
PeterWong's answer misses the '?' from the respond_to() method;
class Event
def tags=(value_from_form)
value_from_form = "" unless value_from_form.respond_to?(:split)
write_attribute(:tags, value_from_form.split(','))
end
end

Resources