Hopefully a simple question around a rails best-practice.
Let's keep this super simple; say I have a task model that has an ID, description and status.
In my controller I have an index action to return all tasks
def index
#tasks = Task.all
end
My question is, in my view, suppose I want to display the tasks in separate HTML tables according to their status.
What is the best practice?
a) Query the database multiple times in the index action, ie
def index
#draft_tasks = Task.where(status: "Draft")
#approved_tasks = Task.where(status: "Approved")
#closed_tasks = Task.where(status: "Closed")
end
b) Query the database once, and filter in the contoller action
def index
tasks = Task.all
#draft_tasks = tasks.#somethinghere
#approved_tasks = tasks.#somethinghere
#closed_tasks = tasks.#somethinghere
end
c) Filter in the view
<% #tasks.each do |k, v| %>
<% some if statement searching for the status I want %>
# Some code to output the table
<%end%>
<%end%>
or
d) Something else?
The generally accepted best practices here are to keep controller methods thin and to keep logic out of the view. So with that in mind, one possible way to do this would be:
# model
class Task
scope :drafts, where(:status => "Draft")
scope :approved, where(:status => "Approved")
scope :closed, where(:status => "Closed")
end
# controller
def index
#draft_tasks = Task.drafts
#approved_tasks = Task.approved
#closed_tasks = Task.closed
end
This will make 3 queries to the database, which could become a performance concern down the road, but if that does happen, you can optimize it at the model level (e.g. by defining class methods drafts, approved, and closed where the first one called prefetches everything). It's less elegant though, so don't prematurely optimize.
This is a loaded question with no one best practice in my opinion. Given the case you have stated (display a table for each status) I would use the following thought process:
I would generally avoid case A when you're just dealing with one Model type. I try to limit the number of database queries when possible
Case B is what I would probably use if the view needs to display different markup depending on the status of a task.
I would usually tend towards case C if the markup is the same for each status. You can use the group_by function for this:
When the amount of information on your page starts to get larger and more complicated, you can start looking at extracting some logic out of the controller and into another object (common terms for this object would be a presenter or decorator). This can make testing some of your presentation logic easier by separating it from the controller and keeping your controllers 'thin'. But for the case you've given, I'd stick with option b or c.
In the simple case where the number of tasks is limited, I would do only a single query to retrieve them, and then separate them as follows:
tasks = Task.all
#draft_tasks = tasks.select { |x| x.status == 'Draft' }
#approved_tasks = tasks.select { |x| x.status == 'Approved' }
#closed_tasks = tasks.select { |x| x.status == 'Closed' }
Furthermore, depending on the bendability of your requirements, I would even render them in a single table with a clear visual marker what the state is (e.g. background-colour or icons). Then there would not even be a reason to separate the tasks beforehand (but I can imagine this would break your UI completely).
None of the above is valid once the number of tasks becomes larger, and you will need to apply pagination, and you need to display three different tables (one for each state).
In that case you will need to use the three separate queries as answered by #Ben.
Now UI-wise, I am not sure how you can paginate over three different sets of data at once. So instead I would use a single table showing all the states, and offer the option to filter on the status. In that case at least it is clear for the user what pagination will mean.
Just my two cents, hope this helps.
option a) seems better just because database can cache the query for you and stuff, so it should be faster.
Related
I have a PORO (Plain Old Ruby Object) to deal with some business logic. It receives an ActiveRecord object and classify it. For the sake of simplicity, take the following as an example:
class Classificator
STATES = {
1 => "Positive",
2 => "Neutral",
3 => "Negative"
}
def initializer(item)
#item = item
end
def name
STATES.fetch(state_id)
end
private
def state_id
return 1 if #item.value > 0
return 2 if #item.value == 0
return 3 if #item.value < 0
end
end
However, I also want to do queries that groups objects based on these state_id "virtual attribute". I'm currently dealing with that by creating this attribute in the SQL queries and using it in GROUP BY statements. See the example:
class Classificator::Query
SQL_CONDITIONS = {
1 => "items.value > 0",
2 => "items.value = 0",
3 => "items.value < 0"
}
def initialize(relation = Item.all)
#relation = relation
end
def count
#relation.select(group_conditions).group('state_id').count
end
private
def group_conditions
'CASE ' + SQL_CONDITIONS.map do |k, v|
'WHEN ' + v.to_s + " THEN " + k.to_s
end.join(' ') + " END AS state_id"
end
end
This way, I can get this business logic into SQL and make this kind of query in a very efficient way.
The problem is: I have duplicated business logic. It exists in "ruby" code, to classify a single object and also in "SQL" to classify a collection of objects in database-level.
Is it a bad practice? Is there a way to avoid this? I actually was able to do this, doing the following:
item = Item.find(4)
items.select(group_conditions).where(id: item.id).select('state_id')
But by doing this, I loose the ability to classify objects that are not persisted in database. The other way out would be classifying each object in ruby, using an Iterator, but then I would lose database performance.
It's seem to be unavoidable to keep duplicated business logic if I need the best of the two cases. But I just want to be sure about this. :)
Thanks!
I'd rather keep database simple, and put logic in Ruby code as much as possible. Since classification is not stored in database, I won't expect the queries to return it.
My solution is to define a concern which will be included into ActiveRecord model classes.
module Classified
extend ActiveSupport::Concern
STATES = {
1 => "Positive",
2 => "Neutral",
3 => "Negative"
}
included do
def state_name
STATES.fetch(state_id)
end
private
def state_id
(0 <=> value.to_i) + 2
end
end
end
class Item < ActiveRecord::Base
include Classified
end
And I fetch items from database just as usual.
items = Item.where(...)
Since each item knows its own classification value, I don't have to ask database for it.
items.each do |item|
puts item.state_name
end
ActiveRecord itself implies a degree of coupling between your persistence and business logic. However, as much as the pattern allows, and if you don't have real performance constraints, the first option should be to keep your persistence code as dumb as possible, and move this "classification" (which is clearly a business rule) away from the database as much as possible.
The rationale is that database-related code is more expensive to change (especially as your system is already in production) and generally more difficult and slower to test than pure business logic.
Is there any chance to introduce trigger in the database? If so, I would go with “calculated” field state_id in the database, that changes it’s value on both INSERT and UPDATE (this will bring even more productivity benefit) and this code in ruby:
def state_if
return #item.state_id if #item.state_id # persistent object
case #item.value
when 0 then 2
when -Float::INFINITY...0 then 3
else 1
end
end
My app has designs that users can like (vote, using acts_as_voteable). To find a design's like count in the view, you use
#design.votes.count
I'm making a popular page to showcase the most popular designs based on the number of votes they have. I only want designs that has at least 5 votes to them. Right now, I had that in the view but I want to push that into the controller. My controller, thus far, looks like this which shows all the designs and sorts them in order of most votes.
def popular
#designs = Design.all
#designs.sort! {|t1, t2| t2.votes.count <=> t1.votes.count}
end
Now i just want to make sure the designs have a minimum vote count of 5.
Previously, I was doing this the wrong way and putting it in my view by putting this inside my Design loop
<% if design.vote.count > 5 %>
...
<% end %>
Thanks!
First of all, the behavior you want is better defined in the Design model and not in the controller since it deals with data. So in your Design model, add the following code:
scope :most_popular, -> do
results = select {|design| design.votes.count > 4 }
results.sort! {|t1, t2| t2.votes.count <=> t1.votes.count}
end
Adding the two scope methods above in your Design model, you could do this in your controller code:
def popular
#designs = Design.most_popular
end
Your controller code ends up being a lot cleaner and you have scope methods that you can reuse anywhere else you need them. :)
Hope that helps!
You can use a having() clause. See: http://guides.rubyonrails.org/active_record_querying.html#having
For example: Design.joins(:votes).group('votes.design_id').having('votes.count > 5').order('votes.count')
Edit
You can also just use a where clause. For example, for the first design:
Design.first.votes.where('count > 5')
Example for multiple designs:
Design.all.map{ |a| a.votes.where('count > 5').count }.sort! # returns a sorted array with all vote counts
I have a model called foo with a date field.
On my index view, I am showing a typical "weekly view" for a specified week. To put the data in my view, I loop through each day of the specified week and query the data one day at time. I do this so that I can make sure to put a NIL on the correct day.
foos_controller.rb
for day in 0..6
foo = Foo.this_date(#date+day.days).first
#foos[day] = foo
end
index.html.haml
- for day in 0..6
%li
- if #foos[day].nil?
Create a new foo?
- else
Display a foo information here
Obviously, there's a lot of things wrong here.
I should find someone smart member to tell me how to write a good query so that I only have to do it once.
I should not have any if/else in my view
My goal here is to either show the content if the it is there for a particular day or show a "create new" link if not.
thanks for the help in advance!!
First, I have no idea what this_date actually does, but I'll assume it's retrieving a record with a specific date from your datastore. Instead of doing 7 queries, you can condense this into one using a date range:
Foo.where(date: (#date..(#date + 6.days)))
You can tack on a .group_by(&:date) to return something similar to the hash you are manually constructing, but using the actual dates as keys instead of the date offset.
To iterate over the dates in the view, I would recommend using Hash#fetch, which allows you to define a default return when a key is not present, e.g:
hash = { :a => 1, :b => 2 }
hash.fetch(:a){ Object.new } #=> 1
hash.fetch(:c){ Object.new } # #<Object:...>
The question now is what object to substitute for nil. If you want to avoid using conditionals here, I'd recommend going with the NullObject pattern (you could involve presenters as well but that might be a bit overkill for your situation). The idea here is that you would create a new class to substitute for a missing foo, and then simply define a method called to_partial_path on it that will tell Rails how to render it:
class NullFoo
def to_partial_path
"null_foos/null_foo"
end
end
You'll need to create partials at both app/views/foos/_foo.html.erb and app/views/null_foos/_null_foo.html.erb that define what to render in each case. Then, in your view, you can simply iterate thusly:
<% (#date..(#date + 6.days)).each do |date| %>
<%= render #foos.fetch(date){ NullDate.new } %>
<% end %>
Is this appropriate for your situation? Maybe it's also a bit overkill, but in general, I think it's a good idea to get in the habit of avoid nil checks whenever possible. Another benefit of the NullObject is that you can hang all sorts of behavior on it that handle these situations all throughout your app.
I am pretty new to Rails and I have a feeling I'm approaching this from the wrong angle but here it goes... I have a list page that displays vehicles and i am trying to add filter functionality where the user can filter the results by vehicle_size, manufacturer and/or payment_options.
Using three select form fields the user can set the values of :vehicle_size, :manufacturer and/or :payment_options parameters and submit these values to the controller where i'm using a
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, :vehicle_size => params[:vehicle_size] )
kind of query. this works fine for individual params (the above returns results for the correct vehicle size) but I want to be able to pass in all 3 params without getting no results if one of the parameters is left blank..
Is there a way of doing this without going through the process of writing if statements that define different where statements depending on what params are set? This could become very tedious if I add more filter options.. perhaps some sort of inline if has_key solution to the effect of:
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, if(params.has_key?(:vehicle_size):vehicle_size => params[:vehicle_size], end if(params.has_key?(:manufacturer):manufacturer => params[:manufacturer] end )
You can do:
#vehicles = Vehicle.order('vehicles.id ASC')
if params[:vehicle_size].present?
#vehicles = #vehicles.where(vehicle_size: params[:vehicle_size])
end
Or, you can create scope in your model:
scope :vehicle_size, ->(vehicle_size) { where(vehicle_size: vehicle_size) if vehicle_size.present? }
Or, according to this answer, you can create class method:
def self.vehicle_size(vehicle_size)
if vehicle_size.present?
where(vehicle_size: vehicle_size)
else
scoped # `all` if you use Rails 4
end
end
You call both scope and class method in your controller with, for example:
#vehicles = Vehicle.order('vehicles.id ASC').vehicle_size(params[:vehicle_size])
You can do same thing with remaining parameters respectively.
The has_scope gem applies scope methods to your search queries, and by default it ignores when parameters are empty, it might be worth checking
I've been implementing some nice interactive interfaces that can sort lists in my m rails app for models that use acts_as_list. I have a sort function that gets called and sets the position for each record afterr each drag and drop using the sortable_element script.aculo.us function.
This is an example of the controller action that handles the sort after the drag and drop completes:
def sort
params[:documents].each_with_index do |id, index|
Document.update_all(['position=?', index+1], ['id=?', id])
end
end
Now I am trying to do this same thing with a model that is a nested set (acts_as_nested_set). An example of the type of interface interaction: http://script.aculo.us/playground/test/functional/sortable_tree_test.html
I am stuck on how to write the controller action to handle the sort when the drag and drop completes.
I've added the :tree=>true parameter to the sortable _element function so far which appears to send a list of hashes but it seems that I am still missing information about the entire nested order....
I was certain this has been done before and didn't want to try to reinvent the wheel, but I can't seem to find any examples of the controller action <-> view with js function setup to handle a sortable acts_as_nested_set
Any help with creating an interactive sortable nested set in rubyonrails would be appreciated!
Thanks,
John
a good solution with ONE sql-query from http://henrik.nyh.se/2008/11/rails-jquery-sortables
# in your model:
def self.order(ids)
update_all(
['ordinal = FIND_IN_SET(id, ?)', ids.join(',')],
{ :id => ids }
)
end
see example app here - http://github.com/matenia/jQuery-Awesome-Nested-Set-Drag-and-Drop
It's a hacky way of doing it, but its basically, sort first, then save order.
Uses nestedsortables, serializelist, and 2 actions to traverse the tree
PS: I know this question is over a year old but hoping that the link above helps someone else coming here.
edit: added Rails3 example with some slightly cleaner code.
Just found this:
sortable_element_for_nested_set on github
Looks like it'll do the job, however I'm having some bugs while trying to implement it. It basically makes the javascript return the id of the element that was moved, then goes through the elements and returns its new parent, left and right values. Can't believe it's taken this long for something like this to be written! Lucky it was just when I needed it :)
Here's a snippet of code from my project that does the trick:
http://gist.github.com/659532
def reorder_children(ordered_ids)
ordered_ids = ordered_ids.map(&:to_i)
current_ids = children.map(&:id)
unless current_ids - ordered_ids == [] && ordered_ids - current_ids == []
raise ArgumentError, "Not ordering the same ids that I have as children. My children: #{current_ids.join(", ")}. Your list: #{ordered_ids.join(", ")}. Difference: #{(current_ids - ordered_ids).join(', ')} / #{(ordered_ids - current_ids).join(', ')}"
end
j = 0
transaction do
for new_id in ordered_ids
old_id = current_ids[j]
if new_id == old_id
j += 1
else
Category.find(new_id).move_to_left_of(old_id)
current_ids.delete(new_id)
end
end
end
end
You call it on the parent, and it'll sort the children.
You just pass in the value that you get from Sortable, like so:
def reorder
#category.reorder_children(params[:categories])
render :nothing => true
end
Hope this helps.
//Lars
the_sortable_tree
Sortable Nested Set for Rails 3.1+
Dreaming about Drag and Drop for Nested Sets? It’s should be with JQuery?
Here’s the solution!