【问题标题】:Issue with setter override on ActiveRecordActiveRecord 上的设置器覆盖问题
【发布时间】:2009-08-15 22:42:17
【问题描述】:

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

让我用一个例子来解释。假设你有两个类,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 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

Rails 似乎不承认它是一个对象并且什么都不做:在属性之后,作者仍然是 nil!当然,我可以试试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 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>

这里的技巧是alias_method_chain,它创建了一个拦截器(在本例中为author_with_lookup)和旧设置器的替代名称(author_without_lookup)。我承认花了一些时间来理解这种安排,如果有人愿意详细解释它,我会很高兴,但令我惊讶的是缺乏关于这类问题的信息。我必须用谷歌搜索很多才能找到一篇文章,标题似乎最初与问题无关。我是 Rails 的新手,你们怎么看:这是一种不好的做法吗?

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

    我建议创建一个虚拟属性而不是覆盖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 %>
    

    这对你的情况有用吗?

    【讨论】:

    • 我想这样做,但我不想重复属性。我上面提出的方法效果很好;我只是想分享它。我确实可以用它来制作 text_field 技巧。不过谢谢你的回复! =D
    • delegate :name, :to =&gt; :author, :prefix =&gt; true替换author_name方法不是更正确吗?
    • @Adam,这当然是另一种方法。在处理多种方法时,我通常只使用delegate。如果只有一个,我更喜欢直接定义方法,因为我觉得这样更清楚。
    • 使用委托是否有任何隐藏的缺点,或者这只是个人喜好?
    • 我有非常相似的需求,但并不完全相同,有没有办法做类似于创建方法 def author= 并在其中以某种方式引用 super(author) 的等效项?
    【解决方案2】:

    当您覆盖访问器时,您必须为write_attributeself[:the_attribute]= 设置一个实际的数据库属性,而不是您要覆盖的关联生成属性的名称。这对我有用。

    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
    # => #<Author id: 1, name: "John Doe">
    
    b2 = Book.new(:author => "Noone")
    p b2.author
    # => #<Author id: nil, name: "Noone">
    b2.save
    p b2.author
    # => #<Author id: 3, name: "Noone">
    

    不过,我强烈建议按照 Ryan Bates 的建议去做;创建一个新的author_name 属性并保持关联生成的方法不变。少一点模糊,少一点混乱。

    【讨论】:

    • 正如我上面所说,您提出的方法只有在作者保存时才有效(即有一个 id),这不是我的情况。我可以有一个新作者,只有当这本书也被保存时才必须保存。
    • 我根据您的评论重新写了一点。现在更有意义了吗?
    • 哦,谢谢,现在可以按我的预期工作了。但是(尽管有所有建议)我会保留原来的解决方案。尽管如此,如果有一天我改变主意,这是一个不错的选择。 =]
    【解决方案3】:

    我使用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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-03-16
      • 1970-01-01
      • 2013-07-26
      • 2011-12-17
      • 2023-03-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多