简体   繁体   中英

Can I refactor the controller to reduce database queries?

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM