Rails: find_by making additional sql query, previously did includes - activerecord

I've got three classes:
class Batch
has_many :final_grade_batches
end
class FinalGradeBatch
belongs_to :batch
belongs_to :student
end
class Student
has_many :final_grade_batches
end
I want to retrieve a final_grade_batch the following way:
batch = Batch.includes(:final_grade_batches).find(1)
batch.final_grade_batches.find_by(student_id: 2)
The final lines produces this SQL query:
FinalGradeBatch Load (0.6ms) SELECT "final_grade_batches".* FROM "final_grade_batches" WHERE "final_grade_batches"."batch_id" = $1 AND "final_grade_batches"."student_id" = 2 LIMIT 1 [["batch_id", 1]]
If I included final_grade_batches in the Batch find query, why is it looking for the final grade batch again? I know it needs to find the one that has the student's id, but should Rails make a query to get that? Isn't it loaded into memory by now?
Is there any way I can get a final grade batch without Rails hitting the database again? Thanks!

find_by always makes database queries. Substitute it with:
batch.final_grade_batches.select( |grade_batch| grade_batch.student_id == 2).first
This works with the array you already loaded from the database instead of doing an additional query.

Related

Ruby Sequel STI model_map or key_map issue with DB queries

I have a module with a 'parent' class, Trait, and its 'children' inherited classes, Text and Image, using the Ruby Sequel gem with Postgresql within a module, Tank.
module Tank; end
module Tank
class Trait < Sequel::Model
plugin :single_table_inheritance, :type
end
end
module Tank
class Image < Trait; end
end
module Tank
class Text < Trait; end
end
While within a console, I attempt to call #destroy on a trait. This results in an error because Sequel attempts to find a Trait with #type Tank::Text as shown below.
> Trait[439555].destroy
Sequel::NoExistingObject: Attempt to delete object did not result in
a single row modification (Rows Deleted: 0, SQL: DELETE FROM "traits"
WHERE (("traits"."type" IN ('Tank::Text')) AND ("id" = 439555)))
I have tried using some of the advanced options; #model_map, #key_map, and #key_chooser as seen in the Sequel documentation: http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/SingleTableInheritance.html.
Admittedly I am not so familiar with lambdas and procs, but this was my attempt:
module HoldingTank
class Trait < Sequel::Model
plugin :single_table_inheritance, :type,
model_map: { 'Text'=>:Text, 'Image'=>:Image }
end
end
and I tried adding the following when that did not work:
key_chooser: lambda {|i| i.model.sti_key_map[i.model.to_s.split('::').last].first}
with both tries having similar results to the following, where now it is searching for rows where 1 = 0 (obviously something went wrong):
> Trait[439555].destroy
Sequel::NoExistingObject: Attempt to delete object did not result in
a single row modification (Rows Deleted: 0, SQL: DELETE FROM "traits"
WHERE ((1 = 0) AND ("id" = 439555)))
For what it's worth, the Trait class already existed as entirely Text entries before I added STI. I updated all the current Trait instances to have #type = 'Text' which I have confirmed in the console. I also get these results and I don't know if they are to be expected or not as I am new to Sequel and STI:
> Text.count
=> 0
> Trait.where(type: 'Text').count
=> 931589
Thanks in advance for any assistance.

How to order by integer returned by instance method? [duplicate]

Is there anyway I can order the results (ASC/DESC) by number of items returned from the child model (Jobs)?
#featured_companies = Company.joins(:jobs).group(Job.arel_table[:company_id]).order(Job.arel_table[:company_id].count).limit(10)
For example: I need to print the Companies with highest jobs on top
Rails 5+
Support for left outer joins was introduced in Rails 5 so you can use an outer join instead of using counter_cache to do this. This way you'll still keep the records that have 0 relationships:
Company
.left_joins(:jobs)
.group(:id)
.order('COUNT(jobs.id) DESC')
.limit(10)
The SQL equivalent of the query is this (got by calling .to_sql on it):
SELECT "companies".* FROM "companies" LEFT OUTER JOIN "jobs" ON "jobs"."company_id" = "companies"."id" GROUP BY "company"."id" ORDER BY COUNT(jobs.id) DESC
If you expect to use this query frequently, I suggest you to use built-in counter_cache
# Job Model
class Job < ActiveRecord::Base
belongs_to :company, counter_cache: true
# ...
end
# add a migration
add_column :company, :jobs_count, :integer, default: 0
# Company model
class Company < ActiveRecord::Base
scope :featured, order('jobs_count DESC')
# ...
end
and then use it like
#featured_company = Company.featured
#user24359 the correct one should be:
Company.joins(:jobs).group("companies.id").order("count(companies.id) DESC")
Something like:
Company.joins(:jobs).group("jobs.company_id").order("count(jobs.company_id) desc")
Added to Tan's answer. To include 0 association
Company.joins("left join jobs on jobs.company_id = companies.id").group("companies.id").order("count(companies.id) DESC")
by default, joins uses inner join. I tried to use left join to include 0 association
Adding to the answers, the direct raw SQL was removed from rails 6, so you need to wrap up the SQL inside Arel (if the raw SQL is secure meaning by secure avoiding the use of user entry and in this way avoid the SQL injection).
Arel.sql("count(companies.id) DESC")
Company.where("condition here...")
.left_joins(:jobs)
.group(:id)
.order('COUNT(jobs.id) DESC')
.limit(10)

