Why does ActiveRecord allow perception of success when updating an ID, however it doesn't really work(i.e. no change in database)?

Hi,

Why does ActiveRecord allow perception of success when updating an ID, however it doesn’t really work(i.e. no change in database)?

Here’s an example. The update of “id” versus update of a non-existent attribute.

?> ai = AccountItem.find(:first)
=> #<AccountItem id: 1, account_id: 1, date: “2009-01-11”, amount: #BigDecimal:22fb334,‘0.1E2’,4(8), balance: #BigDecimal:22fb1f4,‘0.0’,4(8), description: “test”, notes: nil, created_at: “2009-01-11 09:47:28”, updated_at: “2009-01-11 09:47:28”>

ai.update_attributes!(:id => 99999)
=> true # SEEMS TO INDICATE IT WORKED

AccountItem.find(:all)
=> [#<AccountItem id: 1, account_id: 1, date: “2009-01-11”, amount: #BigDecimal:22f27c0,‘0.1E2’,4(8), balance: #BigDecimal:22f2680,‘0.0’,4(8), description: “test”, notes: nil, created_at: “2009-01-11 09:47:28”, updated_at: “2009-01-11 09:47:28”>] # IT DIDN’T AS THE ID IS STILL 1

?> ai.update_attributes!(:XXXX => 99999)
NoMethodError: undefined method XXXX=' for #<AccountItem:0x22fc6a8> <== THIS IS A BIT MORE LIKE YOU WOULD EXPECT from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/attribute_methods.rb:251:in method_missing’
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2372:in send' from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2372:in attributes=’
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2371:in each' from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2371:in attributes=’
from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/base.rb:2278:in `update_attributes!’
from (irb):60

thanks

That is because :id is a special case and is protected from mass assignment.

The reason it "works" or says that it does is because when you submit
an update form, you might have a params hash that looks like:

params[:account] #=> {:id => 2, :name => "Bob"}

and you will then do:

a = Account.find(params[:account][:id])
a.update_attributes(params[:account])

Now... in 99% of cases, you want this update_attributes command to
work, but you definately do NOT want some smartarse out there sending
in a carefully crafted web request that changes the ID value of the
account!

So that's why, update_attributes "silently" ignores any attribute that
is protected from mass assignment.

Actually... it doesn't ignore it. Look in your logs and you will see
something along the lines of "Can't mass assign protected attribute
:id" or something.

For what it's worth, doing:

account.update_attribute(:id, 1)

or

account.id = 1
account.save

Both work.

Hope that helps

Mikel

thanks Mike - I was trying to setup a spec that tests that my foreign key constraints would hold - I’ll try the method you suggest

PS. Just noted the other methods also don’t work fyi - see below. That is it doesn’t seem to be mass assignment related.

?> ai = AccountItem.find(:first)

=> #<AccountItem id: 1, account_id: 1, date: “2009-01-11”, amount: #BigDecimal:22e80b8,‘0.1E2’,4(8), balance: #BigDecimal:22e7f78,‘0.0’,4(8), description: “test”, notes: nil, created_at: “2009-01-11 09:47:28”, updated_at: “2009-01-11 09:47:28”>

ai.id = 100000

=> 100000

ai

=> #<AccountItem id: 100000, account_id: 1, date: “2009-01-11”, amount: #BigDecimal:22e34b4,‘0.1E2’,4(8), balance: #BigDecimal:22e3374,‘0.0’,4(8), description: “test”, notes: nil, created_at: “2009-01-11 09:47:28”, updated_at: “2009-01-11 09:47:28”>

ai.save

=> true

?> AccountItem.find(:all)

=> [#<AccountItem id: 1, account_id: 1, date: “2009-01-11”, amount: #BigDecimal:22db098,‘0.1E2’,4(8), balance: #BigDecimal:22daf58,‘0.0’,4(8), description: “test”, notes: nil, created_at: “2009-01-11 09:47:28”, updated_at: “2009-01-11 09:47:28”>]

Now.. that is interesting

To be honest, I've only ever used setting the id to a specific value
on a new record... not on an existing one.

The problem is that the following code in
rails/activerecord/lib/base.rb is what does the update:

      def update(attribute_names = @attributes.keys)
        quoted_attributes = attributes_with_quotes(false, false,
attribute_names)
        return 0 if quoted_attributes.empty?
        connection.update(
          "UPDATE #{self.class.quoted_table_name} " +
          "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " +
          "WHERE
#{connection.quote_column_name(self.class.primary_key)} =
#{quote_value(id)}",
          "#{self.class.name} Update"
        )
      end

As you can see, the WHERE clause is going to use id to find the record
we are after, as you set id to a different value... that won't
work....

This code would have to be updated to allow id to be set using a
"new_id" and "old_id" value pair...

I think this is enough of an edge case though that you would be better
served by making a new record and deleting the old one...

like so:

a = Account.find(1)
new_account = Account.new(a.params)
new_account.id = new_id
new_account.save

*should* work :slight_smile:

Mikel

a = Account.find(1)
new_account = Account.new(a.params)
new_account.id = new_id
new_account.save

*should* work :slight_smile:

Well.. it most definitely won't...

You need:

new_account = Account.new(a.attributes)

and you might get further

thanks - given all of this what do you think about the fact Rails doesn’t highlight the issue via exception? It still seems misleading to me the way it works…