has_many through association before persistence - ruby-on-rails

Before a :through relationship has been persisted, is it still possible to return the association?
Here is a simplified version of the class structure:
class Foo < ActiveRecord::Base
has_many :quxes
has_many :bars, through: :quxes
end
class Bar < ActiveRecord::Base
# there is no has_many :quxes -- this class doesn't care
end
class Qux < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
So, I want to make calls like foo.bars which is equivalent to foo.quxes.map(&:bars)
I'm cloning Foos, but not saving them. Quxs are copied from old_foo to new_foo by
new_foo.quxes << old_foo.quxes.map(&:dup)
Please note that the above results in:
new_foo.quxes.first.foo == new_foo
new_foo.quxes.first.foo_id == old_foo.id
which shows that the association exists, but is not yet persisted.
It seems to me you should now be able to do:
new_foo.bars # same as new_foo.quxes.map(&:bar)
But it actually returns []
Is it possible for this association new_foo.bars to work before new_foo and its new quxes are saved? Is this even expected/desirable behavior for :through?

The through relation still "works" in the sense that you can manipulate it normally. I think what you mean is that it doesn't contain any bars added to any of the quxes. This is because the relation bars is separate from the relation quxes even if its not independent. To put it another way, bars is not simply quxes.map(&:bar), as you say; it runs a totally separate query, something like:
> puts foo.bars.to_sql
SELECT "bars".* FROM "bars" INNER JOIN "quxes" ON "bars"."id" = "quxes"."bar_id" WHERE "quxes"."foo_id" = 1
> puts new_foo.quxes.to_sql
SELECT "quxes".* FROM "quxes" WHERE "quxes"."foo_id" = 1
This means that unless an associated bar is persisted, the bars relation's SQL won't pick it up:
> persisted_foo.quxes.first.build_bar
#<Bar id: nil>
> persisted_foo.bars
#<ActiveRecord::Associations::CollectionProxy []>
> persisted_foo.quxes.first.save!
> persisted_foo.reload
> persisted_foo.bars
#<ActiveRecord::Associations::CollectionProxy [#<Bar id: 1>]>

Related

Is overriding an ActiveRecord relation's count() method okay?

