英文原文:http://robots.thoughtbot.com/post/159809241/whats-the-deal-with-rails-polymorphic-associations

 1 class Person < ActiveRecord::Base  2 
 3   has_one :address, :as => :addressable  4 
 5 end  6 
 7 class Company < ActiveRecord::Base  8 
 9   has_one :address, :as => :addressable 10 
11 end 12 
13 class Address < ActiveRecord::Base 14 
15   belongs_to :addressable, :polymorphic => true 16 
17 end

以上的例子显示出address可以属于任意的model,另一个写法可以是:

1 class Address < ActiveRecord::Base 2 
3   belongs_to :person 4 
5   belongs_to :company 6 
7 end

这样的话,address表中会有两个foreign key。但这两个中只有一个有值,因为一个地址要么属于某个人,要么属于某个公司。

has_one和has_many可引用as这个关键字的参数作为“polymorphic interface”,那这是什么东东呢?

Polymorphic interface让我联想到Ruby中诸如“<< message”这样的语句。我可以将message传给一堆不同的东西,如数组、字串、IO等。而且,当它们收到时都知道要做什么。

我把<<当作这些不同的类都实现的一个接口。从静态语言(比如Java)的角度来说,有以下这样一个接口:

1 public interface Collection { 2 
3   Collection <<(Object anObject); 4 
5 }

其他的类都实现了这个接口:

 1 public class Array implements Collection {  2 
 3   public Collection << (Object object) {  4 
 5     // add object to me
 6 
 7   }  8 
 9 } 10 
11 public class String implements Collection { 12 
13   public Collection << (Object object) { 14 
15     // add object to me
16 
17   } 18 
19 }

同样的道理,最好我们最开始看到的那些rails类有如下关系就好了:

 1 class Address < ActiveRecord::Base  2 
 3 end  4 
 5 class Person < Address  6 
 7 end  8 
 9 class Company < Address 10 
11 end

但是,Person和Company显然都不可能是Address,那我们就把polymorphic interface作为父类名:

 1 class Addressable < ActiveRecord::Base  2 
 3 end  4 
 5 class Person < Addressable  6 
 7 end  8 
 9 class Company < Addressable 10 
11 end

这样就好多了。Person和Company都变成Addressable了,那Address怎么办呢?

1 class Addressable < ActiveRecord::Base 2 
3   has_one :address 4 
5 end

这样,Person和Company都“has_one”address,它们从Addressable继承了这个属性,而且,这里也不用声明“:polymorphic => true”

问题似乎解决了,但Person和Company都继承自Addressable的话,就造成了“Single-table inheritance”(STI)。若是Person和Company没有什么共同的属性,STI就会给我们一张巨大的表,当某一行存的是Company属性时,所有Person的栏位都是空的;若存的是Person属性时,所有Company的栏位也会是空的。如果还有更多的类要继承Addressable,那这个表就会越长越大。

如果不考虑数据库的话,这样的模型是可以讲得通的,只考虑继承行为不考虑状态。

这里,我们可以看到STI的继承关系适合一些有相同属性而不是行为的类。

事实上,有3种常用方法可以用来表达关系数据库的继承关系。但Rails只提供我们STI。

1. Single Table Inheritance (STI) - 所有的继承关系指向同一张表,并且表中会附加Type栏,里面存放类名。

2. One Table per Concrete Class - 每一个具体的类各自有一张表,基类是抽象类,它没有表。任何常用的属性会在子类中重复。

3. One Table per Class - 继承关系中的每个类都有自己的表,子类的表中有foreign key指向对应的父类表中的某一行。

第二种明显不能用,所以看一下第三种。

回到Person,Company和Addressable的例子,如果它们各自有一张表,那数据库会是什么样呢?

1 addressables (id) 2 
3 people (id, name, age, height, weight, addressable_id) 4 
5 companies (id, size, established_date, addressable_id) 6 
7 addresses (id, street, city, state, addressable_id)

当我们从数据库中度一个Person时,我们通过addressable_id和addressables表做join。当我们需要一个Person的address时,我们通过addressable_id和address表做join。

至此,我们用继承代替了polymorphic association。

要是,我们再加一个特性,让用户能给people和companies加tag该怎么做呢?我们可以用Rais插件acts_as_taggable,而它是使用polymorphic associations的。下面我们不用它,看会发生什么。

增加一个Taggable类,但Ruby不支持多重继承,也没有Java/C#的接口方式。那我们用Ruby的module,这样的话Addressable也要被改为module。

 1 module Addressable  2 
 3 end
 4 
 5 module Taggable  6 
 7 end
 8 
 9 class Person < ActiveRecord::Base 10 
11   include Addressable 12 
13   include Taggable 14 
15 end
16 
17 class Company < ActiveRecord::Base 18 
19   include Addressable 20 
21   include Taggable 22 
23 end

但在Rails中,我们不能直接写:

1 module Addressable 2 
3   has_one :address 4 
5 end

而要写成:

 1 module Addressable  2 
 3   def self.included(klazz) # klazz is that class object that included this module
 4 
 5     klazz.class_eval do  6 
 7       has_one :address  8 
 9     end 10 
11   end 12 
13 end

现在数据库就变成这样了:

1 addresses (id, street, city, state, person_id, company_id)

一个address不能既属于person又属于company,看来我们又回到最初的问题了。

我们只好用polymorphic associations。

先对问题来个小小的总结:首先,我们用了polymorphic associations;然后,我们决定重构,用“更自然的方式”—— 继承,得到了Addressable和Taggable两个类。但当Person和Company都需要Taggable时,我们使用了Ruby的module方式来实现多重继承,但这却使我们回到了问题的起点。

于是,我们用polymorphic associations,并称这样的association为polymorphic interface。

 1 class Address < ActiveRecord::Base  2 
 3   # here's where we'll use Addressable
 4 
 5   belongs_to :addressable, :polymorphic => true  6 
 7 end  8 
 9 class Tagging < ActiveRecord::Base 10 
11   # here's where we'll use Taggable
12 
13   belongs_to :taggable, :polymorphic => true 14 
15 end

Java/.Net世界中的Object-relational mapping (ORM)库,它们提供了3种继承方式,而Rais中只提供STI。因为,Ruby缺少interface机制,所以使用另外两种的话相对其他支持interface的语言来说就不够强大。上面我们看到,当我们新加Tagbble需求的时候就不够用了。

而Rails的polymorphic associations在Java/.Net的ORM库中也没有使用过,是因为,Ruby用module来实现多重继承的方式有独一无二的地方,polymorphic associations就是这个独一无二。如果读过acts_as_taggable插件的代码,就会发现它用了polymorphic associations,并且它用了module来包含任何共用的行为。如果在你的类定义中调用了acts_as_taggble,所有的这些行为就都可以马上用了。