My rails app suffers from performance issues. The root page of my app loads several images that have been attached to articles using Active Storage. The (index) action for the page is as follows:
class ArticlesController < ApplicationController
def index
@articles = Article.with_attached_image.all
@top = Article.with_attached_image.last
@news = Article.with_attached_image.where(category: 'news').last
@business = Article.with_attached_image.where(category: 'business').last
@ent = Article.with_attached_image.where(category: 'entertainment').last
@tech = Article.with_attached_image.where(category: 'tech').last
@sports = Article.with_attached_image.where(category: 'sports').last
@op = Article.with_attached_image.where(category: 'opinion').last
end
end
The corresponding view is as follows:
<div class="grid-container">
<div class="main" style="background-image: url(<%= (url_for(@top.image)) %>)">
<div class="top-article" >
<h2><%= @top.title %></h2>
<p><%= link_to "Read more..." %></p>
</div>
</div>
<div class="sports" style="background-image: url(<%= (url_for(@sports.image)) %>)">
<h2><%= link_to "Sports", sports_path %></h2>
</div>
<div class="tech" style="background-image: url(<%= (url_for(@tech.image)) %>)">
<h2><%= link_to "Technology", tech_path %></h2>
</div>
<div class="opinion" style="background-image: url(<%= (url_for(@op.image)) %>)">
<h2><%= link_to "Opinion", opinion_path %></h2>
</div>
<div class="entertainment" style="background-image: url(<%= (url_for(@ent.image)) %>)">
<h2><%= link_to "Entertainment", entertainment_path %></h2>
</div>
<div class="business" style="background-image: url(<%= (url_for(@business.image)) %>)">
<h2><%= link_to "Business", business_path %></h2>
</div>
<div class="news" style="background-image: url(<%= (url_for(@news.image)) %>)">
<h2><%= link_to "News", news_path %></h2>
</div>
</div>
I'm assuming that the best plan of action for reducing the number of database calls is a nested query. However, I'm unsure of how to implement this or if it is even the best solution. Any help in refactoring this index action to reduce database calls and improve overall app performance would be greatly appreciated.
The number of queries there shouldn't be a problem. If you have a ton of articles and is frequently filtering by categories, you could add an index to the category
column, by adding this to a migration, for example:
def change
add_index :articles, :category
end
Reference: https://guides.rubyonrails.org/active_record_migrations.html
You could also extract the categories into their own model/table, as is most usual for this kind of data, and index the foreign key on the articles table that points to the categories table:
# Create the categories table
def change
create_table :categories do |t|
t.name, null: false, index: {unique: true}
end
end
# Add the foreign key to articles
def change
change_table :articles do |t|
t.references :category, foreign_key: true, index: true, null: false
end
end
# Category model
class Category < ApplicationRecord
has_many :articles
end
# Article model
class Article < ApplicationRecord
belongs_to :category, required: true
scope :by_category, ->(category_name) do
joins(:category).where(categories: {name: category_name})
end
end
# Controller
class ArticlesController < ApplicationController
def index
@news = Article.with_attached_image.by_category('news').last
# etc...
end
end
Also, if you are using the @articles
variable at all, consider using some kind of pagination (eg kaminari ) so you don't fetch and initialize all records in the table.
You're asking the database for different slices of data you already have. You can do most of this by just working with @articles
as an array.
def index
@articles = Article.with_attached_image.all
@top = @articles.last
@categories = {}
%w( news business entertainment tech sports opinion ).each do |category|
@categories[category] = @articles.reverse.find do |article|
article.category == category
end
end
end
But if you don't use @articles
in your view, it may be faster to just fetch each category individually, just like you have it, since each query should only return one record. This should be pretty fast if you have database indexes setup properly.
def index
# Omit potentially unused call to: Article.with_attached_image.all
@top = Article.with_attached_image.last
@news = Article.with_attached_image.where(category: 'news').last
@business = Article.with_attached_image.where(category: 'business').last
@ent = Article.with_attached_image.where(category: 'entertainment').last
@tech = Article.with_attached_image.where(category: 'tech').last
@sports = Article.with_attached_image.where(category: 'sports').last
@op = Article.with_attached_image.where(category: 'opinion').last
end
The following will perform only two queries, and will use a constant amount of memory regardless of the size of the articles
table.
def index
# First query
@top = Article.with_attached_image.last
# Second query
one_random_article_per_category = Article.find_by_sql(
<<-SQL.squish
select *
from articles
where category = 'news'
limit 1
union all
select *
from articles
where category = 'business'
limit 1
union all
-- etc ...
SQL
)
articles_by_category = one_random_article_per_category.group_by(&:category)
last_article_in_category = ->(cat) { articles_by_category.fetch(cat).last }
@news = last_article_in_category['news']
@business = last_article_in_category['business']
@ent = last_article_in_category['entertainment']
@tech = last_article_in_category['tech']
@sports = last_article_in_category['sports']
@op = last_article_in_category['opinion']
end
The SQL is repetitive, but effective. Generating the SQL from an array of categories is probably desirable, and left as an exercise for the reader.
@articles
is not used in the view template, so I've omitted it here.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.