How can I refactor this Rails controller? - ruby-on-rails

I have the following in my controller:
#custom_exercises = #user.exercises.all
#all_exercises = Exercise.not_the_placeholder_exercise.public.order("name").all
if #user.trainers.present?
trainer_exercises = []
#user.trainers.each do |trainer|
trainer_exercises << trainer.exercises.all
end
#my_trainer_custom_exercises = trainer_exercises
end
#exercises = #custom_exercises + #all_exercises
if #my_trainer_custom_exercises.present?
#exercises << #my_trainer_custom_exercises
#exercises.flatten!
end
This feels really messy. How could I refactor this?

First step: set up an AR relationship between users and exercises, probably along the lines of:
class User < ActiveRecord::Base
has_many :trainer_exercises,
:through => :trainers,
:foreign_key => :client_id,
:source => :exercises
end
Second step: move #all_exercises to a class method in Exercise.
class Exercise < ActiveRecord::Base
def self.all_exercises
not_the_placeholder_exercise.public.order("name").all
end
end
This way, the whole controller gets a whole lot simpler:
#custom_exercises = #user.exercises.all
#trainer_exercises = #user.trainer_exercises.all
#exercises = Exercise.all_exercises + #custom_exercises + #trainer_exercises

From a purely less lines of code perspective, you could start with this ( more or less / not tested but should work:
if #user.trainers.present?
#my_trainer_custom_exercises = #user.trainers.each.inject([]){ |trainer, trainer_exercises|
trainer_exercises << trainer.exercises.all
}
end

Related

Display Similar Items With Having Distinct Count Rails 5.1

I'm trying to display a list of gins that have a similar minimum number of botanicals on my show page. I feel I'm close, but the current output is not right. It's actually just printing the name of the gin a number of times.
Gin Load (1.6ms) SELECT "gins".* FROM "gins" INNER JOIN
"gins_botanicals" ON "gins_botanicals"."gin_id" = "gins"."id" INNER
JOIN "botanicals" ON "botanicals"."id" =
"gins_botanicals"."botanical_id" WHERE "botanicals"."id" IN (4, 10, 3)
AND ("gins"."id" != $1) GROUP BY gins.id HAVING (COUNT(distinct
botanicals.id) >= 3) [["id", 2]]
I have three models; two resources with a joins table:
gin.rb
class Gin < ApplicationRecord
belongs_to :distillery, inverse_of: :gins
accepts_nested_attributes_for :distillery, reject_if: lambda {|attributes| attributes['name'].blank?}
acts_as_punchable
has_many :gins_botanical
has_many :botanicals, through: :gins_botanical
botanical.rb
class Botanical < ApplicationRecord
has_many :gins_botanical
has_many :gins, through: :gins_botanical
gins_botanical.rb
class GinsBotanical < ApplicationRecord
belongs_to :gin
belongs_to :botanical
gins_controller
def show
#gin = Gin.friendly.find(params[:id])
#gin.punch(request)
#meta_title = meta_title #gin.name
#similiar_gins = Gin.joins(:botanicals).where("botanicals.id" => #gin.botanical_ids).where.not('gins.id' => #gin.id).having("COUNT(distinct botanicals.id) >= 3").group("gins.id")
end
so in #similar_gins i am trying to count how many matching botanicals does the current #gin have compared to all the other #gins and if >= 3 return the values.
And in my view:
show.html.erb
<% #similiar_gins.each do |gin| %>
<%= #gin.name %>
<% end %>
I'm suspecting my where is not correct...
Yes, I have the similar feature but I have implemented like below
#gin = Gin.find(params[:id])
if #gin.botanicals.count > 1
#botanicals = #gin.botanical_ids
#gin_ids = Botanical.select('distinct gin_id').where('gin_id IN (?)', #botanicals).limit(10)
#ids = #gin_ids.map(&:gin_id)
#similiar_gins = Gin.where('id IN (?)', #ids).where.not(id: #gin) #=> similar all without current gin
end
This code is converted from my code which is relation is category and jobs, if you need to see my code for showing the similar jobs then it is
def show
#job = Job.find(params[:id])
if #job.categories.count > 1
#category = #job.category_ids
#jobs = JobCategory.select('distinct job_id').where('category_id IN (?)', #category).limit(10)
ids = #jobs.map(&:job_id)
#releted_jobs = Job.where('id IN (?)', ids).where.not(id: #job)
end
end
Hope it helps

Custom Rails Dashboard, how to optimize data retrieval to display in view?

