What's New in Edge Rails: Dirty Objects 23

Posted by ryan
at 7:38 PM on Monday, March 31, 2008



The ability to track whether or not your active record objects have been modified or not becomes a lot easier with the new dirty object functionality of edge rails. It’s dead simple, and pretty slick, to use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
article = Article.find(:first)
article.changed?  #=> false

# Track changes to individual attributes with
# attr_name_changed? accessor
article.title  #=> "Title"
article.title = "New Title"
article.title_changed? #=> true

# Access previous value with attr_name_was accessor
article.title_was  #=> "Title"

# See both previous and current value with attr_name_change accessor
article.title_change  #=> ["Title", "New Title"]

Beyond the clever attribute based accessor methods, you can also query to object directly for its list of all changed attributes. (Continuing from the previous example):

1
2
3
4
5
# Get a list of changed attributes
article.changed  #=> ['title']

# Get the hash of changed attributes and their previous and current values
article.changes  #=> { 'title' => ["Title", "New Title"] }

Note: Once you save a dirty object it clears out its changed state tracking and is once again considered unchanged.

1
2
3
article.changed?  #=> true
article.save  #=> true
article.changed?  #=> false

If you’re going to be modifying an attribute outside of the attr= writer, you can use attr_name_will_change! to tell the object to be aware of the change:

1
2
3
4
article = Article.find(:first)
article.title_will_change!
article.title.upcase!
article.title_change  #=> ['Title', 'TITLE']

And coming down the pipe is a feature that will make the most of this functionality – partial SQL updates that will only update attributes that have changed…

tags: ruby, rubyonrails

Comments

Leave a response

  1. PratikMarch 31, 2008 @ 08:43 PM

    Partial updates are already in edge :)

  2. jasonMarch 31, 2008 @ 09:05 PM

    What is the benefit of partial updates?

  3. Jason WatkinsMarch 31, 2008 @ 09:17 PM

    Jason: just imagine a model with large text content attributes, or god forbid, a blob.

  4. Josh MartinMarch 31, 2008 @ 09:45 PM

    I’m surprised that it took this long. Partial updates are much faster, cleaner, and easier to understand (let alone the most efficient way to update a table and avoid some locking mechanisms). Wether it is digging through logs, checking if you updated something that needs to flush the cache, or any other reason, this is has been a long time coming. Glad to see some much needed progress in this area finally committed.

  5. DonApril 01, 2008 @ 01:42 AM

    That’s cool, though your last example is a little scary. That’s going to potentially be really hard to remember to have to flag attributes as dirty in those cases. Not that it happens a lot, but that will just make it all the more frustrating to debug…

  6. SebastianApril 01, 2008 @ 03:30 AM

    great! :)

  7. ThorbenApril 01, 2008 @ 04:34 AM

    I think Don is right! Will I have to flag any change I made to field without the field= method?! That would cause really strange and undebuggable errors when I miss it.

    On the other hand this is a cool feature. Maybe I just missed a point or it needs a little bit more tweaking? And: What happens when I do this:

    article = Article.find(:first) article.title #=> “Test” article.title = “Test”

    What would:

    article.title_changed?

    return?

  8. ThorbenApril 01, 2008 @ 04:35 AM

    AAAARG! ;)

    article = Article.find(:first)

    article.title #=> “Test”

    article.title = “Test”

  9. RyanApril 01, 2008 @ 07:21 AM

    Pratik: Yes, I know partial updates are already here – I was trying to setup a little teaser for my readers whilst I labored on that article :)

    Thorben: This functionality is smart enough to only mark a field as changed when its values are different. So your example will return false:

    
    article = Article.find(:first)
    article.title #=> "Title" 
    article.title = "Title" 
    article.title_changed? #=> false
    
  10. bryanlApril 01, 2008 @ 07:49 AM

    Get this functionality in your 2.0 app.

    http://code.bitsweat.net/svn/dirty

  11. Paul BarryApril 01, 2008 @ 08:18 AM

    But what about this?

    article = Article.find(:first)
    article.title.upcase! #=> "TITLE" 
    article.title_changed? #=> ?
  12. Paul BarryApril 01, 2008 @ 08:49 AM

    After looking at the code in the plugin Bryan posted a link to, it’s pretty obvious that my example would return false, which is why the `will_change!` flag is necessary. Why not have the `changed?` methods and `save` methods check to see if the value is different, rather than have the setters keep track of things that changed? Then you don’t have to worry about the flag, if the value is different, no matter how it changed, `changed?` returns the right value.

  13. DanielApril 01, 2008 @ 09:48 AM

    Paul: You would then have to store all the original attributes twice in memory. Don’t know if that’s the reason, but it would be rather ugly.

  14. MikeApril 01, 2008 @ 08:21 PM

    article.title.changed? and article.title.was would have been much cleaner syntax than underscore accessor.

  15. Luke RedpathApril 02, 2008 @ 06:10 AM

    “would have been much cleaner syntax”

    What makes it cleaner? There isn’t anything wrong with underscores. You’re asking the parent object if attribute x has changed, not asking the attribute itself if it’s changed, so your suggested syntax isn’t semantically correct.

  16. jeromeApril 02, 2008 @ 01:00 PM

    You may now use Article.first instead of Article.find(:first) ! :)

  17. Paul BarryApril 03, 2008 @ 11:00 AM

    Daniel: Yeah, I guess that’s the tradoff. I consider having to set the flag pretty ugly as well.

  18. PabloApril 03, 2008 @ 05:42 PM

    What would happend with partial updates at cases like Paul’s example?

  19. BrandonApril 03, 2008 @ 07:26 PM

    @Luke… okay, I see your point, that is technically true, but Mike’s approach seems much more readable to me too, since dots are definitely always separators, while with underscores you have to stop and think about it. Perhaps just because this is new… maybe when I’m more used to it I’ll come around to seeing it your way.

  20. Mark ThomasApril 05, 2008 @ 03:25 PM

    That’s great, but can I selectively turn it off? What if I have an attribute which contains a large file blob? I don’t want to have it in memory twice.

  21. Sam GranieriApril 13, 2008 @ 03:42 PM

    I think this breaks acts_as_versioned

  22. Andy WattsApril 14, 2008 @ 01:52 PM

    Agreed, this seems to break acts_as_versioned.

    NoMethodError: You have a nil object when you didn’t expect it! You might have expected an instance of Array. The error occurred while evaluating nil.include? from /www/trunk/vendor/rails/activerecord/lib/active_record/dirty.rb:118:in `write_attribute’ from /www/trunk/vendor/rails/activerecord/lib/active_record/attribute_methods.rb:200:in `challenge_id=’ from /www/trunk/vendor/rails/activerecord/lib/active_record/attribute_methods.rb:229:in `send’ from /www/trunk/vendor/rails/activerecord/lib/active_record/attribute_methods.rb:229:in `method_missing’ from (irb):2

  23. Kevin KohrtApril 16, 2008 @ 09:36 AM

    Looks great. Can’t wait to use this feature. Doing a quick threat-check for acts_as_versioned in our code and did find ... if const.respond_to? :versions #test for acts_as_versioned which I hope suggests there is a means to identify and work around acts_as_versioned issues

Comment