build methods for has_one though has_one - ruby-on-rails

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.

Related

ActiveRecord grab shared model from polymorphic association

I'm looking for a better way to query Users from 2 different Models used in a polymorphic association. Here is the setup
class Schedule < ApplicationRecord
belongs_to :announcement
has_many :targets, dependent: :destroy
has_many :lists, through: :targets, source: :target, source_type: 'List'
has_many :accounts, through: :targets, source: :target, source_type: 'Account'
end
class Target < ApplicationRecord
# belongs_to :announcement
belongs_to :schedule
belongs_to :target, polymorphic: true
delegate :announcement, to: :schedule
end
class List < ApplicationRecord
belongs_to :account
has_many :targets, as: :target, dependent: :destroy
has_many :lists_users
has_many :users, through: :lists_users
end
class Account < ApplicationRecord
has_many :announcements, dependent: :destroy
has_many :targets, as: :target, dependent: :destroy
has_many :users, dependent: :destroy
end
At the moment I'm solving this by creating a method inside the Schedule model that grabs Users this way:
def subscribers
targets.map(&:target).map(&:users).flatten.uniq
end
I looked at something similar with this question, but didn't seem to solve it.
I would do that like this:
class Schedule < ApplicationRecord
def subscribers
# fetch all associated user IDs
lists_user_ids = lists.joins(:lists_users).distinct.pluck("lists_users.user_id")
accounts_user_ids = accounts.joins(:users).distinct.pluck("users.id")
user_ids = (lists_user_ids + accounts_user_ids).uniq
# fetch users by IDs
User.where(id: user_ids)
end
end

Multiple associations in the has_many "through" model

I have a Products & Parts model which would each have multiple uploads, which are also polymorphic. Is it possible for me to have a single ItemUpload model to handle the association between the Products/Parts and Uploads, or do they need to be separate? I'd try myself just to see, but don't want to cause any potential headaches down the line! Note that I'm aware I need to do the source: and source_type: stuff to clean up the polymorphic association with has_many, but would like to clarify this point first before proceeding. Current models:
class Product < ApplicationRecord
has_many :uploads, as: :uploadable, dependent: :destroy
end
class Part < ApplicationRecord
has_many :uploads, as: :uploadable, dependent: :destroy
end
class Upload < ApplicationRecord
belongs_to :uploadable, polymorphic: true
end
What I would ideally like:
Class ItemUpload < ApplicationRecord
belongs_to :product, optional: true
belongs_to :part, optional: true
belongs_to :upload
end
Is that ok or would I need a separate ProductUpload and PartUpload model?
I would have thought your associations would look more like:
class Product < ApplicationRecord
has_many :item_uploads, as: :itemable, dependent: :destroy
has_many :uploads, through: :item_uploads
end
class Part < ApplicationRecord
has_many :item_uploads, as: :itemable, dependent: :destroy
has_many :uploads, through: :item_uploads
end
class Upload < ApplicationRecord
has_many :item_uploads
has_many :products, through: :item_uploads, source: :itemable, source_type: 'Product'
has_many :parts, through: :item_uploads, source: :itemable, source_type: 'Part'
end
Class ItemUpload < ApplicationRecord
belongs_to :itemable, polymorphic: true
belongs_to :upload
end
That should allow you to do:
product.uploads
part.uploads
upload.products
upload.parts
BTW, in reference to the link you provided:
Upload ≈ User
ItemUpload ≈ Membership
Product, Part ≈ Project, Group
The above follows the pattern in the linked article.

Rails associations confusing

I have 4 models, which are Company, Candidate, Job and Application
For Company
has_many :candidates
has_many :jobs
For Candidate
belongs_to :company
has_one :application
For Jobs
belongs_to :company
has_many :applications
For Application
belongs_to :candidate
belongs_to :job
I'm not sure whether the relationships between Candidate, Jobs and Application are correct or not. It would be great if someone can give some suggestions for improvement. Thank you.
You're on the right track. Adding indirect assocations as well will let you query up and down the heirarchy:
class Company < ApplicationRecord
has_many :jobs
has_many :applications, through: :jobs
has_many :candidates, through: :applications
end
class Job < ApplicationRecord
belongs_to :company
has_many :applications
has_many :candidates, through: :applications
end
class Application < ApplicationRecord
belongs_to :candidate
belongs_to :job
has_one :company, through: :job
end
class Candidate < ApplicationRecord
has_many :applications
has_many :jobs, through: :applications
has_many :companies, through: :jobs
end
I think the easiest way to build activerecord associations is to imagine your associations in real life. In this case, a company has several jobs, each job has several applications, and each application has one candidate.
Hence the relation would be
for Company
has_many :jobs
for Job
belongs_to :company
has_many :applications
for Application
belongs_to :job
has_one :candidate
for Candidate
belongs_to :application

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

has_many (different objects) as: one type of object

I need a way of referrencing 2 different objects as 1.
I have a Message object with needs to keep track of Recipients. the problem is that Recipients could be a User or a Contact.
should the models be: ?
class Message < ActiveRecord::Base
has_many :users, as: :recipients
has_many :contacts, as: :recipients
end
class User < ActiveRecord::Base
belongs_to :recipient, polymorphic: true
end
class User < ActiveRecord::Base
belongs_to :recipient, polymorphic: true
end
because, I feel like polymorphic relationships are built to go the opposite way.
also, this way doesn't allow me to reference #message.recipients which is what I need.
I hope this makes sense
Thank you
What you have done is completely incorrect. I think you need many-to-many association. My association whould be that:
class Message < ActiveRecord::Base
has_many :recipient_links
has_many :users, through: :recipient_links, source: :recipient, source_type: 'User'
has_many :contacts, through: :recipient_links, source: :recipient, source_type: 'Contact'
end
class Contact < ActiveRecord::Base
has_many :recipient_links, as: :recipient
has_many :messages, through: :recipient_links
end
class User < ActiveRecord::Base
has_many :recipient_links, as: :recipient
has_many :messages, through: :recipient_links
end
# fields: message_id, recipient_id, recipient_type
class RecipientLink < ActiveRecord::Base
belongs_to :recipient, polymorphic: true
belongs_to :message
end
... I can't add comments yet so i give here solution for: When use answer from up to receive all #message.recipients every type in only 2 request:
RecipientLink.includes(:recipient).where(message_id: #message.id).collect(&:recipient)

Resources