锻造坊 - 将数据硬塞进数据库

作者:Reuven M. Lerner

关系数据库非常适合存储和检索数据,但有时它们并不能完全胜任这项任务。Joe Celko,他的SQL for Smarties系列书籍是我的最爱之一,他专门用了一整本书来探讨树和层级结构的问题。这些数据结构在大多数编程语言中可能很常见且很有用,但如果重视数据库的有效使用,则它们可能难以建模为表。如果您正在处理许多相关但不同的实体类型(例如不同类型的员工或不同类型的车辆),情况会变得更加棘手。

解决此问题的一种方法是不使用关系数据库。对象非常擅长处理树和数组以及继承层级结构。此外,对象数据库确实存在,并且基于Python的Zope应用框架已经证明,即使在生产环境中也可能拥有对象数据库。Gemstone展示了Ruby在其Smalltalk虚拟机之上运行,并附带对象数据库,这意味着Ruby程序员可能很快就能使用类似的技术。

但是,对象数据库仍然远未成为主流。大多数Web开发者都可以访问关系数据库,而几乎没有其他选择。对于这些人,我们能做些什么呢?

本月,我们将研究两种不同的方法,可以处理不太适合关系数据库的数据。这些技术彼此截然不同,甚至无法接近关系数据库可以提供的全部可能性。但是,它们都有效并且已在生产环境中使用——如果您的数据似乎不适合标准数据库范例,您可能需要考虑其中一种方法。

PostgreSQL的表继承

一些数据建模问题通常更难处理。例如,面向对象编程世界的经典入门介绍了一个人力资源部门。人力资源部门跟踪员工,所有员工都有一些共同的特征。但是,有些员工是程序员,有些是秘书,有些是经理——每种员工类型都有需要与之关联的特定数据。

在面向对象的世界中,这很容易建模。您可以创建一个员工类,然后创建程序员、秘书和经理的多个子类。子类化创建了“是-一个”关系,例如程序员是员工。这意味着程序员具有员工的所有属性,但也具有一些使其与普通员工区分开来的附加特征。有了这些子类,我们就可以创建一个公司人员的数组(或任何其他数据结构),知道尽管有些人是程序员,有些人是秘书,但他们都是员工,可以这样对待。

将这个想法转化为关系数据库的世界可能有点棘手。一种解决方案是在数据库表中使用继承。PostgreSQL多年来一直这样做;因此,它被许多用户称为对象关系数据库。例如,您可以在PostgreSQL中执行以下操作

CREATE TABLE Employees (
    id            SERIAL,
    first_name    TEXT    NOT NULL,
    last_name     TEXT    NOT NULL,
    email_address TEXT    NOT NULL,

    PRIMARY KEY(id),
    UNIQUE(email_address)
);

CREATE TABLE Programmers (
    main_language    TEXT    NOT NULL
) INHERITS(Employees);

CREATE TABLE Secretaries (
    words_per_minute    INTEGER    NOT NULL
) INHERITS(Employees);


INSERT INTO Employees (first_name, last_name, email_address)
    VALUES ('George', 'Washington', 'georgie@whitehouse.gov');

INSERT INTO Programmers (first_name, last_name,
                         email_address, main_language)
    VALUES ('Linus', 'Torvalds', 'torvalds@osdl.org', 'C');

INSERT INTO Secretaries (first_name, last_name,
                         email_address, words_per_minute)
    VALUES ('Condoleezza', 'Rice', 'rice@state.gov', 10);

如果我们请求系统中的所有员工,我们将获得我们输入的所有三个人

atf=# select * from employees;
 id | first_name | last_name  |     email_address
----+------------+------------+------------------------
  1 | George     | Washington | georgie@whitehouse.gov
  2 | Linus      | Torvalds   | torvalds@osdl.org
  3 | Condoleezza| Rice       | rice@state.gov
(3 rows)

当然,此查询仅显示Employees表的列,这些列对于该表以及从该表继承的表是通用的。如果我们想知道某人每分钟打多少字,我们必须专门向Secretaries表发出该查询

atf=# select * from secretaries;
 id | first_name | last_name | email_address  | words_per_minute
----+------------+-----------+----------------+------------------
  3 | Condoleezza| Rice      | rice@state.gov |               10
(1 row)

请注意,所有三个表的id列(定义为SERIAL,即非重复递增整数)在所有三个表中都是唯一的。

多态关联

