英文原文: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,所有的这些行为就都可以马上用了。