Using inverse_of on a has_many :through? - ruby-on-rails

I'll start off with my test, to show the desired functionality:
item = LineItem.new
order = Order.new
OrderLineItem.new(order: order, line_item: item)
related_order = item.orders.first
expect(related_order).to eq order
which evaluates to:
expected: #<Order ....>
got: nil
The 3 models above are related as follows:
class LineItem < ActiveRecord::Base
has_many :order_line_items, as: :line_item, inverse_of: :line_item
has_many :orders,
through: :order_line_items,
source: :order,
inverse_of: :line_items
end
class OrderLineItem < ActiveRecord::Base
belongs_to :order, inverse_of: :order_line_items
belongs_to :line_item,
polymorphic: true, inverse_of: :order_line_items
end
class Order < ActiveRecord::Base
has_many :line_items,
through: :order_line_items,
source: :line_item,
inverse_of: :orders
has_many :order_line_items, dependent: :destroy, inverse_of: :order
end
I can make this all work by saving everything and calling item.reload before my expect line. But isn't inverse_of supposed to hook all this up?
Maybe I'm expecting too much of inverse_of with pre-saved objects?

No, inverse_of doesn't work with has_many :through
See the "Bidirectional associations" section of the ActiveRecords associations docs: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods.
There are limitations to :inverse_of support:
does not work with :through associations.
does not work with :polymorphic associations.
for belongs_to associations has_many inverse associations are ignored.
See also: ActiveRecord :inverse_of does not work on has_many :through on the join model on create

Related

build methods for has_one though has_one

