诗歌rails之Association之魔法

    Association魔法

 

    先来看看什么是association,以及association如何使你的代码更加简单和优美。

 

    举个rubyonrails guides上的例子。

 

    一个customer有很多orders,它们的模型是这样子的:

 

 

Ruby代码
  1. class Customer < ActiveRecord::Base   
  2. end   
  3.   
  4. class Order < ActiveRecord::Base   
  5. end   
 

    假如要创建一个属于一个customer的order,则需要:

 

 

Ruby代码
  1. @order = Order.create(:order_date => Time.now,  :customer_id => @customer.id)   
 

    或者删除一个customer,以及他的所有orders:

 

 

Ruby代码
  1. @orders = Order.find_by_customer_id(@customer.id)   
  2. @orders.each do |order|   
  3.   order.destroy   
  4. end   
  5.   
  6. @customer.destroy   

 

    这样的代码是非常繁琐的,并且在语义上很不清晰。让我们为这两个模型声明association:

 

 

Ruby代码
  1. class Customer < ActiveRecord::Base   
  2.   has_many :orders:dependent => :destroy   
  3. end   
  4.   
  5. class Order < ActiveRecord::Base   
  6.   belongs_to :customer   
  7. end   

 

    当我们为这两个模型声明了association之后,一切就变得简单和明了了:

 

 

Ruby代码
  1. @order = @customer.orders.create(:order_date => Time.now)   
  2.   
  3. @customer.destroy   

 

    这就是assciation的魔法。

 

 

    选择正确的association

 

    使用何种association,跟数据库schema的设计有关;但最重要的,是反之,在设计数据库schema的时候,要考虑到数据的真正含义。

 

    比如,一个company,它拥有一个account,这是明显的account从属于company的关系。

 

    那么,它们的模型和association声明最好应该是这样的,以便跟语义匹配:

 

 

Ruby代码
  1. class Company  
  2.   has_one :account  
  3. end  
  4.   
  5. class Account  
  6.   belongs_to :company  
  7. end  
 

    但association的声明,还得符合数据库的表结构(特别是建立在一个遗留数据库上的时候)。这时候的外键(foreign key)应该在accounts表上,而不是companies表上。

 

    又比如,一个teacher有很多students,同时一个students也有很多teachers。所以,它们应该是多对多的关系,那么,association的模型声明应该是这样的:

 

 

Ruby代码
  1. class Teacher  
  2.   has_and_belongs_to_many :students  
  3. end  
  4.   
  5. class Student  
  6.   has_and_belongs_to_many :teachers  
  7. end  
 

   这种多对多的映射,在数据库上需要有一个中间表:students_teachers。但有时候,我们希望这个中间表变得有意义起来,而不是纯粹起到关联作用,比如在它上面添加一些其它字段。并且希望这个中间表的名字更加有意义,以便映射到一个模型。这时候,需要使用另外一种association,来建立这三个模型之间的关联:

 

 

Ruby代码
  1. class Teacher  
  2.   has_many :relations  
  3.   has_many :students:through => :relations  
  4. end  
  5.   
  6. class Relation  
  7.   belongs_to :teacher  
  8.   belongs_to :student  
  9. end  
  10.   
  11. class Student  
  12.   has_many :relations  
  13.   has_many :teachers:through => :relations  
  14. end  

 

   还有一种高阶用法,有时候,我们希望一个模型属于几种不同的其它模型。比如,image即属于一个lightbox,也属于一个shopping cart。那我们可以使用多态association(polymorphic association ):

 

 

Ruby代码
  1. class Image  
  2.   belongs_to :image_collection:polymorphic => true  
  3. end  
  4.   
  5. class Lightbox  
  6.   has_many :images:as => :image_collection  
  7. end  
  8.   
  9. class ShoppingCart  
  10.   has_many :images:as => :image_collection  
  11. end  
 

    同时,在做migration的时候,也要注意声明这是一种多态的关联:

 

 

Ruby代码
  1. create_table :images do |t|   
  2.   t.string :name    
  3.   t.references :image_collection:polymorphic => true  
  4.   t.timestamps   
  5. end   

 

    这个多态的reference,其实会生成两个字段:image_collection_id和:image_collection_type。

 

   随之而来的那些方法

 

    这是association带来的非常重要的东西,就是随着association的声明,会有一些相关的方法被创建出来。

 

     比如我们声明了belongs_to关联之后,有四个方法被自动创建:

 

 

Ruby代码
  1. association(force_reload = false)  
  2. association=(associate)  
  3. build_association(attributes = {})  
  4. create_association(attributes = {})  

 

     又比如对于has_many关联,有如下方法被创建:

 

 

Ruby代码
  1. collection(force_reload = false)  
  2. collection<<(object, …)  
  3. collection.delete(object, …)  
  4. collection=objects  
  5. collection_singular_ids  
  6. collection_singular_ids=ids  
  7. collection.clear  
  8. collection.empty?  
  9. collection.size  
  10. collection.find(…)  
  11. collection.exist?(…)  
  12. collection.build(attributes = {}, …)  
  13. collection.create(attributes = {})  
 

    正是这些方法,让代码变得简单。

 

    举个简单的例子,还是上面那个customer、order模型:

 

 

