Restrict eagerly loaded with `where` without another query in Rails - ruby-on-rails

A has many Bs, B has many Cs. C has a property called thing:
class A < ActiveRecord::Base
has_many :bs
end
class B < ActiveRecord::Base
belongs_to :a
has_many :cs
end
class C < ActiveRecord::Base
belongs_to :b
attr_accessible :thing
end
I'd like to query for all Bs belonging to an A, and eagerly load Cs that belong to said B:
> a = A.first
A Load (0.2ms) SELECT "as".* FROM "as" LIMIT 1
=> #<A id: 1, created_at: "2012-08-21 09:25:18", updated_at: "2012-08-21 09:25:18">
> bs = a.bs.includes(:cs)
B Load (0.2ms) SELECT "bs".* FROM "bs" WHERE "bs"."a_id" = 1
C Load (0.1ms) SELECT "cs".* FROM "cs" WHERE "cs"."b_id" IN (1)
=> [#<B id: 1, a_id: 1, created_at: "2012-08-21 09:25:22", updated_at: "2012-08-21 09:25:22", thing: nil>]
>
This works well:
> bs[0]
=> #<B id: 1, a_id: 1, created_at: "2012-08-21 09:25:22", updated_at: "2012-08-21 09:25:22", thing: nil>
> bs[0].cs
=> [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">]
>
—but not in the case where I want to later perform where() searches on the Cs that belong to B instances:
> bs[0].cs.where(:thing => 1)
C Load (0.2ms) SELECT "cs".* FROM "cs" WHERE "cs"."b_id" = 1 AND "cs"."thing" = 1
=> []
> bs[0].cs.where(:thing => 2)
C Load (0.2ms) SELECT "cs".* FROM "cs" WHERE "cs"."b_id" = 1 AND "cs"."thing" = 2
=> [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">]
>
Note that queries are re-issued, despite our having the available information.
Of course, I can just use Enumerable#select:
> bs[0].cs.select {|c| c.thing == 2}
=> [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">]
>
This avoids a re-query, but I was sort of hoping Rails could do something similar itself.
The real downside is that I want to use this code where we don't know if the association has been eagerly loaded or not. If it hasn't, then the select method will load all C for B before doing the filter, whereas the where method would produce SQL to get a smaller set of data.
I'm not convinced this matters at all, but if there was something I'm missing about eager loading, I'd love to hear it.

I don't think you're missing anything. I don't believe active record can do anything that smart -- and it would be very difficult to do reliably I think. Like you say, it would have to determine whether you've eager-loaded the association, but it would also have to make a guess as to whether it would be faster to loop through the in-memory collection of Cs (if it's a small collection) or whether it would be faster to go to the database to get all the appropriate Cs in one shot (if it's a very large collection).
In your case, the best thing might be to just set the default scope to always preload the cs, and maybe even write your own fancy method to get them by thing. Something like this maybe:
class B < ActiveRecord::Base
belongs_to :a
has_many :cs
default_scope includes(:cs)
def cs_by_thing(thing)
cs.select{|c|c.thing == thing}
end
end
Then you could always know that you never go back to the DB when querying for your cs:
a = A.first
[db access]
a.bs.first
[db access]
a.bs.first.cs
a.bs.first.cs_by_thing(1)
a.bs.first.cs_by_thing(2)

Related

How to fetch all parent records on a self-join model

Suppose we have the following setup:
class Category < ApplicationRecord
belongs_to :parent, class_name: 'Category', foreign_key: :parent_id
has_many :children, class_name: 'Category', foreign_key: :parent_id
end
In other words: A category can have sub-categories. (And if the category has no parent, then it's a "top-level" category.)
What I'd like is: Given a collection of categories, merge this with their parent categories.
If it makes things easier, for now it's safe to assume that each category will be at most "one level deep", i.e. there are no "grand-child" categories. (But this might change one day, so ideally I'd like the code to be robust against that!)
My use case is that I'd like to be able to do something like this:
post.categories = categories.with_parents
..Since that will make all of the existing parent-specific category logic simpler to handle going forwards.
Naturally I could handle this by doing something like:
post.categories = (categories + categories.map(&:parent)).compact.uniq
...but I'm hoping there's a way to achieve this more elegantly and efficiently.
Here's the best way I've found so far, but it feels... wrong 😬 -- surely there a way of doing this with a join/union/merge??
class Category < ApplicationRecord
# ...
def self.with_parents
self.or(unscoped.where(id: select(:parent_id)))
end
end
If you just want to assign category and parent to post, this is as simple as I could make it:
def self.with_parent_ids
pluck(:id, :parent_id).flatten.compact.uniq
end
>> categories = Category.limit(2).reverse_order.with_parent_ids
Category Pluck (0.8ms) SELECT "categories"."id", "categories"."parent_id" FROM "categories" ORDER BY "categories"."id" DESC LIMIT $1 [["LIMIT", 2]]
=> [6, 3, 5, 2]
>> Post.first.category_ids = categories
I don't know if this is more elegant, but if parent_id is the only thing you have and you want to get all the parents up the tree:
>> Category.find_by_sql <<~SQL
WITH RECURSIVE tree AS (
( #{Category.where(id: 5).to_sql} )
UNION
( SELECT "categories".* FROM "categories" JOIN tree ON categories.id = tree.parent_id )
) SELECT * FROM tree
SQL
=>
[#<Category:0x00007f7fe354aa78 id: 5, name: "Audiobooks", parent_id: 2>, # started here
#<Category:0x00007f7fe354a9b0 id: 2, name: "Books", parent_id: 1>, # parent
#<Category:0x00007f7fe354a8e8 id: 1, name: "Products", parent_id: nil>] # grand parent
# etc

Finding matches from an array rails

Patient have an array of clinician id's that they are shared with stored in shared_with. I would like to get a list of the patients where the current user, a clinician, has their id stored in the patient's shared_with
What I have tried to do now doesn't work:
#shared = Patient.find_by(shared_with: current_user.clinician.id).order("first_name asc")
For example, our current_user is associated with clinician.id 1 and there are patients with shared_with values of 1, 4 for patient 10 and 1, 7 for patient 15. I want #shared to be a list with just patient 10 and 15.
Patient model:
Patient:
clinician_id: integer
first_name: string
last_name: string
user_id: integer
shared_with: string
serialize :shared_with, Array
Patient.rb:
class Patient < ActiveRecord::Base
belongs_to :clinician
belongs_to :user
accepts_nested_attributes_for :user, :allow_destroy => true
end
As far as I can tell, the Patient model doesn't need a belongs_to for clinicians, and doesn't need a clinician_id -- unless these are related in another fashion...in which case, carry on.
Assuming your database supports an array field (such as postgres) then you're very close. You just need to wrap it in braces and since it's now in quotes, you'll need a #{} set for interpolation. Like so:
Patient.where(shared_with: "{#{current_user.clinician.id}}").order("first_name asc")
Doing a test with mock modeling you provided I see this in the console:
2.1.1 :005 > current_user = User.first
User Load (0.8ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
=> #<User id: 1, name: "benji", created_at: "2015-08-30 02:35:17", updated_at: "2015-08-30 02:35:17">
2.1.1 :006 > Patient.where(shared_with: "{#{current_user.clinician.id}}").order("first_name asc")
Clinician Load (59.0ms) SELECT "clinicians".* FROM "clinicians" WHERE "clinicians"."user_id" = $1 ORDER BY "clinicians"."id" ASC LIMIT 1 [["user_id", 1]]
Patient Load (0.7ms) SELECT "patients".* FROM "patients" WHERE "patients"."shared_with" = '{1}' ORDER BY first_name asc
=> #<ActiveRecord::Relation [#<Patient id: 2, clinician_id: nil, first_name: "tom", last_name: "jerry", user_id: nil, created_at: "2015-08-30 19:12:59", updated_at: "2015-08-30 19:26:37", shared_with: ["1"]>]>

How does .first work on a CollectionProxy in Rails?

I have a couple models set up like this:
class Contract < ActiveRecord::Base
has_many :invoices, dependent: :destroy
end
class Invoice < ActiveRecord::Base
belongs_to :contract
end
I have a feature test set up like this...
feature "Some cool functionality", js: true do
let(:contract) { create(:contract) }
let(:invoice) { create(:invoice, contract: contract) }
#etc...
end
While debugging the test I noticed this...
(byebug) p contract
#<Contract id: 1, created_at: "2014-02-25 01:52:52", updated_at: "2014-02-25 01:52:52">
(byebug) p invoice
#<Invoice id: 1, contract_id: 1, created_at: "2014-02-25 01:52:52", updated_at: "2014-02-25 01:52:52">
Here's the confusing part:
(byebug) p contract.invoices.first
nil
I thought that would return my invoice defined in my feature test.
However, I think I can verify that contract has one invoice...
(byebug) p contract.invoices.count
(1.0ms) SELECT COUNT(*) FROM "invoices" WHERE "invoices"."contract_id" = $1 [["contract_id", 1]]
1
What's going on here?
Try calling contract.reload
When you call let the value of the variable/method is cached after the first time it is invoked. So when you call contract.invoices.first you are invoking invoices on the cached contract object currently in memory.
To ensure you're using the most up-to-date data, try using Rails' reload! console method:
# from command line
reload!

Rails Activerecord multiple table includes

I have four tables:
argument with fields
id
comments with
id
comment_id
argument_id
user_id
users
id
nicknames with
id
proposal_id
user_id
name
each argument has many comments,
each comment belongs to a user,
each user has a specific nickname in the argument.
When I fetch the argument comments from DB, I would like to include also the nicknames of each author.
The answer is about the ActiveRecord query I don't know how to write.
I tried with
#argument.comments.includes(:user => :nicknames)
but it doesn't seems to work and when I get the nickname through
nickname = #argument.nicknames.find_by_user_id(comment.user.id)
it executes the query...
[1m[36mNickname Load (0.6ms)[0m [1mSELECT "nicknames".* FROM "nicknames" WHERE "nicknames"."argument_id" = 59 AND "nicknames"."user_id" = 9 LIMIT 1[0m
any suggestion?
You can tell if an association is loaded with loaded?.
What is happening here, if I understand your problem, is that you are trying to run a finder on an ActiveRecord::Relation. Quickly browsing through the code, it does not appear that it will try to see if a collection is loaded before it issues the query. It does, however, take a block that will avoid multiple queries. For example (the model names have been changed because I am using a sample project I created for another question):
c = Canteen.first
Canteen Load (0.2ms) SELECT "canteens".* FROM "canteens" LIMIT 1
=> #<Canteen id: 1, name: "Really good place", created_at: "2012-12-13 00:04:11", updated_at: "2012-12-13 00:04:11">
c.meals.loaded?
=> false
c.meals.find {|m| m.id == 3}
Meal Load (0.2ms) SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" = 1
=> #<Meal id: 3, canteen_id: 1, name: "Banana Pie", price: #<BigDecimal:7fcb6784fa78,'0.499E1',18(45)>, created_at: "2012-12-13 00:37:41", updated_at: "2012-12-13 00:37:41">
You see in the last example that ActiveRecord issues the query to load the associated records. This is because ActiveRecord is calling to_a on the association, forcing the entire set to be loaded, and then filtering based on the block conditions. Obviously, this is not ideal.
Let's try again, eager loading the association.
c = Canteen.includes(:meals).first
Canteen Load (0.2ms) SELECT "canteens".* FROM "canteens" LIMIT 1
Meal Load (0.2ms) SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" IN (1)
=> #<Canteen id: 1, name: "Really good place", created_at: "2012-12-13 00:04:11", updated_at: "2012-12-13 00:04:11">
c.meals.loaded?
=> true
c.meals.find {|m| m.id == 3}
=> #<Meal id: 3, canteen_id: 1, name: "Banana Pie", price: #<BigDecimal:7fcb68b596f0,'0.499E1',18(45)>, created_at: "2012-12-13 00:37:41", updated_at: "2012-12-13 00:37:41">
In the last example here, you see that the collection is not loaded again. Instead, the block is used to filter the already loaded records.
As you can see below, even if the records are loaded, ActiveRecord will issue a query to grab the associated record:
c.meals.loaded?
=> true
c.meals.find(1)
Meal Load (0.1ms) SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" = 1 AND "meals"."id" = ? LIMIT 1 [["id", 1]]
=> #<Meal id: 1, canteen_id: 1, name: "Enchiladas", price: #<BigDecimal:7fcb6584ce88,'0.699E1',18(45)>, created_at: "2012-12-13 00:04:40", updated_at: "2012-12-13 00:04:40">
SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" = 1 AND "meals"."id" = 3
=> [#<Meal id: 3, canteen_id: 1, name: "Banana Pie", price: #<BigDecimal:7fcb68b808e0,'0.499E1',18(45)>, created_at: "2012-12-13 00:37:41", updated_at: "2012-12-13 00:37:41">]
Maybe something like :
#argument.includes(:comments => [{ :user => :nicknames }])
Didn't try it though...
You can try something like this to include more than one table
User.find(:all, :include => Room.find(:all,:include => :review))

Rails associations - problems with altering values, and too much caching!

Suppose I've got a card-game app, which features a Player model, which has an actions integer column; and a Card model. A player can play a card they own, which costs an action; one particular card grants two actions when it's played.
If I code this as follows:
class Player < ActiveRecord::Base
has_many :cards
def play_card(card)
raise "Not yours!" unless cards.include? card
self.actions -= 1
card.play
save!
end
end
class Card < ActiveRecord::Base
belongs_to :player
def play
player.actions += 2
end
end
... then the net effect of Player#play_card is to decrement actions by 1. The only way I've found to make both changes apply to the same object, thereby resulting in a net increment of 1 action, is to define the functions like this:
class Player < ActiveRecord::Base
has_many :cards
def play_card(card)
raise "Not yours!" unless cards.include? card
self.actions -= 1
// Stick that change in the Database
save!
card.play
end
end
class Card < ActiveRecord::Base
belongs_to :player
def play
// Force reload of the player object
player(true).actions += 2
// And save again
player.save!
end
end
But that turns a single database write into two writes and a read! Surely there must be a better way. What am I missing?
In the first version of your code you are loading the same row of the table players but while you are expecting rails to be smart enough to recognize that it has already load this row in memory, rails doesn't work that way. So when you are issuing a +=2 on player it does he +=2 on another instance than the one on which you have done -=1.
i've setup a little example to show that there are too instance of the same row:
ruby-1.8.7-p174 > p_instance_1 = Player.first
=> #<Player id: 1, actions: -1, created_at: "2010-10-13 17:07:22", updated_at: "2010-10-13 17:11:00">
ruby-1.8.7-p174 > c = Card.first
=> #<Card id: 1, player_id: 1, created_at: "2010-10-13 17:07:28", updated_at: "2010-10-13 17:07:28">
ruby-1.8.7-p174 > p_instance_2 = c.player
=> #<Player id: 1, actions: -1, created_at: "2010-10-13 17:07:22", updated_at: "2010-10-13 17:11:00">
ruby-1.8.7-p174 > p_instance_1.object_id
=> 2158703080
ruby-1.8.7-p174 > p_instance_2.object_id
=> 2156926840
ruby-1.8.7-p174 > p_instance_1.actions += 1
=> 0
ruby-1.8.7-p174 > p_instance_2.actions += 1
=> 0
So finally as you haven't save the instance with the +=2 applied, there's only the one with the -1 that is saved
UPDATE
You can try to trick rails to use the same instance of player all the way. This is a little bit ugly but it works.
class Player < ActiveRecord::Base
has_many :cards
def play_card(card)
raise "Not yours!" unless cards.include? card
new_self = card.player
card.play
new_self.actions -= 1
new_self.save!
end
end
class Card < ActiveRecord::Base
belongs_to :player
def play
player.actions += 2
end
end
so when you input those commands:
ruby-1.8.7-p174 > p = Player.first
=> #<Player id: 1, actions: 0, created_at: "2010-10-14 13:33:51", updated_at: "2010-10-14 13:33:51">
ruby-1.8.7-p174 > p.play_card(Card.first)
=> true
ruby-1.8.7-p174 > p
=> #<Player id: 1, actions: 0, created_at: "2010-10-14 13:33:51", updated_at: "2010-10-14 13:33:51">
ruby-1.8.7-p174 > p.reload
=> #<Player id: 1, actions: 1, created_at: "2010-10-14 13:33:51", updated_at: "2010-10-14 13:34:40">
You have the right number of actions in player, and in the logs card is only loaded once:
Player Load (0.5ms) SELECT * FROM "players" LIMIT 1
Card Load (0.2ms) SELECT * FROM "cards" LIMIT 1
Card Load (0.2ms) SELECT "cards".id FROM "cards" WHERE ("cards"."id" = 1) AND ("cards".player_id = 1) LIMIT 1
Player Load (0.1ms) SELECT * FROM "players" WHERE ("players"."id" = 1)
Player Update (0.6ms) UPDATE "players" SET "updated_at" = '2010-10-14 13:34:40', "actions" = 1 WHERE "id" = 1
To sum up the whole thing, I would say that there's something wrong in your code design. If i understand well,what you would like is that every AR instance of a table row is the same object in the ObjectSpace, but I guess that if rails was build that way it would introduce strange behaviors where you could work on half backed object changed in validations and other hooks.

Resources