Changing ID to something more friendly

When creating a record the URL generated to view that record ends with its id
/record/21
I would like to be able to change that to something easier to read, such as my name and reference attributes from the model. I have looked at friendly_id but has trouble implementing a custom method to generate the URL
class Animal < ActiveRecord::Base
extend FriendlyId
friendly_id :name_and_ref
def name_and_ref
"#{name}-#{reference}"
end
end
I ended up getting an error
PG::UndefinedColumn: ERROR: column animals.name_and_ref does not exist LINE 1: SELECT "animals".* FROM "animals" WHERE "animals"."name_an... ^ : SELECT "animals".* FROM "animals" WHERE "animals"."name_and_ref" = 'Clawd-A123456' ORDER BY "animals"."id" ASC LIMIT 1
def show
#animal = Animal.friendly.find(params[:id])
end
I then come across the to_param method which Rails has available, in my model I have
def to_param
"#{self.id}-#{self.name}"
end
which will generate a URL for me of
/19-clawd
This works, but when I do the following it throws an error
def to_param
"#{self.name}-#{self.reference}"
end
My question though is how can I generate the URL to be name and reference without it throwing
Couldn't find Animal with 'id'=Clawd-A123456
If you would like to use your own "friendly id" then you'll need to adjust the find statement in your controller to something like
id = params[:id].split(/-/, 2).first
#animal = Animal.find(id)
Similarly, for the name/reference combination
name, reference = params[:id].split(/-/, 2)
#animal = Animal.find_by(name: name, reference: reference)
The second choice is a little more difficult because you'll have to do some work in the model to guarantee that the name/reference pair is unique.
The easiest way, is to go with friendly_id and simply add the missing database column. Keep in mind that you will need to ensure this new column is unique for every record. It basically acts as primary key.

Simplify multiple calls to ActiveRecord #find down to single line/SQL statement

I'm having a little brain block when it comes to condensing the use of two #find methods in ActiveRecord down to a single statement and SQL query.
I have a Sinatra route where the slug of both a parent and child record are supplied (the parent has many children). Atm I'm first finding the parent with a #find_by_slug call and then the child by #find_by_slug again on the matched parents association.
This results in two SQL queries that in my mind should be able to be condensed down to one easily... Only I can't work out how that's achieved with ActiveRecord.
Model, route and AR log below. Using ActiveRecord 3.2
Edit
I realised I need to clarify the exact outcome to require (I write this very late in the day). I only require the Episode but atm I'm getting the Show first in-order to get to the Episode. I only require the Episode and figured their must be a way to get at that object without adding the extra line and getting the Show first.
Model
class Show < ActiveRecord::Base
has_many :episodes
end
class Episode < ActiveRecord::Base
belongs_to :show
end
Sinatra route
get "/:show_slug/:episode_slug" do
#show = Show.find_by_slug show_slug
#episode = #show.episodes.find_by_slug episode_slug
render_template :"show/show"
end
ActiveRecord logs
Show Load (1.0ms) SELECT `shows`.* FROM `shows` WHERE `shows`.`slug` = 'the-show-slug' LIMIT 1
Episode Load (1.0ms) SELECT `show_episodes`.* FROM `show_episodes` WHERE `show_episodes`.`show_id` = 1 AND `show_episodes`.`slug` = 'episode-21' LIMIT 1
If you only need the #episode, you can maybe do
#episode = Episode.joins(:shows).where('shows.slug = ?', show_slug).where('episodes.slug = ?', episode_slug).first
If you also need #show, you've got to have two queries.

ActiveRecord conditions across multiple tables.

I'm trying to retrieve cars from my database where each car has a manufacturer, and can have multiple styles.
For example, a ford fiesta is a coupe, sedan and hatch.
I've got my relationships set-up in my models, but now I want to create a query to return the results. The query construction will depend on what parameters are supplied.
This is what I've got so far
conditions = {}
conditions[:manufacturer_id] = params[:manufacturer_id] unless params[:manufacturer_id].blank? # this works!
conditions[:style_id] = "style_id IN (?)", params[:style_ids] unless params[:style_ids].blank? #this breaks it :(
cars = Car.find(:all, :conditions=> conditions)
return render :json => cars
The error getting returned is
PG::Error: ERROR: column cars.style_ids does not exit of course this is because the style_id is in a join table called cars_styles. Is there a way to tell ActiveRecord which table to look for within the condition?
The key thing here is that I want to only have one controller method which takes the params in existence and then creates the right query. So if I don't have a manufacturer_id, it will only query the styles, or if vice versa. Of course, I'll be adding other params later too.
I ended up doing this with scoped queries like this
scope :from_manufacturer, lambda{|manu|{:joins => :manufacturer, :conditions => "manufacturers.id = #{manu}" }}
scope :from_style, lambda{|style|{:joins => :style, :conditions => "styles.id = #{style}"}}
def self.get_cars(params)
scope = self
[:manufacturer,:style].each do |s|
scope = scope.send("from_#{s}", params[s]) if params[s].present?
end
scope
end
Works great!

Resources