Rails 5.1.2
Ruby 2.5.3
I understand there are multiple ways to impliment this relationship, however, this question is more about why the following doesn't work rather than solving a real world problem.
has_many setup
class Subscriber < ApplicationRecord
has_many :subscriptions, inverse_of: :subscriber
has_many :promotions, through: :subscriptions, inverse_of: :subscriptions
accepts_nested_attributes_for :subscriptions
accepts_nested_attributes_for :promotions
end
class Subscription < ApplicationRecord
belongs_to :subscriber, inverse_of: :subscriptions
belongs_to :promotion, inverse_of: :subscriptions
end
class Promotion < ApplicationRecord
has_many :subscriptions, inverse_of: :promotion
has_many :subscribers, through: :subscriptions, inverse_of: :subscriptions
accepts_nested_attributes_for :subscriptions
accepts_nested_attributes_for :subscribers
end
In the above Subscriber model which is setup to use has_many relationships following would work:
s = Subscriber.new
s.subscriptions.build
# OR
s.promotions.build
Following that, I would expect Subscriber to behave the same way with has_one relationships
has_one setup
class Subscriber < ApplicationRecord
has_one :subscription, inverse_of: :subscriber
has_one :promotion, through: :subscription, inverse_of: :subscriptions
accepts_nested_attributes_for :subscription
accepts_nested_attributes_for :promotion
end
class Subscription < ApplicationRecord
belongs_to :subscriber, inverse_of: :subscription
belongs_to :promotion, inverse_of: :subscriptions
end
class Promotion < ApplicationRecord
has_many :subscriptions, inverse_of: :promotion
has_many :subscribers, through: :subscriptions, inverse_of: :subscription
accepts_nested_attributes_for :subscriptions
accepts_nested_attributes_for :subscribers
end
However, attempting to build the nested promotion association with the equivalent has_one build methods results in a NoMethodError (undefined method 'build_promotion' for #<Subscriber:0x00007f9042cbd7c8>) error
s = Subscriber.new
s.build_promotion
However, this does work:
s = Subscriber.new
s.build_subscription
I feel it's logical that one should expect to build nested has_one relationships in the same way one builds has_many.
Is this a bug or by design?
Checking the code, when you call has_one, it creates the build_, create_ and create_..! methods ONLY if the reflection is "constructable"
https://github.com/rails/rails/blob/b2eb1d1c55a59fee1e6c4cba7030d8ceb524267c/activerecord/lib/active_record/associations/builder/singular_association.rb#L16
define_constructors(mixin, name) if reflection.constructable?
Now, checking the constructable? method, it returns the result of calculate_constructable https://github.com/rails/rails/blob/ed1eda271c7ac82ecb7bd94b6fa1b0093e648a3e/activerecord/lib/active_record/reflection.rb#L452
And for the HasOne class, it returns false if you use the :through option https://github.com/rails/rails/blob/ed1eda271c7ac82ecb7bd94b6fa1b0093e648a3e/activerecord/lib/active_record/reflection.rb#L723
def calculate_constructable(macro, options)
!options[:through]
end
So, I'd say it's not a bug, it's made like that by design. I don't know the reason though, maybe it feels logical but I guess there's some things to consider that are not that simple.

ActiveRecord Joins result

Doing an ActiveRecord join in RoR seems to work if I look at the generated SQL.
But what I can't figure out is why the result of that SQL isn't returned into the variable.
What I'm doing is:
class Book < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :readers, :through => :readings
accepts_nested_attributes_for :readings
end
class Reader < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :books, :through => :readings
accepts_nested_attributes_for :books
end
class Reading < ActiveRecord::Base
belongs_to :reader
belongs_to :book
end
Now, when asking:
result = Reading.where(:reader_id => rid, ).joins(:book).select(columns.collect{|c| c[:name]}.join(',')).flatten
It shows the correct generated SQL:
SELECT readings.id,books.title,books.author,readings.when FROM `readings` INNER JOIN `books` ON `books`.`id` = `readings`.`book_id` WHERE `readings`.`reader_id` = 2
BUT: the result variable only contains the values of the Reading record, NOT the fields of the joined table.
What am I missing?
I have made the association changes in question also:-
class Book < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :readers, :through => :readings
accepts_nested_attributes_for :readings
end
class Reader < ActiveRecord::Base
has_many :readings, dependent: :destroy
has_many :books, :through => :readings
accepts_nested_attributes_for :books
end
class Reading < ActiveRecord::Base
belongs_to :reader
belongs_to :book
end
Query in this way:-
reader = Reader.find(rid)
result = reader.books.pluck(:name).join(',')
Ultimately, I've rewritten my helper class and fetched the various fields as I needed them. (as krishnar suggested)
Anyways: Thanx you guys for your contributions.

Is it good practice to validate IDs in a join model?

I have a HMT association setup between my Artist and Group models:
class Artist < ApplicationRecord
has_many :artist_groups, dependent: :destroy
has_many :artist_groups, through: :artist_groups
end
class ArtistGroup < ApplicationRecord
has_many :memberships, class_name: "ArtistGroupMembership", dependent: :destroy
belongs_to :artist
belongs_to :group
has_and_belongs_to_many :roles
accepts_nested_attributes_for :memberships, reject_if: :all_blank, allow_destroy: true
validates_presence_of :artist_id, :group_id
end
class Group < ApplicationRecord
has_many :artist_groups, dependent: :destroy
has_many :members, through: :artist_groups, source: :artist
end
As you'll notice in my ArtistGroup join model it validates to make sure the an artist and group are present.
When the association is saved, whether I do something like this:
artist.groups.push(Group.first)
or create a form in my view (sans ID inputs) ActiveRecord is smart enough to map the association. With this in my should I even be validating these IDs in my join models? I notice this becomes even more of a pain when dealing with polymorphic associations.
Rails 5 automatically requires that the belongs_to :artist refers to an existing artist so having extra validation is completely unnecessary. You can make that requirement optional by doing
belongs_to :artist, optional: true

Trying to 'alias' a polymorphic has_many relationship

I have a User model:
class User < ActiveRecord::Base
has_many :tracks, dependent: :destroy
has_many :tracked_locations, through: :tracks, source: :tracking, source_type: 'Location'
and a Track model (think of it as 'following'):
class Track < ActiveRecord::Base
belongs_to :user
belongs_to :tracking, polymorphic: true
end
The idea here is I will have many models to track / follow so I am using polymorphism. For example I have a Location model:
class Location < ActiveRecord::Base
has_many :tracks, :as => :tracking, :dependent => :destroy
has_many :users, through: :tracks
Now in the console Location.first.users works fine along with User.first.tracked_locations.
Now I will be adding another polymorphic relationship along the lines of Flagged. The user can 'flag' another model with a note etc. So if I add has_many :users, through: :flagged to the Location model for example I need to differentiate between tracking users and flagged users.
I tried:
has_many :tracking_users, through: :tracks, source: :tracking, source_type: 'User'
but I get:
NoMethodError: undefined method `evaluators_for' for #<Location:0x007ff29e5409c8>
Can I even do this or am I missing something simple here?
UPDATE
Based on the answer below I figured it out:
has_many :tracking_users, through: :tracks, class_name: "User", foreign_key: "user_id", source: :user
I'm not 100% on this, but you could try:
has_many :tracking_users, through: :tracks, class_name: "User", foreign_key: "user_id", source: :user
Or you could also just create a class method and do it by hand.
def self.tracking_users
user_ids = tracks.collect(&:user_id)
User.where(id: user_ids)
end
edit: Had a brainfart, changed the "source" up there to :user. That tells what table to actually do the lookup in with the other attribute you've provided. of course it wouldn't be in :tracks

Cannot have a has_one :through association ' where the :through association is a collection

I have the following associations. PropertyOwner is a join model which belongs to a property and polymorphically belongs to an owner, which in the below example is a ForeclosureDefense. Everything works well, until I had the has_one :main_property. The idea is the ForeclosureDefense model can have many properties, but the last property is the main property:
class ForeclosureDefense < ActiveRecord::Base
has_many :property_owners, as: :owner
has_many :properties, through: :property_owners
has_one :main_property, through: :property_owners, source: :property, order: 'created_at desc'
end
class PropertyOwner < ActiveRecord::Base
belongs_to :property
belongs_to :owner, polymorphic: :true
end
class Property < ActiveRecord::Base
has_many :property_owners
has_many :owners, through: :property_owners
has_many :foreclosure_owners, through: :property_owners, source: :owner, source_type: "ForeclosureDefense"
has_many :folder_owners, through: :property_owners, source: :owner, source_type: "Folder"
end
Unfortunately, when I try to use that has_one :main_property association, I get the following error:
ActiveRecord::HasOneThroughCantAssociateThroughCollection: Cannot have a has_one :through association 'ForeclosureDefense#main_property' where the :through association 'ForeclosureDefense#property_owners' is a collection.
What am I doing wrong?
My solution was just to add it as a class-level macro:
def main_property
properties.order('created_at desc').first
end

Resources