Problem with setter override on ActiveRecord

(This message was originally written on StackOverflow -- the formatting is much prettier there -- ruby on rails - Issue with setter override on ActiveRecord - Stack Overflow)

This is not exactly a question, it's rather a report on how I solved an issue with write_attribute when the attribute is an object, on Rails' Active Record. I hope this can be useful to others facing the same problem.

Let me explain with an example. Suppose you have two classes, Book and Author:

class Book < ActiveRecord::Base   belongs_to :author end

class Author < ActiveRecord::Base   has_many :books end

Very simple. But, for whatever reason, you need to override the author= method on Book. As I'm new to Rails, I've followed the Sam Ruby's suggestion on Agile Web Development with Rails: use attribute_writer private method. So, my first try was:

class Book < ActiveRecord::Base   belongs_to :author

  def author=(author)     author = Author.find_or_initialize_by_name(author) if author.is_a? String     self.write_attribute(:author, author)   end end

Unfortunately, this does not work. That's what I get from console:

book = Book.new(:name => "Alice's Adventures in Wonderland", :pub_year => 1865)

=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>

book.author = "Lewis Carroll"

=> "Lewis Carroll"

book

=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>

book.author

=> nil

It seems that Rails does not recognize it is an object and makes nothing: after the attribuition, author is still nil! Of course, I could try write_attribute(:author_id, author.id), but it does not help when the author is not saved yet (it still has no id!) and I need the objects be saved together (author must be saved only if book is valid).

After search a lot for a solution (and try many other things in vain), I found this message: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8, so finally I could had some working code:

class Book < ActiveRecord::Base   belongs_to :author

  def author_with_lookup=(author)     author = Author.find_or_initialize_by_name(author) if author.is_a? String     self.author_without_lookup = author   end   alias_method_chain :author=, :lookup end

This time, the console was nice to me:

book = Book.new(:name => "Alice's Adventures in Wonderland", :pub_year => 1865)

=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>

book.author = "Lewis Carroll"=> "Lewis Carroll" book

=> #<Book id: nil, name: "Alice's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>

book.author

=> #<Author id: nil, name: "Lewis Carroll", created_at: nil, updated_at: nil>

The trick here is the alias_method_chain, that creates an interceptor (in this case author_with_lookup) and an alternative name to the old setter (author_without_lookup). I confess it took some time to understand this arrangement and I'd be glad if someone care to explain it in detail, but what surprised me was the lack of information about this kind of problem. I have to google a lot to find just one post, that by the title seemed initially unrelated to the problem. I'm new to Rails, so what do you think guys: is this a bad practice?

It seems that Rails does not recognize it is an object and makes nothing: after the attribuition, author is still nil! Of course, I could try write_attribute(:author_id, author.id), but it does not help when the author is not saved yet (it still has no id!) and I need the objects be saved together (author must be saved only if book is valid).

Well the key thing is that the author isn't stored as an attribute. Associations are backed by instance variables, but those instance variables contain proxy objects rather than the object itself. When you overrode author= you leapfrogged all of that code.

After search a lot for a solution (and try many other things in vain), I found this message:http://groups.google.com/group/rubyonrails-talk/browse_thread/threa…, so finally I could had some working code:

...

The trick here is the alias_method_chain, that creates an interceptor (in this case author_with_lookup) and an alternative name to the old setter (author_without_lookup). I confess it took some time to understand this arrangement and I'd be glad if someone care to explain it in detail, but what surprised me was the lack of information about this kind of problem.

Well the key thing is that overriding without calling through to the original functionality is dangerous in general. Some people have a fit at the mere mention of alias_method_chain but you want to be doing something along those lines.

Fred I have to google a lot to find just one post,