homeASCIIcasts

167: 更多关于虚拟属性 

(view original Railscast)

Other translations: En

Other formats:

Written by fanhaipeng (fanhaipeng.javaeye.com)

第16集中第一次讲到了虚拟属性。如果你对虚拟属性还不熟悉,你应该先读一下那一集的文字版,或者看那集的视频,将会对你理解本集的内容有帮助。当你要处理的表单非常复杂,里面的字段不能直接对应到你的数据模型的属性上的话,虚拟属性会大显身手。

本集当中,我们将结合回调函数用虚拟属性来给一个博客程序增加标签功能。我们先从下面这个包括一些文章的简单程序开始。

The articles page from our example blogging app.

我们要做的是在一个文本框中输入已经存在的、或者新的标签,把他们加到一篇新文章上。虽然已经有很多给 Rails 用的标签插件,但是我们还是打算重头开始实现一个我们自己的方案,利用虚拟属性,这一切都将变的非常容易。

创建数据模型

我们先创建一个 Tag 数据模型。这个数据模型非常简单,只有一个属性, name 。 控制台代码

script/generate model tag name:string  

TagArticle 数据模型之间必然是有联系的。一篇文章可以有多个标签,一个标签也可以属于多篇文章,这是一种多对多的关系。我们将创建一个用于连接的数据模型,起名叫Tagging 。这个数据模型只有两个整数字段,article_idtag_id ,分别是ArticleTag 数据模型的外部键。 控制台代码

script/generate model tagging article_id:integer tag_id:integer  

下面,我们执行新建的这两个数据迁移任务来创建这两个新的数据模型。

rake db:migrate

接下来我们会很快地在数据模型中来定义这些关系。Tagging 将同时属于(译注:belong_toArticleTag

/app/models/tagging.rb

class Tagging < ActiveRecord::Base     
  belongs_to :article    
  belongs_to :tag    
end

Tag 有一个指向Articlehas_many :though 的关系。

/app/models/tag.rb

class Tag < ActiveRecord::Base     
  has_many :taggings, :dependent => :destroy    
  has_many :articles, :through => :taggings    
end 

同样,Article 也有一个指向Tag 这样的关系。

/app/models/article.rb

class Article < ActiveRecord::Base  
  has_many :comments, :dependent => :destroy  
  has_many :taggings, :dependent => :destroy  
  has_many :tags, :through => :taggings  
  validates_presence_of :name, :content  
end 

注意在TagArticle 数据模型中用到的 :dependent => :destroy 。这将保证,当一个Tag 或者Article 被删除后、任何不被引用的Tagging 也会被删除。

视图

搞定了数据模型,我们现在可以修改一下创建新文章的视图,加一个文本框,在里面输入空格分隔的标签,使其与文章关联。因为我们要把文本框中的文字转化为关联关系,所以最简洁的实现方法就是用虚拟属性。

/app/views/articles/new.html.erb

<% form_for @article do |form| %>     
  <ol class="formList">     
    <li>     
      <%= form.label :name, "Name" %>     
      <%= form.text_field :name %>     
    </li>     
    <li>     
      <%= form.label :tag_names, "Tags" %>     
      <%= form.text_field :tag_names %>     
    <li>     
      <%= form.label :content, "Content" %>     
      <%= form.text_area :content, :rows => 10 %>     
    </li>     
    <li>     
      <%= form.submit "Submit" %>     
    </li>     
  </ol>     
<% end %>    

添加了tag_names字段的创建新文章的表单

我们的Article 数据模型没有一个叫tag_names 的属性,所以我们需要定义一个虚拟属性来表示关联到这篇文章的标签字符串。之前,我们使用读方法和赋值方法来创建虚拟属性的。对于我们的Article 数据模型虽然我们也可以定义一个tag_names= 方法来给文章的标签赋值,但是这种方法有一些缺陷。其中一个缺陷是,无论一篇文章有没有被保存,在赋值函数中创建标签的同时也会创建一个Tagging 记录。

更好的做法是使用一个回调函数。在 Article 类中用一个 after_save 的回调函数可以保证只有在一篇文章被保存后它的标签才会被保存。数据模型定义如下所示:

/app/models/article.rb

class Article < ActiveRecord::Base     
  has_many :comments, :dependent => :destroy    
  has_many :taggings, :dependent => :destroy    
  has_many :tags, :through => :taggings    
  validates_presence_of :name, :content    
    
  attr_accessor :tag_names    
  after_save :assign_tags    
    
  private     
  def assign_tags     
    if @tag_names    
      self.tags = @tag_names.split(/\s+/).map do |name|     
        Tag.find_or_create_by_name(name)     
      end    
    end    
  end    
    
end    

我们还是需要为虚拟属性 tag_names 定义读方法和赋值方法,不过我们可以通过访问器(译注: accessor )来定义。当一篇文章被保存后,私有方法 assign_tags 将会被调用,它先看 @tag_names 是不是 nil ,如果不是就把它的值按找到的空格来分割,形成一个数组。然后再用 map 方法迭代这个数组返回每一个标签。 find_or_create_by_name 函数读取一个标签,如果该标签不存在就创建出来。最后我们把这个 Tag 数组赋值给 Article 类的 tags 属性。

经过这些数据模型上的修改,我们现在可以新建一篇文章、并给它加一些标签来测试一下我们的代码。

Adding a new article with tags.

新建好一篇文章后我们可以用 Rails 的控制台来看看它的标签是不是已经被正确的加上了。

>> a = Article.last  
=> #<Article id: 3, name: "New Article", content: "I am a new article.", author_name: nil, created_at: "2009-06-24 20:38:56", updated_at: "2009-06-24 20:38:56">  
>> a.tags  
=> [#<Tag id: 1, name: "stuff", created_at: "2009-06-24 20:38:56", updated_at: "2009-06-24 20:38:56">, #<Tag id: 2, name: "things", created_at: "2009-06-24 20:38:56", updated_at: "2009-06-24 20:38:56">]  

没错,两个标签已经被创建并且关联到了我们的文章上。不过我们还没有大功告成,如果我们要编辑这篇文章,我们看到标签文本框里是空的。

The tags field is not repopulated when the article is invalid.

为了解决这个问题,我们需要改一下Article 数据模型,为tag_names 加一个读方法,这个方法将一篇文章的所有标签以一个字符串的形式返回。因为我们要显式地加一个读方法,所以我们需要把attr_accessor 替换为attr_writer

/app/models/article.rb

def tag_names     
  @tag_names || tags.map(&:name).join(' ')     
end

上面的方法里,如果对象变量@tag_names 已经有值,就返回它;如果没有,就把这篇文章所有的标签用空格连接成一个字符串,然后返回。

与验证器(译注:Validators)一同工作

现在这种实现方法的另一个好处是,它可以和验证器协同工作。如果我们添加了一个标签,同时删除文章的内容使得表单不合法,那么就会有错误信息显示出来,并且标签文本框中的内容还能保持不变。

The tags field now has its value maintained when the form in invalid.

因为我们使用了after_save 的回调函数,所以在这篇文章没有被保存前,新标签“etc”不会被创建。标签以及它和文章的关联关系只有在表单合法并且这篇文章被保存后才会被创建。

现在我们可以很容易地给我们的文章加标签了。如果你的表单比较复杂或者里面有关联到其他数据模型的字段,那么将虚拟属性和回调函数结合起来真的值得一用。