Suppose you have the following models:
class Category < ActiveRecord::Base
has_one :current_heat, class_name: 'Heat'
has_many :scores, :through => :current_heat
end
class Heat < ActiveRecord::Base
belongs_to :category
has_many :scores
end
class Score < ActiveRecord::Base
belongs_to :heat
end
Surprisingly, when I invoke Category.first.scores ActiveRecord produces the following queries:
SELECT `categories`.* FROM `categories` LIMIT 1
SELECT * FROM `scores` INNER JOIN `heats` ON `scores`.`heat_id` = `heats`.`id` WHERE `heats`.`category_id` = 1
The above query ignores the has_one nature of Category#current_heat. I would have expected something more like:
SELECT `categories`.* FROM `categories` LIMIT 1
SELECT `heats`.* FROM `heats` WHERE `heats`.`category_id` = 1 LIMIT 1
SELECT * FROM `scores` WHERE `scores`.`heat_id` = 6
which is produced only when you explicitly traverse the has_one association from the root with Category.first.current_heat.scores.
It's as if ActiveRecord is silently treating my has_one as a has_many. Can someone explain this behavior to me? Is there an elegant workaround or a "right way" to do it?
Maybe you could remove the
has_many :scores, :through => :current_heat
and instead just delegate :scores through the has_one:
delegate :scores, :to => :current_heat
that would preserve your desired access method Category.first.scores.
has_one doesn't really exist to babysit your database in this fashion. It won't throw errors if there is more than one record that matches the foreign_key, it will just choose the first one. It assumes you haven't errantly added extra records which would break the has_one relation on your own.
In conclusion, the sql that it generates is fine as long as there is only one record attached to the Category. If somehow you've added extra records which shouldn't exist since it is a has_one, then it won't work, but it's not the job of activerecord to tell you that this has happened.
Related
I'm getting a somewhat cryptic error message of
ActiveRecord::ConfigurationError 1
when trying to execute #invoices = invoice.joins(pos: [vendor_id: 1])
I am trying to call all invoices that belong to POs that belong to Vendor 1.
My models are setup as follows:
Vendors can have many POs, and POs can have many invoices
class Vendor < ApplicationRecord
has_many :pos
class Po < ApplicationRecord
belongs_to :vendor
has_many :items, :dependent => :destroy
has_many :invoices
class Invoice < ApplicationRecord
belongs_to :po
I gather that the ActiveRecord::ConfigurationError is
Raised when association is being configured improperly or user tries
to use offset and limit together with ActiveRecord::Base.has_many or
ActiveRecord::Base.has_and_belongs_to_many associations.
But I'm having trouble figuring out what's wrong with my associations. Any ideas?
I think your associations are OK... I think your statement is just bad, and usually when I need to query like this, I resort to a little SQL clause because it's either too complex or flat-out hard to read otherwise.
Try this (note the capital I in Invoice):
vendor_id = 1
#invoices = Invoice.joins(:po).where("pos.vendor_id = ?", vendor_id)
I have the following model setup in a Rails 5.0.0.1 app on Heroku with Postgres 9.5.5:
class Tape < ApplicationRecord
has_many :numbers
belongs_to :player
end
class Player < ApplicationRecord
has_many :tapes
has_many :snapshots
end
class Number < ApplicationRecord
belongs_to :tape
belongs_to :snapshot
end
class Snapshot < ApplicationRecord
has_many :numbers, dependent: :destroy
belongs_to :player
end
#I want to order #numbers by the created_at field of its other parent, Snapshot, not tape
#tape = Tape.includes(player: {snapshots: :numbers}).find 55
#numbers = Number.includes(snapshot: :player).where(players: {id: #tape.player_id}, tape_id: #tape.id).order('snapshots.created_at DESC')
It seems like I'm having to do extra SQL work simply to order #numbers by its parent Snapshot.
I tried #tape.numbers.joins(:snapshot).order('snapshots.created_at DESC') and I see a SQL query ending in something like '....ORDER BY "numbers"."id" ASC, snapshots.created_at DESC' which means Rails interprets this to mean I want to order by numbers.id as well - which I don't. I only want to order by snapshots.created_at. Now a query like #tape.numbers.joins... will also be an extra SQL query. 2 questions:
1) what's the code I can write that will result in the least amount of SQL work? Or is what I have pretty much it?
2) Why does rails insert the extra '....ORDER BY "numbers"."id" clause in #tape.numbers.joins(:snapshot).order('snapshots.created_at DESC')?
I'm using Rails 5.0.0.1 ATM and i've come across issue with ActiveRecord relations when optimizing count of my DB requests.
Right now I have:
Model A (let's say 'Orders'), Model B ('OrderDispatches'), Model C ('Person') and Model D ('PersonVersion').
Table 'people' consists only of 'id' and 'hidden' flag, rest of the people data sits in 'person_versions' ('name', 'surname' and some things that can change over time, like scientific title).
Every Order has 'receiving_person_id' as for the person which recorded order in DB and every OrderDispatch has 'dispatching_person_id' for the person, which delivered order. Also Order and OrderDispatch have creation time.
One Order has many dispatches.
The straightforward relations thus is:
has_many :receiving_person, through: :person, foreign_key: "receiving_person_id", class_name: 'PersonVersion'
But when I list my order with according dispatches I have to deal with N+1 situation, because to find accurate (according to the creation date of Order/OrderDispatch) PersonVersion for every receiving_person_id and dispatching_person_id I'm making another requests.
SELECT *
FROM person_versions
WHERE effective_date_from <= ? AND person_id = ?
ORDER BY effective_date_from
LIMIT 1
First '?' is Order/OrderDispatch creation date and second '?' is receiving/ordering person id.
Using this query I'm getting accurate person data for the time of Order/OrderDispatch creation.
It's fairly easy to write query with subquery (or subqueries, as Order comes with OrderDispatches on one list) in raw SQL, but I have no idea how to do that using ActiveRecord.
I tried to write custom has_one relation as this is as far as I've come:
has_one :receiving_person. -> {
where("person_versions.id = (
SELECT id
FROM person_versions sub_pv1
WHERE sub_pv1.date_from <= orders.receive_date
AND sub_pv1.person_id = orders.receiving_person_id
LIMIT 1)")},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
It works if I use this only for receiving or dispatching person. When I try to eager_load this for joined orders and order_dispatches tables then one of 'person_versions' has to be aliased and in my custom where clause it isn't (no way to predict if it's gonna be aliased or not, it's used both ways).
Different aproach would be this:
has_one :receiving_person, -> {
where(:id => PersonVersion.where("
person_versions.date_from <= orders.receive_date
AND person_versions.person_id = orders.receiving_person_id").order(date_from: :desc).limit(1)},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
Raw 'person_versions' in where is OK, because it's in subquery and using symbol ':id' makes raw SQL get correct aliases for person_versions table joined to orders and order_dispatches, but I get 'IN' instead of 'eqauls' for person_versions.id xx subquery and MySQL can't do LIMIT in subqueries which are used with IN/ANY/ALL statements, so I just get random person_version.
So TL;DR I need to transform 'has_many through' to 'has_one' using custom 'where' clause which looks for newest record amongst those which date is lower than originating record creation.
EDIT: Another TL;DR for simplification
def receiving_person
receiving_person_id = self.receiving_person_id
receive_date = self.receive_date
PersonVersion.where(:person_id => receiving_person_id, :hidden => 0).where.has{date_from <= receive_date}.order(date_from: :desc, id: :desc).first
end
I need this method converted to 'has_one' relation so that i could 'eager_load' this.
I would change your schema as it's conflicting with your business domain, restructuring it would alleviate your n+1 problem
class Person < ActiveRecord::Base
has_many :versions, class_name: PersonVersion, dependent: :destroy
has_one :current_version, class_name: PersonVersion
end
class PersonVersion < ActiveRecord::Base
belongs_to :person, inverse_of: :versions,
default_scope ->{
order("person_versions.id desc")
}
end
class Order < ActiveRecord::Base
has_many :order_dispatches, dependent: :destroy
end
class OrderDispatch < ActiveRecord::Base
belongs_to :order
belongs_to :receiving_person_version, class_name: PersonVersion
has_one :receiving_person, through: :receiving_person_version
end
I have a few models...
class Game < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :game
belongs_to :voter, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :games, dependent: :destroy
has_many :votes, dependent: :destroy
end
In my controller, I have the following code...
user = User.find(params[:userId])
games = Game.includes(:manager, :votes)
I would like to add an attribute/method voted_on_by_user to game that takes a user_id parameter and returns true/false. I'm relatively new to Rails and Ruby in general so I haven't been able to come up with a clean way of accomplishing this. Ideally I'd like to avoid the N+1 queries problem of just adding something like this on my Game model...
def voted_on_by_user(user)
votes.where(voter: user).exists?
end
but I'm not savvy enough with Ruby/Rails to figure out a way to do it with just one database roundtrip. Any suggestions?
Some things I've tried/researched
Specifying conditions on Eager Loaded Associations
I'm not sure how to specify this or give the includes a different name like voted_on_by_user. This doesn't give me what I want...
Game.includes(:manager, :votes).includes(:votes).where(votes: {voter: user})
Getting clever with joins. So maybe something like...
Game.includes(:manager, :votes).joins("as voted_on_by_user LEFT OUTER JOIN votes ON votes.voter_id = #{userId}")
Since you are already includeing votes, you can just count votes using non-db operations: game.votes.select{|vote| vote.user_id == user_id}.present? does not perform any additional queries if votes is preloaded.
If you necessarily want to put the field in the query, you might try to do a LEFT JOIN and a GROUP BY in a very similar vein to your second idea (though you omitted game_id from the joins):
Game.includes(:manager, :votes).joins("LEFT OUTER JOIN votes ON votes.voter_id = #{userId} AND votes.game_id = games.id").group("games.id").select("games.*, count(votes.id) > 0 as voted_on_by_user")
There are these models:
Patient
Patient has_many MedicalOrders
MedicalOrder
MedicalOrder belongs_to Patient
MedicalOrder has_many Tasks
Task
Task belongs_to MedicalOrder
Task has_many ControlMedicines
ControlMedicine
ControlMedicine belongs_to Task
And there's this block of code to get the actual #patient's control_medicines:
def index
#control_medicines = []
#patient.medical_orders.each do |mo|
mo.tasks.order(created_at: :desc).each do |t|
t.control_medicines.each do |cm|
#control_medicines << cm
end
end
end
end
I know it's not the best way to query associated models but haven't figured out how to do it using .includes() method. Mostly because .includes() only works being called to a Class (eg, Patient.includes()) and they're not suitable for nested models, like in this situation.
I've read about preloading, eager_loading and includes but all the examples are limited to get data from two associated models.
You can use the has_many through in Rails to allow ActiveRecord to make your joins for you.
class Patient < ActiveRecord::Base
has_many :medical_orders
has_many :tasks, through: :medical_orders
has_many :control_medicines, through: :tasks
end
Writing your query like:
#patient.control_medicines
Generates SQL like:
SELECT "control_medicines".* FROM "control_medicines"
INNER JOIN "tasks" ON "tasks"."id" = "control_medicines"."task_id"
INNER JOIN "medical_orders" ON "medical_orders"."id" = "tasks"."medical_order_id"
WHERE "medical_orders.patient_id" = $1 [["id", 12345]]