Search code examples
ruby-on-railsdatabasetransactionsupdate-attributes

update_attributes changes attributes even if validation fails


For example, if I run test.update_attributes prop1: 'test', prop2: 'test2' when prop1 and prop2 have validations that prevent those values, test.prop1 will still be 'test' and test.prop2 will still be 'test2'. Why is this happening and how can I fix it?


Solution

  • According to the Rails docs for update_attributes, it's an alias of update. Its source is as follows:

    # File activerecord/lib/active_record/persistence.rb, line 247
    def update(attributes)
      # The following transaction covers any possible database side-effects of the
      # attributes assignment. For example, setting the IDs of a child collection.
      with_transaction_returning_status do
        assign_attributes(attributes)
        save
      end
    end
    

    So, it's wrapped in a DB transaction which is why the rollback happens. However, let's check out assign_attributes. According to its source:

    # File activerecord/lib/active_record/attribute_assignment.rb, line 23
    def assign_attributes(new_attributes)
      ...
      _assign_attribute(k, v)
      ...
    end
    

    That is defined as:

    # File activerecord/lib/active_record/attribute_assignment.rb, line 53
    def _assign_attribute(k, v)
      public_send("#{k}=", v)
    rescue NoMethodError
      if respond_to?("#{k}=")
        raise
      else
        raise UnknownAttributeError.new(self, k)
      end
    end
    

    So, when you call test.update_attributes prop1: 'test', prop2: 'test', it basically boils down to:

    test.prop1 = 'test'
    test.prop2 = 'test'
    test.save
    

    If save fails the validations, our in-memory copy of test still has the modified prop1 and prop2 values. Hence, we need to use test.reload and the issue is resolved (i.e. our DB and in-memory versions are both unchanged).

    tl;dr Use test.reload after the failed update_attributes call.