Getting ActiveRecord::RecordInvalid error with has_many through association; validation issue on join table

Viewed 2153

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>'
1 Answers
Related