在ActiveRecord上使用setter覆盖问题

这不是一个问题,而是在Rails的Active Record上,当属性是对象时,我是如何解决write_attribute问题的。 我希望这对面临同样问题的其他人有用。

让我举个例子来解释一下。 假设您有两个类, BookAuthor

 class Book < ActiveRecord::Base belongs_to :author end class Author < ActiveRecord::Base has_many :books end 

非常简单。 但是,无论出于何种原因,您需要覆盖Book上的author =方法。 由于我是Rails的新手,我遵循了Sam Ruby关于使用Rails进行敏捷Web开发的建议:使用attribute_writer私有方法。 所以,我的第一次尝试是:

 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 

不幸的是,这不起作用。 这就是我从控制台得到的:

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

似乎Rails不承认它是一个对象并且没有做任何事情:在归属之后,作者仍然是零! 当然,我可以尝试write_attribute(:author_id, author.id) ,但是当作者还没有保存时​​它没有帮助(它仍然没有id!)并且我需要将对象保存在一起(作者必须只保存如果书有效)。

在搜索了很多解决方案之后(并尝试了许多其他事情都是徒劳的),我发现了这条消息: http : //groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8 ,所以最后我可以得到一些工作代码:

 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 

这一次,控制台对我很好:

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

这里的技巧是alias_method_chain ,它创建一个拦截器(在本例中为author_with_lookup )和旧setter的另一个名称( author_without_lookup )。 我承认花了一些时间来理解这种安排,如果有人愿意详细解释,我会很高兴,但让我感到惊讶的是缺乏关于这类问题的信息。 我必须谷歌很多才能找到一个post,标题似乎最初与问题无关。 我是Rails的新手,所以你觉得伙计们:这是一个不好的做法吗?

我建议创建一个虚拟属性,而不是覆盖author=方法。

 class Book < ActiveRecord::Base belongs_to :author def author_name=(author_name) self.author = Author.find_or_initialize_by_name(author_name) end def author_name author.name if author end end 

然后你可以做一些很酷的事情,比如将它应用到表单字段。

 <%= f.text_field :author_name %> 

这适合你的情况吗?

当您覆盖访问者时,您必须为self[:the_attribute]=self[:the_attribute]=设置实际的DB属性,而不是您要覆盖的关联生成的属性的名称。 这对我有用。

 require 'rubygems' require 'active_record' ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") ActiveRecord::Schema.define do create_table(:books) {|t| t.string :title } create_table(:authors) {|t| t.string :name } end class Book < ActiveRecord::Base belongs_to :author def author=(author_name) found_author = Author.find_by_name(author_name) if found_author self[:author_id] = found_author.id else build_author(:name => author_name) end end end class Author < ActiveRecord::Base end Author.create!(:name => "John Doe") Author.create!(:name => "Tolkien") b1 = Book.new(:author => "John Doe") p b1.author # => # b2 = Book.new(:author => "Noone") p b2.author # => # b2.save p b2.author # => # 

我强烈建议做Ryan Bates建议的事情; 创建一个新的author_name属性,并保持关联生成的方法author_name 。 减少模糊,减少混乱。

我使用alias_method解决了这个问题

 class Book < ActiveRecord::Base belongs_to :author alias_method :set_author, :author= def author=(author) author = Author.find_or_initialize_by_name(author) if author.is_a? String set_author(author) end end