Efficiently working with arrays and hashes? - ruby-on-rails

I have these arrays:
#users = [[1,'Mark'],[2,'Bill'],[3,'John']]
#projects = [[1,'Change the world'],[2,'Build a computer'],[3,'Run in a circle']]
#points_due = [[1,1,"40"],[1,3,"80"],[2,1,"20"]]
I have a few solutions but I am looking for the most performance efficient way of iterating all these items as if the results were given to me via ActiveRecord.
I want to display a <UL> with a Project, and a <LI> with the user name and the points due to that particular user. I came up with some solutions converting the arrays into hashes using inject and map, however, I feel there is a better way to do such a thing.
I would like to do something like:
#projects.each do |project|
<%= project.name %>
<ul>
project.each do |user|
<li><%= user.name %> | <%= user.points_due %></li>
end
</ul>
end

To do things like what you're talking about (iterating over model X and then showing something for each model Y), a nested .each loop (more or less like the one you've sort-of included in your question) is a pretty standard method. If the display starts getting complicated, you might extract it into one or more layers of partials.
Worrying about the performance of this or other methods is likely premature at best. The performance of different ways of manipulating data structures is not going to be a significant factor in the overall performance of your page. Once you have something working, see if it's actually slow, then worry about how to make it faster.
Edit, based on your comments: An Array of Hashes (with symbols for keys) would probably be better than an Array of Arrays for this data. Not because they're more performant, but because it will make your code more understandable (to yourself, even if no one else ever works on it).
Consider:
Using AR objects, you would do the following to print the names of each Project:
#projects.each do |project|
puts project.name
end
If you simulate this behavior using a Hash:
#projects.each do |project|
puts project[:name]
end
Whereas using Arrays:
#projects.each do |project|
puts project[1]
end
Much harder to remember, and you have to make sure your code puts attributes in the proper order, or that might be some other aspect of the project, instead of the name.
Assuming you've agreed with me and are using this structure (#projects = [{:title => 'foo', 'id' => 1}, {:title => 'bar', :id => 2}] etc.), here's a way to perform the iteration you mention, simply printing out (what I believe to be) the desired data - the number of points listed in #points_due for each user that has some in that project:
#projects.each do |project|
puts project[:name]
#points_due.select{|pd| pd[:project_id] == project[:id] }.each do |project_points|
user = #users.select{|u| u[:id] == project_points[:user_id]}.first
puts "#{user[:name]} has #{project_points[:points]} points due on this project."
end
end
I hope this approximates the output you want, but if not, it should be fairly easy to use it as an example - judicious use of .select will probably get it done.
Edit the second: Converting Arrays to Hashes:
#projects_as_arrays = [[1,'Change the world'],[2,'Build a computer'],[3,'Run in a circle']]
#projects_as_hashes = #projects_as_array.map do |p_arr|
{:id => p_arr[0], :name => p_arr[1]}
end
You should be able to do something along these general lines for each of your arrays.

Related

Rails: Global instance variables in Rails

