I have three associated models like these:
class Product < ActiveRecord::Base
belongs_to :user
has_many :descriptions, {
dependent: :destroy,
before_add: [:add_user_id_to_description, :validate_description]
}
has_many :documents, through: :descriptions
# ...
def validate_description(d)
unless d.valid?
d.errors[:user_id].each do |err|
self.errors.add(:base, "Doc error: #{err}")
end
end
end
end
class Document < ActiveRecord::Base
belongs_to :user
has_many :descriptions, {
dependent: :destroy,
before_add: [:add_user_id_to_description, :validate_description]
}
has_many :products, through: :descriptions
end
class Description < ActiveRecord::Base
belongs_to :user
belongs_to :product
belongs_to :document
end
When I do something like:
doc = user.documents.build
doc.update_attributes(:product_ids => [1,2])
And the description
validation fails, then I get false
and the appropriate errors on doc
. This is exactly what I want.
However, if doc
already exists, e.g.:
doc = user.documents.first
doc.update_attributes(:product_ids => [1,2])
And the description
validation fails, then I get an ActiveRecord::RecordInvalid
error.
I know exactly why this happens--the insert_record
method from has_many_through_association.rb calls save!
internally, which propagates the error. It exits early, skipping this call, for new records.
Is there some way I can set up my models to prevent this save!
? Or am I forced to rescue
from the error?
EDIT
I've tried the setup described by Carlos Drew below; I've also tried setting validates_associated :descriptions
, and adding inverse_of: :whatever
to the has_many :descriptions
options hash. I also tried setting a before_validation
callback on the Product
and Document
models, but apparently association callbacks get run first (?). Each attempt seemed to produce the exact same error message.
I'm pasting my error trace from the console below.
Document Load (1.8ms) SELECT "documents".* FROM "documents" WHERE "documents"."user_id" = 19 ORDER BY "documents"."id" DESC LIMIT 1
(1.0ms) BEGIN
Product Load (41.7ms) SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT 1 [["id", 3640]]
Product Load (4.1ms) SELECT "products".* FROM "products" INNER JOIN "descriptions" ON "products"."id" = "descriptions"."product_id" WHERE "descriptions"."document_id" = 3552
User Load (7.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 19 LIMIT 1
Account Load (2.0ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."user_id" = 19 LIMIT 1
(0.9ms) SELECT COUNT(*) FROM "descriptions" WHERE "descriptions"."user_id" = 19
(1.2ms) ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: User You have reached limit of 1
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/validations.rb:56:in `save!'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/attribute_methods/dirty.rb:33:in `save!'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:264:in `block in save!'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:313:in `block in with_transaction_returning_status'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:208:in `transaction'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:311:in `with_transaction_returning_status'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:264:in `save!'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/has_many_through_association.rb:85:in `save_through_record'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/has_many_through_association.rb:52:in `insert_record'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/collection_association.rb:496:in `block (2 levels) in concat_records'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/collection_association.rb:344:in `add_to_target'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/collection_association.rb:495:in `block in concat_records'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/collection_association.rb:493:in `each'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/collection_association.rb:493:in `concat_records'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/collection_association.rb:134:in `block in concat'
... 14 levels...
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/associations/builder/collection_association.rb:71:in `block in define_writers'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/attribute_assignment.rb:85:in `block in assign_attributes'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/attribute_assignment.rb:78:in `each'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/attribute_assignment.rb:78:in `assign_attributes'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/persistence.rb:216:in `block in update_attributes'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:313:in `block in with_transaction_returning_status'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:208:in `transaction'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/transactions.rb:311:in `with_transaction_returning_status'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/activerecord-3.2.13/lib/active_record/persistence.rb:215:in `update_attributes'
from (irb):2
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
from /usr/local/rvm/gems/ruby-1.9.3-p125/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'