Eric Norcross
Eric Norcross

Reputation: 4306

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?

Upvotes: 1

Views: 347

Answers (1)

arieljuod
arieljuod

Reputation: 15838

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.

Upvotes: 3

Related Questions