PostgreSQL将这种类型的对象层级结构集成到其关系系统中的方式令人印象深刻、灵活且有用。然而,由于它是PostgreSQL独有的,这意味着没有更高级别的、数据库无关的应用框架可以支持它。在Ruby on Rails中尤其如此,它试图将所有数据库视为相似或相同的,甚至鼓励程序员使用基于Ruby的领域特定语言(迁移)来创建和修改数据库定义。使用PostgreSQL的继承功能可能会起作用,但使其与Rails兼容需要进行相当多的调整。

此外,Rails已经有一个名为多态关联的功能,它使我们可以像处理单个类的一部分一样处理不同类型的项目。这与对象层级结构不同——我们不能说秘书和程序员都是员工类型。但是,我们可以说秘书和程序员都是可雇用的,并通过该描述将它们视为相似的。

首先,您可能还记得Rails有一些称为关联的东西,它允许我们将一个模型连接到另一个模型。例如,假设每个公司都有一名或多名员工。因此,我们可以创建一些简单的模型。我们可以使用以下命令生成迁移

./script/generate model company name:string
./script/generate model employee first_name:string
    last_name:string email_address:string company_id:integer

然后,我们可以使用以下命令将自动生成的迁移文件转换为实际的数据库表

rake db:migrate

现在,我们可以通过修改模型文件来指示每个公司可以拥有一名或多名员工。例如,我们将以下内容添加到employee.rb

class Company < ActiveRecord::Base
  has_many :employees
end

同样,我们可以说

class Employee < ActiveRecord::Base
  belongs_to :company
end

有了has_many和belongs_to,我们现在就在这两个模型之间创建了“关联”。这似乎并不太令人兴奋,但这意味着我们可以将这两个表视为对象类,并将表中的每一行视为一个实例

xyz = Company.create(:name => 'XYZ Corporation')

george = Employee.create(:first_name => 'George',
    :last_name => 'Washington',
    :email_address => 'georgie@whitehouse.gov',
    :company_id => xyz.id)

现在,我们可以说

p xyz.employees.first

我们得到了我们的george用户。同样,我们可以说

p george.company

并得到我们的xyz公司。这对于Rails程序员来说都是标准的东西,并且它是ActiveRecord功能(称为关联)的一部分。您可以创建各种关联,并为其指定任意名称。例如,我们可以说

class Company < ActiveRecord::Base
  has_many :employees
  has_many :employees_with_a, :class_name => 'Employee',
            :conditions => "first_name ilike '%a%'"
end

有了这个,并在重启控制台后(或输入reload!),我们现在可以说

xyz = Company.find_by_name('XYZ Corporation')

xyz.employees_with_a

这会打印空列表——考虑到我们目前只定义了一个员工,并且他的名字不包含字母a,这并不奇怪。但是,现在我们可以创建第二个员工

jane = Employee.create(:first_name => 'Jane',
                       :last_name => 'Austin',
                       :email_address => 'jane@bookauthor.com',
                       :company_id => xyz.id)

如果我们再次运行我们的关联

xyz.employees_with_a

现在我们得到了我们的jane员工。

这一切都很好,但是如果我们想表示不同类型的员工,每种员工都受雇于一家公司,但具有不同的关联数据,会发生什么情况?这就是多态关联变得有用的地方。为了使此功能正常工作,我们需要更改模型的定义以及模型之间的关系(如果您在家中尝试,请在继续之前删除现有的Employee和Company模型)

./script/generate model company name:string
./script/generate model contract employable_id:integer
 employable_type:string company_id:integer
./script/generate model programmer main_language:string
 first_name:string last_name:string email_address:string
./script/generate model secretary words_per_minute:integer
 first_name:string last_name:string email_address:string

以上script/generate调用创建了四个不同的模型:一个用于公司,另一个用于程序员,另一个用于秘书,第四个用于合同。我们的PostgreSQL模型允许我们拥有一个Employee表,并让程序员和秘书从该表继承。Rails不允许我们指定一个模型从另一个模型继承。相反,我们使用Rails来描述模型之间的关系。公司通过雇佣合同与程序员和秘书联系起来。

由于我们正在查看独立模型之间的关系,而不是继承层级结构,因此没有明显的好地方可以放置程序员和秘书共有的属性。最后,我决定将属性分别放在程序员和秘书模型中,尽管存在重复。

现在,让我们定义关联

class Company < ActiveRecord::Base
  has_many :contracts
end

class Contract < ActiveRecord::Base
  belongs_to :company
  belongs_to :employable, :polymorphic => true
end

class Programmer < ActiveRecord::Base
  has_many :contracts, :as => :employable
  has_many :companies, :through => :contracts
end

class Secretary < ActiveRecord::Base
  has_many :contracts, :as => :employable
  has_many :companies, :through => :contracts