In my app users can submit recipes through a form, which will be published on a website. Before recipes get published they are moderated through a moderator.
Therefore my app shows in the navbar a count of all currently unpublished recipes for the moderator like so:
To achieve this at the moment I do the following:
application.rb
before_action :count_unpublished
def count_unpublished
#unpublished_count = Recipe.where(:published => false).count
end
_navbar.html.erb
<li>
<%= link_to recipes_path do %>
Recipes <span class="badge" style="background-color:#ff7373"><%= #unpublished_count %></span>
<% end %>
</li>
It works, but I am wondering now if this is a good practice as now with every action my app hits the recipe database (which is maybe not very elegant).
Is there a better solution to achieve this?
cache_key = "#{current_user.id}_#{unpublished_count}"
#unpublished_count = Rails.cache.fetch(cache_key, expires_in: 12.hours) do
Recipe.where(:published => false).count
end
For More: http://guides.rubyonrails.org/caching_with_rails.html#low-level-caching
To avoid hitting the database, you can introduce caching. It comes in many forms: faster storage (memcached, redis), in-process caching (global/class variables) and so on. And they all share the same problem: you need to know when to invalidate the cache.
Take a look at this guide to get some ideas: Caching with Rails.
If I were you, I would not care about this until my profiler tells me it's a performance problem. Instead, I'd direct my efforts to developing the rest of functionality.
Your falling into the trap of premature optimisation. Before doing any optimisation (which increases code complexity most of the time) you have to profile your code to find the bottleneck. Improving a SQL requests which counts for a small part of the total response time is useless. In the contrary if the SQL takes a big amount of time, that is a great improvement.
For that I can recommend these 2 gems:
https://github.com/miniProfiler/rack-mini-profiler
https://github.com/BaseSecrete/rorvswild (disclaimer: I'm the author of this one)
To reply to your question, the better way would be:
# app/models/recipe.rb
class Recipe < AR::base
# A nice scope that you can reuse anywhere
scope :unpublished, -> { where(published: false) }
end
Then in your navbar.html.erb:
<li>
<%= link_to recipes_path do %>
Recipes <span class="badge" style="background-color:#ff7373"><%= Recipe.unpublished.count %></span>
<% end %>
</li>
You have no more these ugly callback and instance variable in the controller.
Unless you have a lot of recipes (something like 100K or more) performance won't be an issue. In that case you can add an index:
CREATE INDEX index_recipes_unpblished ON recipes(published) WHERE published='f'
Note that the index applies only when published is false. Otherwise it would be counter productive.
I think that caching in your case is not well because the invalidation is extremely complex and leads to awful and easy breakable code. Don't worry to hit the database, we will never write faster code than PostgreSQL/MySQL, etc.

RoR - Missing Keys in Hashes inside of URL Parameter Array

I am creating a series of link_to’s, and I am passing some nested information as an array into each URL. My desired outcome looks like so:
?features%5B%5D%5BThick%5D=98&features%5B%5D%5BThin%5D=99
//For some legibility
?features[][Thick]=98&features[][Thin]=99
However, the keys to the hashes inside of the array are not showing up, and I am instead seeing:
?features%5B%5D%5B%5D=98&features%5B%5D%5B%5D=99
//For some legibility
?features[][]=98&features[][]=99
The erb that is creating this series of URLs is here:
<% #products.each |product| do %>
<%= link_to "", new_line_item_path(product_id: product, features: [product.features.each{|feature| {feature.name.to_sym => feature.feature_color_default}}])%>
<% end %>
Is this just a syntactic mistake or is it because I am taking the wrong approach?
**Perhaps this is too much information for this issue but Products have many Features which in turn have many Colors through Feature_Colors.
I'm not sure why the above wasn't working but I made the following changes and am all set. Hopefully the keywords here help someone else if they are making the same mistakes.
First, I pulled the hash creation out into the Product model, like so:
def default_features
list = Hash.new
features.each do |feature|
list[feature.name] = feature.feature_color_default_id
end
return list
end
Then I changed the link_to:
<%= link_to "", new_line_item_path(product_id: product, features: [product.default_features])%>
Working as desired now.

Is there a a clean way to avoid a double query when testing for an empty ActiveRecord result?

In an .html.erb file, it is quite natural to write something like this:
<% unless #results.empty? %>
<ul>
<% #results.each do |result| %>
<li>
<%= link_to result.name, '#' %>
</li>
<% end %>
</ul>
<% end %>
Where #results is the result of an ActiveRecord .all query. Unfortunately this generates two queries to the database: the first looking for a count of the results (the unless condition), the second to retrieve actual results. In this case the query is particularly expensive.
I could simply convert results to an array (which would buffer the entire result set), or put complex logic in the .erb
Neither solution seems to fit the Rails/ActiveRecord design philosophy.
Is there a better way to eliminate the duplicate query?
Rails tries to be smart and not load a whole association/relation when it doesn't need it. As a result some methods on relations or associations look like their counterpart from Enumerable but will instead run some sql if the association is not loaded. first, any?, include? are examples of this.
The easiest way, when you know that this is a case when this optimisation is not paying off is to force the relation to be loaded. You could do this by converting to an array to_a but you might as well be more direct.
#results = Foo.where(...).load
This is also keeps #results as a relation rather than converting to an array.

How efficient is the current_page? helper method?

