I have a view using fragment caching for an ActiveRecord relation, e.g.
<% cache MyModel.all do %>
...
<% end %>
I see two DB queries generated in this case
SELECT COUNT(*) AS "size", MAX("my_model"."updated_at") AS timestamp FROM "my_model"
SELECT "my_model".* from "my_model"
I expect the first one, and it's usually a much more efficient query. I did not expect the second one.
If I instead use:
<% cache ActiveSupport::Cache.expand_cache_key(MyModel.all) do %>
...
<% end %>
then I get only the first query with the same resulting cache key.
Is it a bug or am I doing something wrong?
EDIT: narrowed down to where this happens: see https://github.com/rails/rails/pull/29092#issuecomment-437572543
when
normalize_version
is executed, the relation does not respond to cache_version, and
therefore ends up being expanded with
to_a.
So essentially, calling Product.all.to_a and then for each object
calling cache_version, which returns nil.
Yes, this does look like a bug. Hopefully this would be fixed on this PR, since my own PR for a stopgap fix was rejected
Related
I am trying to get the records to order by :id in the view. I have records ordered by :id in the controller, like so
#bands = Band.where(available: true).order(:id)
Everything appears normal when the app starts, but as records are updated, the ordering behaviour goes wonky. Most often, recently edited records move to the very end, but not always. This shouldn't happen since the records are ordered by :id which shouldn't be changing
For context, in the view is something like
<% #bands.each do |b| %>
<%= b.name %>
<% end %>
Also note, in the rails console, both of these return the correctly ordered results (which makes this problem even stranger):
ActiveRecord::Base.connection.execute("SELECT * FROM bands WHERE available = 'true' ORDER BY id")
and
Band.where(available: true).order(:id)
Also note, when I load the index view, and observe the rails server, I can see that the results are not in the correct order.
I can also see the sql query that has been executed, and it ignores the order part, it simply doesn't have any mention of order in the query
Actually, I don't think it is possible.
But, try this:
#bands = Band.where(available: true).order(:id).to_a
I'm trying to find the last Econ_Result that belongs to a Econ_Report. I want to display the last record of the Econ_Result (ordered by "release_date") for each Econ_Report on the index view. In the controller I tried to take the list of all reports and find the last result using the following:
#econ_reports = EconReport.all
if #econ_reports.econ_results.size >= 1
#last_result = #econ_report.econ_results.last.release_date
end
econ_report.econ_results.size works on the index view when I place it in for each loop. When I try to call the value of the last record I run into issues with the fact that some reports don't yet have results (a temporary issue) so I threw in the if then check in the controller which is currently failing.
Thanks in advance for the rookie help.
Since #econ_reports is a collection of EconReport objects, you can't call an instance method like .econ_results on it. Instead, you can only call it on instances within the collection:
#econ_reports.each do |econ_report|
if econ_report.econ_results.any?
last_result = econ_report.econ_results.last
end
end
However, this can be terribly inefficient for a large collection of #econ_reports: both lines with econ_report.econ_results will query the database separately, meaning that you'll query the database independently for each econ_report in the collection. This is known as the N+1 query problem.
Luckily for you, as discussed in the link, Rails has a built-in solution to optimize this code so you'll only query the database once:
<% #econ_reports.includes(:econ_results).each do |econ_report| %>
<% if econ_report.econ_results.any? %>
<% last_result = econ_report.econ_results.last %>
# do something to display last_result
<% end %>
<% end %>
If you just want the release date you might try:
#last_result = #econ_report.econ_results.order('release_date DESC').limit(1).pluck(:release_date).first
It's worth noting that a Ruby if statement generally looks like:
if condition
end
The then is almost always omitted even though it is allowed.
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.
I have this part of code:
<% current_user.meta_accounts.each do |meta_account| %>
<%= content_tag(:li, link_to(meta_account.name, 'javascript:void(0)')) %>
<% end %>
So, I want Rails to show all my MetaAccounts in list, but I get this:
<li>Wallet</li>
<li>Credit card</li>
<li>Debit card</li>
<li>Debts</li>
<li>javascript:void(0)</li> #This is the problem
So, it also shows me MetaAccount, which isn't created yet.
In my MetaAccounts table I have this. I'm using Postgres.
So, it also shows me the last row, where row_number is *. I don't know why, and how to avoid this.
Thanks for any help!
Try:
<% current_user.meta_accounts.select(&:persisted?).each do |meta_account| %>
<%= content_tag(:li, link_to(meta_account.name, 'javascript:void(0)')) %>
<% end %>
The * row you see in PostgreSQL is not an actual record, it's just a quick way to create new records. If you want to be sure, run the following query:
SELECT COUNT(*) FROM MetaAccounts WHERE user_id=1
It will return 4.
I think the problem comes from an unsaved MetaAccount object in current_user.meta_accounts. This could be the case for instance in a form, where you build an empty object bound to the form. Make sure you don't have a call to current_user.meta_accounts.build(...).
If so, you can simply skip in your each loop all MetaAccount objects with a blank name.
I currently use a query of the form
#recordsfound = Model.where(...)
This is located in my controller and returns all records matching the query. I am using pagination to show a limited number of records at a time. However, when I select to show the next page the query is run again. Is there a way to store the records returned in a variable other than an instance variable and therefore not require the query to be rerun?
Thanks a lot guys
I'm not entirely sure I understand what you mean, but if you want the query to only run once per page even on subsequent visits, you can use fragment caching.
Rails will lazy load, and then use the cache when it hits the query in the view.
<% results.each do |result| %>
<% cache result do %>
<%= result.foo %>
<% end %>
<% end %>
If you have any dependent models, you'll have to make sure you expire the cache when they get updated if necessary:
belongs_to :result, touch: true
Note that if you are in development environment the query will still run. You can change this in your development.rb config file. If you do change this setting, don't forget to revert it . Otherwise strange things will happen and you'll waste your time trying to figure out why your changes aren't visible.
config.action_controller.perform_caching = true