end

换句话说,每家公司都有许多合同。每份合同都将一家公司和可雇用的人员联系在一起。谁是可雇用的?目前,只有程序员和秘书符合要求,通过合同连接到可雇用的接口,然后通过合同连接到公司。

在幕后,Rails正在玩一个令人讨厌的把戏,这应该会让任何优秀的数据库程序员感到恶心。合同模型包括两个字段(employable_id和employable_type),它们指向特定表中的单行。在某些方面,这有点像贫民窟的外键。但区别在于外键可以指向多个表中的任何一个。当然,没有错误检查;只有应用程序可以阻止我在employable_type列中输入随机文本字符串。

所以,现在我们可以创建一些关系

xyz = Company.create(:name => 'XYZ Corporation')

p1 = Programmer.create(:first_name => 'Linus',
                       :last_name => 'Torvalds',
                       :email_address => 'torvalds@osdl.org',
                       :main_language => 'C')

Contract.create(:employable => p1, :company => xyz)

s1 = Secretary.create(:first_name => 'Condoleezza',
                      :last_name => 'Rice',
                      :email_address => 'rice@state.gov',
                      :words_per_minute => 90)

Contract.create(:employable => s1, :company => xyz)

这已经非常了不起了。因为程序员和秘书都是可雇用的(因为他们都向合同模型公开了可雇用的接口,使用has_many :as),我们可以将他们每个人都连接到合同模型的实例。

但是,如果我们添加更多关联,情况会变得更好

class Contract < ActiveRecord::Base
  belongs_to :company
  belongs_to :employable, :polymorphic => true

  belongs_to :programmer,
    :class_name => 'Programmer', :foreign_key => 'employable_id'
  belongs_to :secretary,
    :class_name => 'Secretary', :foreign_key => 'employable_id'
end

class Company < ActiveRecord::Base
  has_many :contracts

  has_many :programmers, :through => :contracts,
           :source => :programmer,
           :conditions => "contracts.employable_type = 'Programmer' "

  has_many :secretaries, :through => :contracts,
           :source => :secretary,
           :conditions => "contracts.employable_type = 'Secretary' "

end

有了这个,我们现在就在程序员和秘书一方与公司另一方之间建立了完整的双向关联。因此,我们可以说

>> xyz.programmers
=> [#<Programmer id: 1, main_language: "C", first_name: "Linus",
last_name: "Torvalds", email_address: "torvalds@osdl.org", created_at:
"2008-06-12 00:47:58", updated_at: "2008-06-12 00:47:58">]

>> xyz.secretaries
=> [#<Secretary id: 1, words_per_minute: 90, first_name:
"Condoleezza", last_name: "Rice", email_address: "rice@state.gov",
created_at: "2008-06-12 00:54:34", updated_at: "2008-06-12
00:54:34">]

但是,我们也可以说

>> Programmer.find(1).companies
=> [#<Company id: 1, name: "XYZ Corporation", created_at: "2008-06-12
    00:47:18", updated_at: "2008-06-12 00:47:18">]

此外,我们可以遍历xyz.contracts,将秘书和程序员模型组合到一个包中

>> xyz.contracts.each {|c| puts c.employable.first_name}
Linus
Condoleezza

尽管Rails没有在模型中提供继承,但多态关联使接近这种功能成为可能。您还可以获得一堆便利功能,使使用这些附加属性更加自然。

结论

并非所有数据都能干净地放入二维表中。当这种情况发生时,您可以尝试将数据硬塞进不合适的容器中。或者,您可以尝试使用软件堆栈的一个或多个级别中内置的帮助。如果您使用PostgreSQL,继承可能非常有用。如果您使用Rails,则可以利用多态关联,使您可以将具有通用API的两个或多个模型视为相似。这不是您每天都会做的事情,但对于需要处理不寻常数据的情况,这是一项有用的技能。

资源

要了解PostgreSQL如何允许继承,请阅读在线手册:www.postgresql.org/docs/8.3/static/ddl-inherit.html

Rails Cookbook,作者Rob Orsini,由O'Reilly出版,其中包含有关多态关联的一些有用信息。

Rails Wiki在wiki.rubyonrails.org/rails/pages/UnderstandingPolymorphicAssociations上提供了一些关于多态关联的很好的例子和描述。

Reuven M. Lerner,一位长期从事Web/数据库开发的开发者和顾问,是西北大学学习科学专业的博士候选人,研究在线学习社区。在芝加哥地区生活四年后,他最近(与妻子和三个孩子)返回了他们在以色列莫迪因的家。

加载Disqus评论