I'm curious about the efficiency of the Rails current_page? helper method. I'm using it in a view, roughly as shown below:
<% if current_page?(:action => "foo") %>
<dt>Label 1:</dt>
<% else %>
<dt>Label 2:</dt>
<% end %>
and
<% if current_page?(:action => "foo") || current_page(:action => "bar") %>
<dt>Label 1:</dt>
<% else %>
<dt>Label 2:</dt>
<% end %>
But would it be more efficient to make this switch some other way? For instance, would it be more efficient to set an instance variable in my controller actions for foo and bar, then check <% if #foo || #bar %>?
And is there any difference in the efficiency between Rails 2 and Rails 3?
I can't speak to the difference between Rails 2 and Rails 3, but looking at the source code in ActionPack, it doesn't look like the method is doing anything particularly complicated. url_for could be slightly inefficient (I don't know offhand), but unless you're doing a huge number of loops over this partial, optimizing this is probably not going to save you a noticeable amount of time.
That said, it would be easy to do some basic benchmarks -- t = Time.now; loop 1000 times over one version; v1_time = t - Time.now; t = Time.now; rinse and repeat with the other version. If you do it, let me know what you find.
All that said, it seems to me that it would be probably be cleaner conceptually to have your controller methods set appropriate flags, if the flags could be expressed as concepts not directly related to the view. I'd be curious what others think.

Refactoring a simple method in my controller

I'm having a tough time deciding how to refactor this method in my controller. The idea is that (in this case) it graphs the users that joined (or were created) in the past two weeks.
You might be wondering why I did the #graph_limit thing, and that is because I always want the day that has the most results to be the tallest bar on my bar chart (which in the view are just created with css by making the height of the <div> using css).
Basically I want to dry it up and... ya know just about improve this method as much as possible:
# Controller
def index
two_weeks_ago = Date.today - 13.days
#users_graphed = User.count(:conditions=>["created_at >= ?", two_weeks_ago], :order => 'DATE(created_at) DESC', :group => ["DATE(created_at)"])
two_weeks_ago.upto(Date.today) do |day|
#graph_limit = 100/#users_graphed.values.max.to_f
#users_graphed[day.to_s] ||= 0
end
end
Also I should mention, that you guys are probably going to rip my code to shreds... so I'm bracing for the outcome.
# View
<% #users_graphed.sort.reverse.each do |user| %>
<li>
<% content_tag :div, :style => "height: #{number_with_precision(user[1] * #graph_limit, :precision => 2)}px; ", :class => "stat_bar" do %>
<%= content_tag(:span, user[1]) unless user[1] == 0 %>
<% end %>
</li>
<% end %>
Ultimately and what my real goal here is to put this into my application controller and be able to chart any models by it's create_at times. maybe something like tasks.chart_by(2.weeks). How would you guys get this separated out into something I can use throughout the whole app?
I agree with Joseph that your controller here is doing a lot of work that should be done in the model. Any time you're specifying multiple find parameters in your controller, ask yourself whether or not that should be in your model instead.
You're doing a lot of iterating here that seems needless. Firstly, You shouldn't be calculating #graph_limit inside the loop. You're recalculating it 14 times, but the value is going to be the same every time. Do that outside the loop.
Secondly, that sort.reverse in your view sticks out. You're already sorting in your finder (:order => 'DATE(created_at) DESC'), and then you're sorting again in your view and then reversing it? You should instead be asking the database for the values in the final order you want them. Then to make your zero-filling code work you can just reverse it, doing Date.today.downto(two_weeks_ago) instead of upto.
I would say that you should really be doing this all in SQL, but unfortunately (as perhaps you've discovered) MySQL makes it difficult to fill in missing days without creating a calendar table to join against.
Thanks Jordan, per your ideas (which were really great by the way) I've created a helper that is like such:
def graph_by_time(klass, time_ago)
time_range_start = Date.today - time_ago
#elements_graphed = klass.count(:conditions=>["created_at >= ?", time_range_start], :order => 'DATE(created_at) DESC', :group => ["DATE(created_at)"])
#graph_limit = 100/#elements_graphed.values.max.to_f
time_range_start.upto(Date.today) do |element|
#elements_graphed[element.to_s] ||= 0
end
return #elements_graphed.sort.reverse
end
The biggest issue here is zero filling the days which have no records associated with them, your method of switching to from upto to downto didnt work and only returned the records which did result in a integer other than zero.

Resources