How to prevent nested create failing with scoped has_many in ActiveRecord?
Asked Answered
P

1

5

A report_template has_many report_template_columns, which each have a name and an index attribute.

class ReportTemplateColumn < ApplicationRecord
  belongs_to :report_template
  validates :name, presence: true
end

class ReportTemplate < ApplicationRecord
  has_many :report_template_columns, -> { order(index: :asc) }, dependent: :destroy
  accepts_nested_attributes_for :report_template_columns, allow_destroy: true
end

The report_template_columns need to be ordered by the index column. I'm applying this with a scope on the has_many association, however doing so causes the following error:

> ReportTemplate.create!(report_template_columns: [ReportTemplateColumn.new(name: 'id', index: '1')])
ActiveRecord::RecordInvalid: Validation failed: Report template columns report template must exist
from /usr/local/bundle/gems/activerecord-5.1.4/lib/active_record/validations.rb:78:in `raise_validation_error'

If I remove the scope the same command succeeds.

If I replace the order scope with where scope that command fails in the same way, so it seems to be the presence of the scope rather than the use of order specifically.

How can I apply a scope to the has_many without breaking the nested creation?

Pigment answered 20/10, 2017 at 11:59 Comment(1)
Don't apply the scope to the assocation. Its an anti-pattern in the same way as defualt_scope. It may seem convient but will make things very difficult if you don't want the scope applied later. weblog.jamisbuck.org/2015/9/19/default-scopes-anti-pattern.htmlAldus
E
9

I believe you need the :inverse_of option added to the has_many association.

class ReportTemplate < ApplicationRecord
  has_many :report_template_columns, -> { order(index: :asc) },
           dependent: :destroy, inverse_of: :report_template
end

The api states that :inverse_of:

Specifies the name of the belongs_to association on the associated object that is the inverse of this has_many association. Does not work in combination with :through or :as options. See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.

I also like how the cocoon gem words their reason for using it:

Rails 5 Note: since rails 5 a belongs_to relation is by default required. While this absolutely makes sense, this also means associations have to be declared more explicitly. When saving nested items, theoretically the parent is not yet saved on validation, so rails needs help to know the link between relations. There are two ways: either declare the belongs_to as optional: false, but the cleanest way is to specify the inverse_of: on the has_many. That is why we write: has_many :tasks, inverse_of: :project

Erythema answered 20/10, 2017 at 12:18 Comment(5)
Should it be belongs_to :report_template, inverse_of: :report_template_columns (plural)? With the singular I'm getting ActiveRecord::InverseOfAssociationNotFoundError: Could not find the inverse association for report_template (:report_template_column in ReportTemplate), with the plural I'm getting the same error in the question.Pigment
Oops, I think I wrote my answer incorrectly. The inverse_of should be on the has_many side of things.Erythema
That's working. Though I don't need to change the create! call in order for the inverse_of to fix the issue. If you remove that section from the answer I'll make this as the accepted answer. Thanks for your help :)Pigment
I'm glad I was able to help!Erythema
In case it helps anyone else - this also fixes a problem (also present in Rails 4) where a nested-nested model can't see its immediate parent during validation. That is: A has_many b, B has_many c, if C validates based on some field in B it fails unless B has_many c, inverse_of: b.Sigridsigsmond

© 2022 - 2024 — McMap. All rights reserved.