I am making a custom dashboard for a school application that requires me to calculate some KPIs, the way am doing it right now is calling several class methods from the Opportunity class in the dashboard/index action from the controller, and storing each method result in a variable that is going to be used in a tile. So each variable is a different tile of the dashboard.
The methods belong to the Opportunity class shown below:
class Opportunity < ApplicationRecord
belongs_to :organization
belongs_to :opportunity_status
has_many :tasks, dependent: :destroy
has_many :opportunity_status_logs, dependent: :destroy
before_create :create_status_log
after_update :create_status_log, if: :opportunity_status_id_changed?
validates :name, :description, :revenue, :opportunity_status_id, :closing_date, presence: true
validates :name, :description, format: { with: /\A[[:alpha:]a-zA-Z0-9ñÑ#()\-.,\s]+\z/ }
validates :revenue, numericality: true
validates :closing_date, inclusion: { in: (Time.zone.today..Time.zone.today+5.years) }
def create_status_log
OpportunityStatusLog.create(opportunity_id: self.id, opportunity_status_id: self.opportunity_status_id)
end
def status_updated_by(user)
#status_log = self.opportunity_status_logs.last
#status_log.user_id = user.id
#status_log.save!
end
def self.actives
self.where.not(opportunity_status_id: [11,12])
end
def self.won
self.where(opportunity_status_id: 11)
end
def self.lost
self.where(opportunity_status_id: 12)
end
def self.average_revenue
self.won.average(:revenue)
end
def self.minimum_revenue
self.won.minimum(:revenue)
end
def self.maximum_revenue
self.won.maximum(:revenue)
end
def self.filter_by_status(status_id)
self.where(opportunity_status: status_id)
end
def self.relative_percentage(item_amount, total)
item_amount * 100 / total
end
def self.conversion_rate
self.won.count / self.all.count.to_f * 100
end
def self.potential_revenue
self.actives.sum(:revenue)
end
end
and this is the way the controller is structured:
class DashboardController < ApplicationController
before_action :authenticate_user!
def index
#opportunities = Opportunity.includes(:opportunity_status).all
#actives = Opportunity.actives.count
#won = Opportunity.won.count
#lost = Opportunity.lost.count
#average_revenue = Opportunity.average_revenue
#minimum_revenue = Opportunity.minimum_revenue
#maximum_revenue = Opportunity.maximum_revenue
#in_appreciation = Opportunity.filter_by_status(6).count
#in_value_proposition = Opportunity.filter_by_status(7).count
#in_management_analysis = Opportunity.filter_by_status(8).count
#in_proposal = Opportunity.filter_by_status(9).count
#in_review = Opportunity.filter_by_status(10).count
#app_perc = Opportunity.relative_percentage(#in_appreciation, #opportunities.count)
#vp_perc = Opportunity.relative_percentage(#in_value_proposition, #opportunities.count)
#ma_perc = Opportunity.relative_percentage(#in_management_analysis, #opportunities.count)
#pp_perc = Opportunity.relative_percentage(#in_proposal, #opportunities.count)
#rw_perc = Opportunity.relative_percentage(#in_review, #opportunities.count)
#conversion_rate = '%.2f' % [Opportunity.conversion_rate]
#potential_revenue = Opportunity.potential_revenue
end
end
Even though it works as expected, it looks like the controller is a bit too fat and I feel that with the current approach if the app scales it will be very slow due to the amount of queries that are being done. So, is there a way to refactor this in order to optimize the data retrieval and the displaying of the KPIs?
Thanks in advance
You can try implementing Facade Pattern in Rails. It will make your controller skinny but on the query part you will still be needing to make those queries, there is no way to skip that.
You can try to optimize db by adding index and creating sql views in future when you get performance lag, at this time it will be like premature optimization

Rails ActiveRecord intersect query with has_many association

I have the following models:
class Piece < ActiveRecord::Base
has_many :instrument_pieces
has_many :instruments, through: :instrument_pieces
end
class Instrument < ActiveRecord::Base
has_many :pieces, through: :instrument_pieces
has_many :instrument_pieces
end
class InstrumentPiece < ActiveRecord::Base
belongs_to :instrument
belongs_to :piece
end
And I have the following query:
Piece
.joins(:instrument_pieces)
.where(instrument_pieces: { instrument_id: search_params[:instruments] } )
.find_each(batch_size: 20) do |p|
Where search_params[:instruments] is an array. The problem with this query is that it will retrieve all pieces that have any of the instruments, so if search_params[:instruments] = ["1","3"], the query will return pieces with an instrument association of either 1 or 3 or of both. I'd like the query to only return pieces whose instrument associations include both instruments 1 and 3. I've read through the docs, but I'm still not sure how this can be done...
It seems like what I wanted was an intersection between the two queries, so what i ended up doing was:
queries = []
query = Piece.joins(:instruments)
search_params[:instruments].each do |instrument|
queries << query.where(instruments: {id: instrument})
end
sql_str = ""
queries.each_with_index do |query, i|
sql_str += "#{query.to_sql}"
sql_str += " INTERSECT " if i != queries.length - 1
end
Piece.find_by_sql(sql_str).each do |p|
Very ugly, but ActiveRecord doesn't support INTERSECT yet. Time to wait for ActiveRecord 5, I suppose.
You can use where clause chaining to achieve this. Try:
query = Piece.joins(:instrument_pieces)
search_params[:instruments].each do |instrument|
query = query.where(instrument_pieces: { instrument_id: instrument } )
end
query.find_each(batch_size: 20) do |p|
or another version
query = Piece.joins(:instruments)
search_params[:instruments].each do |instrument|
query = query.where(instrument_id: instrument)
end
query.find_each(batch_size: 20) do |p|

How do I copy objects of different classes?

I have two models:
class Song < ActiveRecord::Base
attr_accessible :title, :singer, :year, :production
end
and:
class SongsCopy < ActiveRecord::Base
attr_accessible :title, :singer, :year
end
What is the most simple way to copy attributes from A(Song) to B(SongsCopy) while creating B, remembering SongsCopy has no attribute :production?
The optimal way would be to do it inside the database with a bit of SQL:
insert into songs_copies (title, singer, year)
select title, singer, year
from songs
where ...
But if you have a bunch of callbacks and such that you need to run then you could do something like this:
song = some_song_that_you_already_have
copy = SongsCopy.create(song.attributes.except('id', 'production'))
or:
copy = SongsCopy.create(song.attributes.slice('title', 'singer', 'year'))
It's not the prettiest possibility (and certainly not preferred), but the easiest would be:
class SongsCopy < ActiveRecord::Base
def initialize(args = nil)
if args.is_a? Song
super
self.title = song.title
self.singer = song.singer
self.year = song.year
else
super(args)
end
end
end
a = Song
b = SongsCopy.new(a)
I'm sure there's another way to do this, but the above should work.

Ways to simplify and optimize my code?

I've got some code which i would like to optimize.
First, not bad at all, but maybe it can be a bit shorter or faster, mainly the update_result method:
class Round < ActiveRecord::Base
belongs_to :match
has_and_belongs_to_many :banned_champions, :class_name => "Champion", :join_table => "banned_champions_rounds"
belongs_to :clan_blue, :class_name => "Clan", :foreign_key => "clan_blue_id"
belongs_to :clan_purple, :class_name => "Clan", :foreign_key => "clan_purple_id"
belongs_to :winner, :class_name => "Clan", :foreign_key => "winner_id"
after_save {self.update_result}
def update_result
match = self.match
if match.rounds.count > 0
clan1 = match.rounds.first.clan_blue
clan2 = match.rounds.first.clan_purple
results = {clan1=>0, clan2=>0}
for round in match.rounds
round.winner == clan1 ? results[clan1] += 1 : results[clan2] += 1
end
if results[clan1] > results[clan2] then
match.winner = clan1; match.looser = clan2
match.draw_1 = nil; match.draw_2 = nil
elsif results[clan1] < results[clan2] then
match.winner = clan2; match.looser = clan1
match.draw_1 = nil; match.draw_2 = nil
else
match.draw_1 = clan1; match.draw_2 = clan2
match.winner = nil; match.looser = nil
end
match.save
end
end
end
And second, totally bad and slow in seeds.rb:
require 'faker'
champions = [{:name=>"Akali"},
{:name=>"Alistar"},
{:name=>"Amumu"},
{:name=>"Anivia"},
{:name=>"Annie"},
{:name=>"Galio"},
{:name=>"Tryndamere"},
{:name=>"Twisted Fate"},
{:name=>"Twitch"},
{:name=>"Udyr"},
{:name=>"Urgot"},
{:name=>"Veigar"}
]
Champion.create(champions)
10.times do |n|
name = Faker::Company.name
clan = Clan.create(:name=>name)
6.times do |n|
name = Faker::Internet.user_name
clan.players.create(:name=>name)
end
end
for clan in Clan.all do
2.times do
match = Match.create()
c = [clan,Clan.first(:offset => rand(Clan.count))]
3.times do
round = match.rounds.create
round.clan_blue = c[0]
round.clan_purple = c[1]
round.winner = c[0]
round.save!
end
for item in c
for p in item.players.limit(5)
rand_champion = Champion.first(:offset => rand(Champion.count))
match.participations.create!(:player => p, :champion => rand_champion)
end
end
match.save!
end
2.times do
match = Match.create()
c = [clan,Clan.first(:offset => rand(Clan.count))]
3.times do
round = match.rounds.create
round.clan_blue = c[0]
round.clan_purple = c[1]
round.winner = c[1]
round.save!
end
for item in c
for p in item.players.limit(5)
rand_champion = Champion.first(:offset => rand(Champion.count))
match.participations.create!(:player => p, :champion => rand_champion)
end
end
match.save!
end
2.times do
match = Match.create()
c = [clan,Clan.first(:offset => rand(Clan.count))]
2.times do |n|
round = match.rounds.create
round.clan_blue = c[0]
round.clan_purple = c[1]
round.winner = c[n]
round.save!
end
for item in c
for p in item.players.limit(5)
rand_champion = Champion.first(:offset => rand(Champion.count))
match.participations.create!(:player => p, :champion => rand_champion)
end
end
match.save!
end
end
Any chances to optimize them?
Don't underestimate the value of whitespace in cleaning up code readability!
class Round < ActiveRecord::Base
belongs_to :match
belongs_to :clan_blue, :class_name => "Clan", :foreign_key => "clan_blue_id"
belongs_to :clan_purple, :class_name => "Clan", :foreign_key => "clan_purple_id"
belongs_to :winner, :class_name => "Clan", :foreign_key => "winner_id"
has_and_belongs_to_many :banned_champions, :class_name => "Champion", :join_table => "banned_champions_rounds"
after_save { match.update_result }
end
class Match < ActiveRecord::Base
def update_result
return unless rounds.count > 0
clan1, clan2 = rounds.first.clan_blue, rounds.first.clan_purple
clan1_wins = rounds.inject(0) {|total, round| total += round.winner == clan1 ? 1 : 0 }
clan2_wins = rounds.length - clan1_wins
self.winner = self.loser = self.draw_1 = self.draw_2 = nil
if clan1_wins == clan2_wins
self.draw1, self.draw2 = clan1, clan2
else
self.winner = clan1_wins > clan2_wins ? clan1 : clan2
self.loser = clan1_wins < clan2_wins ? clan1 : clan2
end
save
end
end
For your seeds, I'd replace your fixtures with a factory pattern, if it's for tests. If you're going to stick with what you have there, though, wrap the whole block in a transaction and it should become orders of magnitude faster.
Well, on your first example, it appears that you are forcing Match behavior into your Round class, which is not consistent with abstract OOP. Your update_result method actually belongs in your Match class. Once you do that, I think the code will clean itself up a bit.
On your second example, it's hard to see what you are trying to do, but it's not surprising that it's so slow. Every single create and save generates a separate database call. At first glance your code generates over a hundred separate database saves. Do you really need all those records? Can you combine some of the saves?
Beyond that, you can cut your database calls in half by using build instead of create, like this:
round = match.rounds.build
round.clan_blue = c[0]
round.clan_purple = c[1]
round.winner = c[0]
round.save!
If you want to save some lines of code, you could replace the above with this syntax:
match.rounds.create(:clan_blue_id => c[0].id, :clan_purple_id => c[1].id, :winner_id => c[0].id)
In your seeds file:
c = [clan,Clan.first(:offset => rand(Clan.count))]
This works, but it looks like you're picking a random number in Ruby. From what I understand, if you can do something in SQL instead of Ruby, it's generally faster. Try this:
c = [clan,Clan.find(:all, :limit => 1, :order => 'random()')
You won't get too many gains since it's only run twice per clan (so 20x total), but there are similar lines like these two
# (runs 60x total)
rand_champion = Champion.first(:offset => rand(Champion.count))
# (runs up to 200x, I think)
c = [clan,Clan.first(:offset => rand(Clan.count))]
In general, you can almost always find something more to optimize in your program. So your time is most efficiently used by starting with the areas that are repeated the most--the most deeply nested loops. I'll leave optimizing the above 2 lines (and any others that may be similar) to you as an exercise. If you're having trouble, just let me know in a comment.
Also, I'm sure you'll get a lot of good suggestions in many of the responses, so I highly highly highly recommend setting up a benchmarker so you can measure the differences. Be sure run it several times for each version you test, so you can get a good average (programs running in the background could potentially throw off your results).
As far as simplicity, I think readability is pretty important. It won't make your code run any faster, but it can make your debugging faster (and your time is important!). The few things that were giving me trouble were nondescript variables like c and p. I do this too sometimes, but when you have several of these variables in the same scope, I very quickly reach a point where I think "what was that variable for again?". Something like temp_clan instead of c goes a long way.
For readability, I also prefer .each instead of for. That's entirely a personal preference, though.
btw I love League of Legends :)
Edit: (comments won't let me indent code) Upon taking a second look, I realized that this snippet can be optimized further:
for p in item.players.limit(5)
rand_champion = Champion.first(:offset => rand(Champion.count))
match.participations.create!(:player => p, :champion => rand_champion)
end
change Champion.first(:offset => rand(Champion.count))
rand_champs = Champion.find(:all, :limit => 5, :order => 'random()')
for p ...
i = 0
match.participations.create!(:player => p, :champion => rand_champs(i))
i++
end
This will reduce 5 SQL queries into 1. Since it's called 60x, this will reduce your SQL queries from 60 to 12. As an extra plus, you won't get repeated champions on the same team, (or I guess that could be a downside if that was your intention)

Resources