Let's say I have the following relationship in my Rails app:
class Parent < ActiveRecord::Base
has_many :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
end
I want parents to be able to see a list of their chatty kids, and use the count in paginating through that list. Here's a way to do that (I know it's a little odd, bear with me):
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
proxy_association.owner.kids.where(:chatty => true)
end
end
end
But! Some parents have millions of kids, and p.kids.for_chatting.count takes too long to run, even with good database indexes. I'm pretty sure this cannot be directly fixed. But! I can set up a Parent#chatty_kids_count attribute and keep it correctly updated with database triggers. Then, I can:
class Parent < ActiveRecord::Base
has_many :kids do
def for_chatting
parent = proxy_association.owner
kid_assoc = parent.kids.where(:chatty => true)
def kid_assoc.count
parent.chatty_kids_count
end
end
end
end
And then parent.kids.for_chatting.count uses the cached count and my pagination is fast.
But! Overriding count() on singleton association objects makes the uh-oh, I am being way too clever part of my brain light up big-time.
I feel like there's a clearer way to approach this. Is there? Or is this a "yeah it's weird, leave a comment about why you're doing this and it'll be fine" kind of situation?
Edit:
I checked the code of will_paginate, seems like it is not using count method of AR relation, but i found that you can provide option total_entries for paginate
#kids = #parent.kids.for_chatting.paginate(
page: params[:page],
total_entries: parent.chatty_kids_count
)
This is not working
You can use wrapper for collection like here
https://github.com/kaminari/kaminari/pull/818#issuecomment-252788488​,
just override count method.
class RelationWrapper < SimpleDelegator
def initialize(relation, total_count)
super(relation)
#total_count = total_count
end
def count
#total_count
end
end
# in a controller:
relation = RelationWrapper.new(#parent.kids.for_chatting, parent.chatty_kids_count)

ActiveRecord Scope on Polymorphic Relationshps

I have three models that have a polymorphic relationship as follows:
class DataSource < ActiveRecord::Base
belongs_to :sourceable, polymorphic: true
end
class Foo < ActiveRecord::Base
has_many :data_sources, as: :sourceable
# For the sake of this example, I have place the scope here.
# But I am going to put it in a Concern since Baz needs this scope as well.
scope :bar_source_id, -> (id) do
joins(:data_sources)
.where(data_sources: { source: 'bar', source_id: id })
.first
end
end
class Baz < ActiveRecord::Base
has_many :data_sources, as: :sourceable
end
I want to be able to find a Foo record based on the source_id of the related DataSource. In otherwords, Foo.bar_source_id(1) should return a Foo record who's data_sources contains a record with source: 'bar', source_id: 1. However the scope on the Foo model does not work as I expected.
Foo.bar_source_id(1) returns the correct Foo record but if there is no DataSource with a source_id of 1 Foo.all is returned.
On the other hand Foo.joins(:data_sources).where(data_sources: { source: 'bar', source_id: 1 }).first will always return either the correct record or nil if no record exists. This is the behaviour I expected.
Why does this query work when I call it off of the model itself, but not when I include it in the model as a scope?
Edit:
I have a partial answer to my question. .first in the scope was causing a second query that loads all Foo records. If I remove .first I will get back an ActiveRecord_Relation.
Why does .first behave differently in these two contexts?
After digging around I found this: ActiveRecord - "first" method in "scope" returns more than one record
I assume the rationale here is that scope isn't meant to return a single record, but a collection of records.
I can get the results I need by adding a method bar_source_id instead of using a scope.
def self.bar_source_id(id)
joins(:data_sources)
.where(data_sources: { source: 'bar', source_id: id })
.first
end

Ruby on Rails autosave associations

I have three associated classes in Rails v. 3.2.15, with Ruby 2.1.1, and a join-table class between two of them:
class Grandarent < ActiveRecord::Base
has_many :parents, autosave: true
end
class Parent
belongs_to :grandparent
has_many :children, :through => :parent_children, autosave: true
end
class ParentChild
belongs_to :parent
belongs_to :child
end
class Child
has_many :parent_children
has_many :parents, :through => :parent_children
end
If I execute the following, then changes to child are not saved:
gp = Grandparent.find(1)
gp.parents.first.children.first.first_name = "Bob"
gp.save
gp.parents.first.children.first.first_name ## -> Whatever name was to begin with (i.e. NOT Bob)
But if I force Rails to evaluate and return data from each connection, then the save is successful
gp = Grandparent.find(1)
gp.parents
gp.parents.first
gp.parents.first.children
gp.parents.first.children.first
gp.parents.first.children.first.first_name = "Bob"
gp.save
gp.parents.first.children.first.first_name ## -> "Bob"
If I subsequently execute gp = Grandparent.find(1) again, then I've reset the whole thing, and have to force the evaluation of associations again.
Is this intentional behavior, or have I done something wrong? Do I need to hang an autosave on the join table connections as well as (or instead of) the has_many :through connection?
From the documentation, I see that "loaded" members will be saved. Is this what is necessary to load them? Can someone define exactly what "loaded" is, and how to achieve that state?
It is happening because gp.parents caches the parents into a results Array, then parents.first is actually calling Array.first. However, gp.parents.first performs a query with LIMIT 1 every time, and so returns a new object every time.
You can confirm like so:
gp.parents.first.object_id # performs new query (LIMIT 1)
=> 1
gp.parents.first.object_id # performs new query (LIMIT 1)
=> 2
gp.parents # performs and caches query for parents
gp.parents.first.object_id # returns first result from parents array
=> 1
gp.parents.first.object_id # returns first result from parents array
=> 1
You can chain an update with your query like so:
gp.parents.first.children.first.update_attributes(first_name: "Bob")

How do I force rails to not use a cached result for has_many through relations?

I have the following three models (massively simplified):
class A < ActiveRecord::Base
has_many :bs
has_many :cs, :through => :bs
end
class B < ActiveRecord::Base
belongs_to :a
has_many :cs
end
class C < ActiveRecord::Base
belongs_to :b
end
It seems that A.cs gets cached the first time it is used (per object) when I'd really rather it not.
Here's a console session that highlights the problem (the fluff has been edited out)
First, the way it should work
rails console
001 > b = B.create
002 > c = C.new
003 > c.b = b
004 > c.save
005 > a = A.create
006 > a.bs << b
007 > a.cs
=> [#<C id: 1, b_id: 1>]
This is indeed as you would expect. The a.cs is going nicely through the a.bs relation.
And now for the caching infuriations
008 > a2 = A.create
009 > a2.cs
=> []
010 > a2.bs << b
011 > a2.cs
=> []
So the first call to a2.cs (resulting in a db query) quite correctly returned no Cs. The second call, however, shows a distinct lack of Cs even though they jolly well should be there (no db queries occurred).
And just to test my sanity is not to blame
012 > A.find(a2.id).cs
=> [#<C id: 1, b_id: 1>]
Again, a db query was performed to get both the A record and the associated C's.
So, back to the question: How do I force rails to not use the cached result? I could of course resign myself to doing this workaround (as shown in console step 12), but since that would result in an extra two queries when only one is necessary, I'd rather not.
I did some more research into this issue. While using clear_association_cache was convenient enough, adding it after every operation that invalidated the cache did not feel DRY. I thought Rails should be able to keep track of this. Thankfully, there is a way!
I will use your example models: A (has many B, has many C through B), B (belongs to A, has many C), and C (belongs to B).
We will need to use the touch: true option for the belongs_to method. This method updates the updated_at attribute on the parent model, but more importantly it also triggers an after_touch callback. This callback allows to us to automatically clear the association cache for any instance of A whenever a related instance of B or C is modified, created, or destroyed.
First modify the belongs_to method calls for B and C, adding touch:true
class B < ActiveRecord::Base
belongs_to :a, touch: true
has_many :cs
end
class C < ActiveRecord::Base
belongs_to :b, touch: true
end
Then add an after_touch callback to A
class A < ActiveRecord::Base
has_many :bs
has_many :cs, through: :bs
after_touch :clear_association_cache
end
Now we can safely hack away, creating all sorts of methods that modify/create/destroy instances of B and C, and the instance of A that they belong to will automatically have its cache up to date without us having to remember to call clear_association_cache all over the place.
Depending on how you use model B, you may want to add an after_touch callback there as well.
Documentation for belongs_to options and ActiveRecord callbacks:
http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to
Hope this helps!
All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example:
customer.orders # retrieves orders from the database
customer.orders.size # uses the cached copy of orders
customer.orders.empty? # uses the cached copy of orders
But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass true to the association call:
customer.orders # retrieves orders from the database
customer.orders.size # uses the cached copy of orders
customer.orders(true).empty? # discards the cached copy of orders
# and goes back to the database
Source http://guides.rubyonrails.org/association_basics.html
(Edit: See Daniel Waltrip's answer, his is far better than mine)
So, after typing all that out and just checking something unrelated, my eyes happened upon section "3.1 Controlling Caching" of Association Basics guide.
I'll be a good boy and share the answer, since I've just spent about eight hours of frustrating fruitless Googling.
But what if you want to reload the cache, because data might have been
changed by some other part of the application? Just pass true to the
association call:
013 > a2.cs(true)
C Load (0.2ms) SELECT "cs".* FROM "cs" INNER JOIN "bs" ON "cs"."b_id" = "bs"."id" WHERE "bs"."a_id" = 2
=> [#<C id: 1, b_id: 1>]
So the moral of the story: RTFM; all of it.
Edit:
So having to put true all over the place is probably not such a good thing as the cache would be bypassed even when it doesn't need to be. The solution proffered in the comments by Daniel Waltrip is much better: use clear_association_cache
013 > a2.clear_association_cache
014 > a2.cs
C Load (0.2ms) SELECT "cs".* FROM "cs" INNER JOIN "bs" ON "cs"."b_id" = "bs"."id" WHERE "bs"."a_id" = 2
=> [#<C id: 1, b_id: 1>]
So now, not only should we RTFM, we should also search the code for :nodoc:s!
I found another way to disable query cache. In your model, just add a default_scope
class B < ActiveRecord::Base
belongs_to :a
has_many :cs
end
class C < ActiveRecord::Base
default_scope { } # can be empty too
belongs_to :b
end
Verified it working locally. I found this by looking at active_record source code in active_record/associations/association.rb:
# Returns true if statement cache should be skipped on the association reader.
def skip_statement_cache?
reflection.scope_chain.any?(&:any?) ||
scope.eager_loading? ||
klass.current_scope ||
klass.default_scopes.any? ||
reflection.source_reflection.active_record.default_scopes.any?
end
To clear the cache, use .reload
author.books # retrieves books from the database
author.books.size # uses the cached copy of books
author.books.empty? # uses the cached copy of books
author.books # retrieves books from the database
author.books.size # uses the cached copy of books
author.books.reload.empty? # discards the cached copy of books
# and goes back to the database
Source: Controlling caching
You can use the extend option and provide a module to reset the loading before it takes place like so:
# Usage:
# ---
#
# has_many :versions,
# ...
# extend: UncachedAssociation
#
module UncachedAssociation
def load_target
#association.reset
super
end
end

ActiveRecord association returns "NoMethodError for ActiveRecord::Relation"

I have 3 models with "1 to n" associations, like this
Client --1 to n--> Category --1 to n--> Item
In one page, I need to display a list of Items along with their Categories. This page is subject to 3 level of filtering:
Client filtering: I know the client id (I'll use 'id=2' in this example)
Category name: dynamic filter set by the user
Item name: dynamic filter set by the user
And I'm getting more and more confused with ActiveRecord Associations stuff
In my ItemsController#index, I tried this:
categories = Client.find(2).categories
.where('name LIKE ?', "%#{params[:filter_categories]}%")
#items = categories.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
The second line raises a NoMethodError undefined method 'items' for ActiveRecord::Relation. I understand that the first line returns a Relation object, but I cannot find a way to continue from here and get the list of Items linked to this list of Categories.
I also started to extract the list of categories ids returned by the first line, in order to use them in the where clause of the second line, but while writing the code I found it inelegant and thought there may be a better way to do it. Any help would be very appreciated. Thanks
models/client.rb
class Client < ActiveRecord::Base
has_many :categories
has_many :items, through: :categories
...
end
models/category.rb
class Category < ActiveRecord::Base
belongs_to :client
has_many :items
...
end
model/item.rb
class Item < ActiveRecord::Base
belongs_to :category
has_one :client, through: :category
...
end
You can only call .items on a category object, not on a collection. This would work:
#items = categories.first.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
To get what you want, you can do the following:
#items = Item
.where('category_id IN (?) AND name LIKE ?', categories, "%#{params[:filter_items]}%")
Assuming that in the end you are only interested in what is in #items, it would be even better to do it in one query instead of two, using joins:
#items = Item.joins(:category)
.where('items.name LIKE ? AND categories.name = ? AND categories.client_id = 2', "%#{params[:filter_items]}%", "%#{params[:filter_categories]}%")
You can try smth like this:
item_ids = Client.find(2).categories.inject([]) { |ids, cat| ids |= cat.item_ids; ids }
items = Item.find(item_ids)
This how you can get a list of nested objects that associated through another table.

Resources