Which is the best strategy for writing DRY code as well as SQL optimized?
Passing an optional parameter, subject_id, and passing #subject to the view only if this parameter exists as well as filtering "pages" that contain that subject_id in their record.
This:
def list
#pages = Page.order("pages.position ASC")
if (params.has_key?(:subject_id))
#subject = params[:subject_id]
#pages = #pages.where("subject_id = ?", params[:subject_id])
end
end
Or:
def list
if (params.has_key?(:subject_id))
#subject = params[:subject_id]
#pages = Page.order("pages.position ASC").where("subject_id = ?", params[:subject_id])
else
#pages = Page.order("pages.position ASC")
end
end
I'd probably go with a slight variation of the first one:
def list
#pages = Page.order("pages.position ASC")
if (params.has_key?(:subject_id))
#subject = params[:subject_id]
#pages = #pages.where(:subject_id => params[:subject_id])
end
end
In either case, #pages won't hit the database until you try to iterate over it so both versions will be the same as far as the database is concerned. Furthermore, these two should be identical (or as close to identical as to not matter) as far as Ruby is concerned:
x = M.order(...).where(...)
# and
x = M.order(...)
x = x.where(...)
so the above version of list is DRYer and should perform exactly the same as the more repetitive version.
Related
My idea for an #index method of a controller is to set things = Thing.all and then if there are filter params, check for them one by one and chain them on so that at the end, you're left with a single query to execute. But the following queries get executed as they are called:
def things_controller
def index
things = Thing.all #<-- db call #1
if params[:color]
things = things.where(color: params[:color]) #<-- db call #2
end
render json: things #<-- I would like to make a single db call here instead
end
end
How can I prevent multiple unnecessary db calls? Is there some convention for filter params that I should be using?
You just need to reorganise the code like this:
def index
things = if params[:color]
Thing.where(color: params[:color])
# ...else if
else
Thing.all
end
render json: things
end
Updated
If you want to chain where clauses, do this:
def index
valid_params_keys = %w(color size)
filtered_keys = valid_params_keys.select { |key| params.keys.include?(key) }
# Don't need conditional check anymore :).
#products = filtered_keys.inject(Product.all) do |scope, key|
scope.where(key => params[key])
end
end
Since things is an array, you can do this, which is only an array operation.
def index
things = Thing.all
if params[:color]
things = things.select!{ |thing| thing.color == params[:color]}
end
render json: things
end
def index
#users = User.all.paginate(page: params[:page])
#users = User.named(params[:name]).paginate(page: params[:page]) if params[:name].present?
#users = User.countryname(params[:country]).paginate(page: params[:page]) if params[:country].present?
#users = User.gender(params[:gender_type]).paginate(page: params[:page]) if params[:gender_type].present?
end
The following code works fine if only :name or :country or :gender_type is present. But it does not work if multiple params are present. What is the DRY way of writing this code for multiple params? Obviously, I do not want to create a different line of code for each possible combination of params.
Here are the scopes:
class User
scope :countryname, -> (country) { where("country ILIKE ?", "%#{country}%")}
scope :gender, -> (gender_type) { where gender_type: gender_type}
scope :named, -> (name) { where("name ILIKE ?", "%#{name}%")}
If I have a query string of
example.com/users?name=sam&gender_type=male
it simply returns all users with names like sam and ignores their gender... I would need to code:
#users = User.gender(params[:gender_type]).named(params[:name]).paginate(page: params[:page]) if params[:gender_type] && params[:name].present?
but I do not want to have to write a new line of code for every single combination of parameters.
You could use the ruby try method. For example, you could write something like
#users = User.try(:gender, params[:gender_type]).try(:paginate, page: params[:page])
Look at try in api docs for other ways to use it.
the problem was the code should be
#users = User.all.paginate(page: params[:page])
#users = #users.named(params[:name]).paginate(page: params[:page]) if params[:name].present?
etc
Rails will then allow chain scoping automatically. the previous code creates separate independent non-chained instance variables.
I'm putting filtering functionality into an application (Rails 4.1beta) - I did this by creating scopes on the Item model, passing a scope through the request params and doing a case statement in the index action. It all works but there's a code smell I'm trying to get rid of in the index action of one of the controllers;
def index
case params[:scope]
when "recent"
#items = Item.recent
when "active"
#items = Item.active
when "inactive"
#items = Item.inactive
else
#items = Item.all
end
end
It all feels a little too rigid / verbose. I'd really like to just do something like this;
def index
#items = Item.send(params[:scope])
end
but then I leave the application wide open to people calling methods on the Item class. Whacking conditions in there kinda defeats the point of what I'm trying to achieve.
Is there some rails magic I'm missing that can help me here?
You can use different controllers to do each of these.
inactive_items_controller.rb
def index
#items = Item.inactive
end
recent_items_controller.rb
def index
#items = Item.recent
end
etc.
Or you can just move you logic you have above to the model
Item model
def self.custom_scope(scope)
case scope
when "recent"
Item.recent
when "active"
Item.active
when "inactive"
Item.inactive
else
Item.all
end
end
or
def self.custom_scope(scope)
recent if scope == 'recent'
active if scope == 'active'
inactive if scope == 'inactive'
scoped if scope.blank?
end
And then in your index
#items = Item.custom_scope params[:scope]
something like this:
if ['recent', 'active', 'inactive'].include?(params[:scope])
#items = Item.send(params[:scope])
else
#items = Item.all
end
I used Pull Review for reviewing my app's code and it came back with this:
Consider refactoring, similar code detected.
Occurred at:
SkillsController # index
PagesController # index
So the app/controllers/skills_controller.rb index action code is:
def index
#skill = Skill.new
if params[:search]
#skills = Skill.search(params[:search]).order('created_at DESC')
else
#skills = Skill.all.order('created_at DESC')
end
end
and on app/controllers/pages_controller.rb is:
def index
#users = User.all
if params[:search]
#users = User.search(params[:search]).order('created_at DESC')
else
#users = User.all.order('created_at DESC')
end
end
Am I suppose to somehow refactor these two actions on these two controllers? Also, I am not sure how I refactor this. Do I extract the if params[:search] segment and replace the instance variables with another variable that will be used on both actions?
Thanks for your time.
I don't know where your method search comes from. It seems it comes from a custom module/gem for ActiveRecord.
If so, you can change the method to shorten code in controller
def self.search(args)
return self unless args
original_search_logic args
end
# As well as extract order to a scope
scope :by_time, -> { order('created_at DESC') }
Then in controller:
# Skill
def index
#skills = Skill.search(params[:search]).by_time
end
# User
def index
#users = User.search(params[:search]).by_time
end
These should be dry enough for now.
take a look at the has_scope and inherited_resources. You can extract the params[:search] part with has_scope. And use inherited_resources to extract how to get the collection and do the ordering.
I have three models - User. Game, and Activity. I have a view that is supposed to show games where the user has no Activity records. I'm having trouble figuring out how to write the query that excludes Games that already have activity.
When I do these two versions (in the Controller), it does not work (I receive the error undefined method 'game_id' for #<ActiveRecord::Relation:0x69eab40>)
version 1
def index
if current_user
#activities = current_user.activities.game_id
#games = Game.where("id NOT IN (?)", #activities )
end
end
version 2
def index
if current_user
#activities = current_user.activities
#activity_ids = #activities.game_id
#games = Game.where("id NOT IN (?)", #activity_ids )
end
end
However, when I do this it works:
The controller:
def index
if current_user
#activities = current_user.activities
#games = Game.where("id NOT IN (?)", #activities.collect {|p| p.game_id} )
end
end
I'm worried about doing it this way because I'm not sure how scalable it is. I got the idea to do it from this [question], but in along with the answers people said that this solution was not scalable (which was not an issue for that question asker).1
your error is not in that line. what's causing your error is this
#activities = current_user.activities.game_id
current_user.activities is an ActiveRecord::Relation object so calling game_id on it raises an exception. Your solution that works is fine since you're going to use #activities (you're going to, right?). If you want a more sql approach, try
def index
if current_user
#activities = current_user.activities
#games = Game.where("id NOT IN (?)", #activities.uniq.pluck(:game_id))
end
end