2.1 bug?: AR save doesn't work after gsub! on an attribute

All,

Rails 2.1
MySQL 5.x
Mac OS X 10.5

I think I've found a significant bug in ActiveRecord::save relating to
the use of gsub! on String attributes.

Given: A model m with one attribute x, a string.

If I attempt to update the value of x using m.x.gsub!, I can't get the
modified value to save. Here's an example - notice how even though I
update the string value using gsub!, when I save the record, the change
doesn't stick. This definitely used to work before 2.1.

m = MyModel.create(:x => 'abc')

=> #<MyModel id: 13, x: "abc", created_at: "2008-06-19 07:47:52",
updated_at: "2008-06-19 07:47:52">

m.x.gsub!('abc', '123')

=> "123"

m.x

=> "123"

m.save

=> true

m.id

=> 13

MyModel.find(13)

=> #<MyModel id: 13, x: "abc", created_at: "2008-06-19 07:47:52",
updated_at: "2008-06-19 07:47:52">

Notice, however, that if I use m.x = m.x.gsub, it will work:

m = MyModel.create(:x => 'abc')

=> #<MyModel id: 14, x: "abc", created_at: "2008-06-19 07:52:50",
updated_at: "2008-06-19 07:52:50">

m.x = m.x.gsub('abc', '123')

=> "123"

m.x

=> "123"

m.save

=> true

m.id

=> 14

MyModel.find(14)

=> #<MyModel id: 14, x: "123", created_at: "2008-06-19 07:52:50",
updated_at: "2008-06-19 07:53:07">

Is this a bug in AR? What's changed about string attribute handling
that could explain this behavior?

Thanks,
Wes

Have you got partial updates turned on? If you do that, then you can't
edit attributes in place (ie using gsub!)

Fred

Have you got partial updates turned on? If you do that, then you can't
edit attributes in place (ie using gsub!)

Fred

Hmm... I don't know. Are they turned on by default? How do I check?

Wes

> Have you got partial updates turned on? If you do that, then you can't
> edit attributes in place (ie using gsub!)

> Fred

Hmm... I don't know. Are they turned on by default? How do I check?

Can't remember the defaults. Check environment.rb or the initializers,
but if it's on, ActiveRecord::Base.partial_updates will be true

Fred

OK, fair enough.

0000isp37920l:Rails 2.1 Reference weyus$ script/console
Loading development environment (Rails 2.1.0)

ActiveRecord::Base.partial_updates

=> true

Can you explain why when partial_updates are enabled that updating a
string attribute in place will not work, or point me to a relevant
explanation? I'm having trouble thinking that through - granted it is
3:15 AM as well ;)...

Wes

OK, fair enough.

0000isp37920l:Rails 2.1 Reference weyus$ script/console
Loading development environment (Rails 2.1.0)>> ActiveRecord::Base.partial_updates

=> true

Can you explain why when partial_updates are enabled that updating a
string attribute in place will not work, or point me to a relevant
explanation? I'm having trouble thinking that through - granted it is
3:15 AM as well ;)...

Because it doesn't detect that that attribute has changed (since it's
the same object).
see also
http://groups.google.com/group/rubyonrails-core/browse_thread/thread/fc1a9575d316a52d/08f2b2fb8be6b5e6?lnk=gst&q=partial+updates#08f2b2fb8be6b5e6

"That's going to be the way for Active Record attributes. If you want
dirty tracking (and partial updates), you don't get to do inplace
modifications."

Fred

6:23pm here :slight_smile:
(this is my vague understanding - haven't played with this myself yet)

ActiveRecord tracks changes to fields based on calls to "model.field="
and (presumably) other things that change the value of the field
through a call to a method on the model object.

However, if you say:
   m.x.gsub!('abc', '123')
you are getting the value of m.x as a string, and then calling gsub!
which modifies the string in place.

ActiveRecord has no easy way to know you've done this - it would have
to somehow track all object changes to know that m.x is dirty and
needs saving.

If you said:
  m.x = m.x.gsub('abc','123')
everything would work fine, as the call to "=" marks the field as changed.

- Korny

From

http://groups.google.com/group/rubyonrails-core/browse_thread/thread/fc1a9575d316a52d/08f2b2fb8be6b5e6?lnk=gst&q=partial+updates#08f2b2fb8be6b5e6

"Naturally, we can't just change this as existing implementations may
break. But I think we should make partial updates the default for apps
created with 2.1. And then finally the default for all by 3.0. We can
do this by making an option like config.active_record.partial_updates
= true that's only set for apps created post-2.1 and when that option
is not set, we complain in the log about the deprecated status. "

FWIW, my app. was created on Rails 1.0, and this totally broke for me
when I upgraded to 2.1, so I suspect that this sentiment didn't get
carried through.

Wes

From

http://groups.google.com/group/rubyonrails-core/browse_thread/thread/fc1a9575d316a52d/08f2b2fb8be6b5e6?lnk=gst&q=partial+updates#08f2b2fb8be6b5e6

"Naturally, we can't just change this as existing implementations may
break. But I think we should make partial updates the default for apps
created with 2.1. And then finally the default for all by 3.0. We can
do this by making an option like config.active_record.partial_updates
= true that's only set for apps created post-2.1 and when that option
is not set, we complain in the log about the deprecated status. "

FWIW, my app. was created on Rails 1.0, and this totally broke for me
when I upgraded to 2.1, so I suspect that this sentiment didn't get
carried through.

THere was some flip-flopping over what the default should be - can't
remember what the final decision was.

Fred

Of course, you can just tell AR that you used gsub by calling
m.x_has_changed!
Or switch off partial updates in environment.rb.
-jm

All 2.1 apps have partial updates enabled.

I would prefer to freeze all attributes so you get an exception when
you try to modify them in-place. This is what the dirty plugin does.
Unfortunately there are too many quirks with certain frozen classes
(such as Date, which modifies itself) to make this change in 2.1.

Perhaps for 2.2.

jeremy

Because it doesn't detect that that attribute has changed (since it's
the same object).
see also
http://groups.google.com/group/rubyonrails-core/browse_thread/thread/fc1a9575d316a52d/08f2b2fb8be6b5e6?lnk=gst&q=partial+updates#08f2b2fb8be6b5e6

Note also that
instance.attribute = instance.attribute << 'a' << 'b' also doesn't
"qualify" as a changed attribute, since it reassigns it the same object.
Go figure.
-R

I don't quite get that. Can you fill in the blanks?

Wes

Fjan wrote:

Of course, you can just tell AR that you used gsub by calling
m.x_has_changed!
Or switch off partial updates in environment.rb.
-jm

I can't find any reference to this type of method (_has_changed!) in any
of my Rails gems. It doesn't appear to exist.

Wes

Wes,

I think Fjan meant:

m.x_will_change!

Before modifying an attribute in-place:
  person.name_will_change!
  person.name << 'by'
  person.name_change # => ['uncle bob', 'uncle bobby']

http://api.rubyonrails.com/classes/ActiveRecord/Dirty.html