Ruby代码
  1. @customer.order_ids = [1, 2, 3]  
 

    这个方法会把customer原有的并且不在这个id list里的orders给清除掉,创建新的原来不存在的关联,同时,会自动save到数据库。曾经需要非常复杂的操作,现在变得如此简单。

 

    还有,rails并不限制你为association添加自己的方法,这就是Association Extensions

 

   何时保存到数据库

 

    经过观察,发现对于大部分的关联,rails都会在赋值时自动把关联对象以及新的关联关系保存到数据库(除非你特别使用build方法来告诉rails不要save)。但对于belongs_to关联,却是个例外。比如对于上面account和company的例子:

 

 

Ruby代码
  1. @account.company = @company  

 

    上面这条语句,并不会自动把@company(如果是一个new record)以及这两个对象之间的关联关系保存到数据库。

 

    我觉得,这个例外的主要原因是:对于其它关联来讲,关联的key要么在一个中间表,要么在对方表上。比如:

 

 

Ruby代码
  1. @company.account = @account  
 

    外键存在于accounts表上,而不是companies表上。

 

   而对于belongs_to的关联,外键存在于自己表上。你给一个对象设置一些property,在没有调用save之前,它是不应该保存到数据库的。在如下这个场景里,就比较好理解为什么belongs_to关联不会自动保存到数据库:

 

 

Ruby代码
  1. @account.name = "new account"  
  2. @account.company = @company  
  3. @account.value = 100.00  
  4.   
  5. # @account.save  

 

    在调用上面的save语句之前,@account肯定不应该保存任何数据到数据库。

 

    而对于其它类型的关联,自动保存是比较合理的。比如:

 

 

Ruby代码
  1. @company.account = @account  

 

    外键在accounts表上,这时候会自动把外键设上,并保存到数据库。而如果需要显示save才能保存,那么代码就会变得难看并且不合理:

 

 

Ruby代码
  1. @company.account= @account  
  2. # @account.save   #需要显示保存account,而我们是在操作company  

 

    当然,上面讨论的前提是:自身对象本身已经被save了,而不是一个new record。

 

   Association的弹性

 

    Rails很精妙的一点在于,它用convention来使你省却很多麻烦,但它从来不限制你做什么,你如果觉得它的方式不好或者不适用,你可以去改变它。

 

    对于association也一样,它提供了很多options。

 

    比如对于company和account,外键默认是company_id,但你可以通过设定:foreign_key选项,来指定你希望的外键名称。

 

    又比如,对于任何关联,rails都提供了默认的sql查询语句。但我们可以通过:finder_sql来改变它的查询语句。

 

    Association,小心!

 

    Association的使用和创建并不是随心所欲的,你还得小心以下几点:

 

    1. 不要随心所欲地使用名字,至少,不应该跟model本身的instance method冲突。

 

    2. 小心cache:所有的associaiton方法,都是在最近查询的cache上操作,如果你的程序的其它部分改变了数据,就需要reload这些数据。比如

 

 

Ruby代码
  1. customer.orders # retrieves orders from the database   
  2. customer.orders.size # uses the cached copy of orders   
  3. customer.orders(true).empty? # discards the cached copy of orders    
  4.                                                 # and goes back to the database   
 

 

    Association的罪恶

 

    Association是魔法,但如果滥用,它是罪恶。

 

    举一个例子。

 

    有两个模型:

 

 

Ruby代码
  1. class User  
  2.   has_many :lightboxes  
  3. end  
  4.   
  5. class Lightbox  
  6.   belongs_to :user  
  7. end  
 

    我们希望找出拥有lightbox,并且lightbox的images_count大于10的所有users。可能我们会使用下面这个查询语句:

 

 

Ruby代码
  1. User.all(:include => [:lightboxes],   
  2.              :conditions => "lightboxes.id IS NOT NULL AND lightboxes.images_count > 10")  

 

 

   这个查询想当然地include(eager loading)了lightboxes,导致了两个罪恶:

 

   1. 滥用include,:include应该在需要eager loading的时候使用,而不是把它用作:joins的替代(其实它不会替代,只是你以为而已)。在跟踪log的时候,你会看到这个查询导致了两个表之间的left join,这并不是include引起的,而是在你的conditions里面有其它表--lightbox--存在引起的(虽然它应该是通过association知道如何join这两个表)。

 

       但使用left join和lightbox.id IS NOT NULL的方式去过滤没有lightbox的user是一种愚蠢的行为。正确并且快速的方式是使用inner join。

 

   2. 很多人以为eager loading是用join来实现的,其实并不是(http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations ),至少Rails2.2之后不再是。那么,上面那个查询导致了一个很庞大但我们并不需要的数据库查询,即:

 

 

Sql代码
  1. SELECT * FROM lightboxes WHERE id IN (....................)  
 

    正确的查询语句应该是:

 

 

Ruby代码
  1. User.all(:joins=> "INNER JOIN lightboxes ON users.id = lightboxes.user_id",   
  2.              :conditions => "lightboxes.images_count > 10")  
 

   当然,有人会说,这也不是“最美”的查询方式,你可以利用模型之间的association来写出一个更漂亮的查询,但上面这个查询是建立在“我们需要性能”的基础上的。

 

   其它

 

    关于association,还有其它一些重要的东西,比如 Association Callbacks ,就不再一一赘述了。本文完毕。

 

 

Reference:

 

    http://guides.rubyonrails.org/association_basics.htm

posted @ 2009-08-17 20:29  麦飞  阅读(503)  评论(0编辑  收藏  举报