锻造坊 - 使用 ActiveRecord
在过去的几个月中,我们一直在关注 Ruby on Rails,这是一款热门的新型开源工具包,用于创建 Web/数据库应用程序。正如我们在上一期中看到的那样,该工具包的核心元素之一是 ActiveRecord 类,它在 Ruby 对象和关系数据库中的数据之间自动转换。对象关系映射器(通常被称为这种软件)弥合了面向对象世界和关系世界之间的差距,这两个世界以根本不同的方式处理数据。
本月,我们将研究修改 ActiveRecord 以各种方式验证数据的一些方法。我们还将了解如何处理彼此依赖的类,做一些比基本脚手架更复杂的事情,只需几行简单的代码即可实现。
当我第一次开始使用关系数据库时,我会创建如下所示的表
CREATE TABLE People ( first_name TEXT NOT NULL, last_name TEXT NOT NULL, phone_number TEXT NOT NULL, email_address TEXT NOT NULL );
当然,上面对 People 的定义可以很好地工作,为计算机化的地址簿奠定基础。但是,上面的定义存在几个问题。首先,如果有多个同名的人会发生什么?也就是说,如果我们的数据库中有两个名为 George Washington 的人,我们将遇到严重的问题。我们如何知道我们要找的是哪一个 George?
解决这个问题的方法是为数据库中的每条记录分配一个唯一的数字。每个关系数据库产品都有不同的实现方式。在 PostgreSQL 中,我们添加一个新列并为其分配 SERIAL 类型,表明它应该是一个非重复整数
CREATE TABLE People ( id SERIAL NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, phone_number TEXT NOT NULL, email_address TEXT NOT NULL );
然后,我们告诉 PostgreSQL,它应该将 id 视为不仅仅是另一列,而是主键,这是一个保证唯一的标识符,可以用作表中一行的标识
CREATE TABLE People ( id SERIAL NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, phone_number TEXT NOT NULL, email_address TEXT NOT NULL, PRIMARY KEY(id) );
虽然我们现在可以使用姓名来查找地址簿中的人,但我们也可以使用他们唯一的 ID 来查找。即使我们的数据库中有 100,000 个名为 George Washington 的人,我们也可以使用 id 列明确地找到我们感兴趣的那个人。想想您被要求使用驾驶执照号码、国民身份证号码或社会安全号码来识别自己的次数,您很快就会意识到这些号码都可以用作数据库中的主键。
此约束的另一个结果是数据库为 id 列创建索引。即使您有一个非常大的地址表,id 被索引的事实意味着数据库可以使用它来快速查找记录。此外,尽管可以在 INSERT 语句中手动设置 SERIAL 列(就像 INTEGER 列一样),但通常根本不会显式设置它们。相反,PostgreSQL 分配下一个连续整数作为列值——非常适合主键,其值必须是唯一的。
主键在这种方式中很有用,但我们还没有开始了解它们的力量。这是因为主键真正的威力在于它们使我们能够将表链接在一起。例如,考虑我们可能想要构建为现有地址簿的附加模块的计算机化约会日历。我们可以创建一个如下所示的表
CREATE TABLE Appointments ( id SERIAL NOT NULL, person_id INTEGER NOT NULL, start_at TIMESTAMP NOT NULL, end_at TIMESTAMP NOT NULL, comment TEXT, PRIMARY KEY(id) );
上表有一个 id 列,唯一标识每个约会。它还有两列标识约会的开始和结束时间,以及用于可选注释或描述的空间。
但还有一个 person_id 列,它允许我们指示我们将与谁会面。此数据库设计存在许多问题,但也许最引人注目的是,对于我们可以分配给 person_id 的值,没有约束(除了 NOT NULL)。即使我们的 People 表是空的,我们也可以将 person_id 分配为 10、100 或 996——这些数字在技术上可能是可以接受的,但它们无助于我们确保 person_id 指的是实际的人。
解决方案是将 person_id 定义为外键,表明 person_id 的值只有在反映 People 表中现有值时才是合法的。在 PostgreSQL 中,我们按如下方式完成此操作
CREATE TABLE Appointments ( id SERIAL NOT NULL, person_id INTEGER NOT NULL REFERENCES People, start_at TIMESTAMP NOT NULL, end_at TIMESTAMP NOT NULL, comment TEXT, PRIMARY KEY(id) );
有了这些条件,我们可以确保我们只能与地址簿中的某人预约。如果我们试图绕过它会发生什么?让我们看看
INSERT INTO People (first_name, last_name, phone_number, email_address) VALUES ('George', 'Washington', '202-555-1212', 'first.prez@whitehouse.gov');
当我们 SELECT 数据库表中的元素时,我们可以看到自动分配给 id 列的值
id | first_name | last_name | phone_number | email_address ----+------------+------------+--------------+--------------------------- 1 | George | Washington | 202-555-1212 | first.prez@whitehouse.gov
现在让我们插入一个与 George 的约会
INSERT INTO Appointments (person_id, start_at, end_at, comment) VALUES (1, '2005-Oct-2 18:00', '2005-Oct-2 20:00', 'Dinner');
到目前为止,一切都很好。但是,如果我们尝试插入与不存在的人的约会会发生什么?
INSERT INTO Appointments (person_id, start_at, end_at, comment) VALUES (200, '2005-Nov-2 18:00', '2005-Nov-2 20:00', 'Dinner with no one');
PostgreSQL 拒绝了我们的 INSERT 语句,称插入该行将违反使用 REFERENCES 命令引入的约束
ERROR: insert or update on table "appointments" violates foreign key constraint "appointments_person_id_fkey" DETAIL: Key (person_id)=(200) is not present in table "addressbook".
如果我们尝试在与 George 约会时从 People 表中删除 George 会发生什么?
DELETE FROM People WHERE id = 1;
PostgreSQL 再次拒绝了我们的请求,这次表明我们无法删除正在被指向的项目
ERROR: update or delete on "addressbook" violates foreign key constraint "appointments_person_id_fkey" on "appointments" DETAIL: Key (id)=(1) is still referenced from table "appointments".
到目前为止,我们看到的所有约束都处于数据库级别,而不是任何使用该数据库的应用程序级别。这可能会给那些无法访问数据库定义的用户带来麻烦。毕竟,如果应用程序尝试插入、删除或修改行,从而违反约束,应该会发生什么?
简单的答案,也是在数量惊人的 Web/数据库操作中仍然普遍存在的答案是,程序只是报告错误。(有时它甚至会指示错误是什么,不必要地暴露了所有人都能看到的有问题的 SQL 语句。)在某些情况下,应用程序指示存在数据库问题或类似问题。
但是,我们真正想要的是完全避免这些数据库问题。我们希望数据库中的约束能够以某种方式传播到应用程序级别,让应用程序在问题到达数据库级别之前就捕获问题。
虽然 ActiveRecord 无法做到这一点,但它非常接近,使我们在 Rails 应用程序中表示表之间的关系变得非常简单。现在让我们创建一个简单的 Rails 应用程序,该应用程序使用 ActiveRecord 来跟踪我们的地址簿和日历信息。
我们首先通过键入以下内容来创建 Rails 应用程序的骨架rails addressbook,这将创建一个 addressbook 目录并将所有内容放在该目录下。然后,我们修改 config/database.yml 以指向适当位置的开发、测试和生产数据库。(有关 database.yml 应如何的示例,请参阅上个月的“锻造坊”)。
现在,让我们为 People 和 Appointment 表创建基本模型、控制器和视图。我们可以使用 Rails 自带的 script/generate 程序分别创建它们。但在许多情况下,创建一个简单的应用程序或脚手架是最容易的
ruby script/generate scaffold Person ruby script/generate scaffold Appointment
我们现在可以在端口 3000 上启动测试服务器 (script/server);转到 /People 会显示当前人员列表,并允许我们创建新人员。单击“新建人员”链接,您将看到脚手架创建的页面。但是,并非一切都完美——如果您在不输入任何文本字段的情况下单击页面底部的“创建”按钮,会发生什么?
假设 People 表的定义如前所述,Rails 将创建一个新人员,其字段都是空字符串。我们可以通过修改 People 表的定义来解决该问题,添加检查以确保每个字段的内容都是非空字符串——但如果我们这样做,Rails 将向我们显示数据库错误,抱怨我们违反了完整性约束。
解决方案是修改 Person 对象,使其捕获此类错误,强制用户在每个字段中输入内容。我们通过修改位于 app/models/person.rb 中的 Person 类定义来做到这一点。当我们第一次打开 person.rb 时,我们看到它是一个未更改的 ActiveRecord::Base 子类
class Person < ActiveRecord::Base end
我们可以添加 Rails 内置的验证器之一,这些语句允许我们在应用程序级别检查数据的完整性,然后再将其传递到数据库级别。在这种情况下,我们使用 validates_presence_of,命名我们表中的每个字段
class Person < ActiveRecord::Base validates_presence_of :first_name, :last_name, :email_address, :phone_number end
有了这个功能——甚至无需重启服务器——我们可以尝试添加另一个空白人员。但现在我们发现 Rails 阻止了我们,在表单顶部解释了问题(例如,“电话号码不能为空”),并用红色标出了每个违规字段。有了这个验证器,我们可以确保 People 表中的所有行都将包含有效数据。
当我们转到 /Appointments 添加新约会时,即使在我们单击页面底部的“创建”按钮之前,有些事情看起来也很可疑:我们无法输入我们与之会面的人!这将导致问题,因为单击“创建”按钮很快就会证明这一点;PostgreSQL 返回一个错误,Rails 将其显示给所有人看。显然,我们需要解决这个问题。
问题是,用于创建 Appointment 类新实例的视图(即 app/views/appointments/new.rhtml)缺少一个名为 appointment[person_id] 的 HTML 表单元素。如果 new.rhtml 包含 appointment[person_id],它将与表单的其余元素一起提交并插入到数据库中。
问题是,appointment[person_id] 应该从数据库中填充。假设我们有一个名为 @people 的变量可供我们使用,我们可以将如下内容添加到 new.rhtml 中,就在调用 submit_tag 之前
<b>Person:</b><br /> <select name="appointment[person_id]"> <option value="">Select a person</option> <% @people.each do |person| %> <option value="<%= person.id %>"> <%= person.first_name %> </option> <% end %> </select><br />
上面的 RHTML 代码类似于 JSP 和 ASP,因为它将 Ruby 代码嵌入到 HTML 文档中。由 <% %> 包围的代码就地执行,而由 <%= %> 包围的代码被其返回值替换。
因此,上面的代码定义了一个名为 appointment[person_id] 的 HTML 表单元素。然后,它创建一个带有空白值的选项。接下来,我们进入一个标准的 Ruby 习语,迭代列表的元素,使用 person 作为迭代器,提取 person.id 作为值,person.first_name 作为文本。换句话说,我们创建 People 表中人员的 <select> 列表。
但是 @people 来自哪里?我们必须定义它,但我们可以在 Appointments 控制器对象 app/controllers/appointments_controller.rb 中执行此操作。该文件包含脚手架系统为我们创建的所有方法。我们只需要向 new 方法定义添加一行
@people = Person.find_all
现在,我们知道 @people 是我们正在定义的变量,并且我们知道 Person 是 ActiveRecord::Base 的子类,它将我们连接到数据库中的 People 表。find_all 方法返回表中的所有元素。
最后,我们修改我们的数据模型类 appointment.rb,添加一个验证器以确保我们将为每个字段提供非空值
class Appointment < ActiveRecord::Base validates_presence_of :start_at, :end_at, :comment, :person_id end
完成所有这些设置后,我们可以开始安排约会。每个约会将与一个人进行,我们可以确保它将包含我们想要的所有数据。此外,我们知道,当 PostgreSQL 接收要插入的数据时,它将是有效的。
虽然数据库中的约束确保数据始终有效,但我们通常希望在应用程序级别执行此类验证。不幸的是,在许多语言中这样做很棘手或耗时。ActiveRecord 是 Ruby on Rails 核心的对象关系映射器,使确保您的用户永远不会看到数据库错误变得相对容易。它附带了许多验证器,以及用于创建自定义验证器的基础结构。此外,它还附带了许多例程,使我们能够描述不同表之间的关系。通过对控制器、视图和模型进行一些小的修改,我们能够快速创建一个具有有效数据的自定义应用程序。
本文资源: /article/8580。
Reuven M. Lerner,一位长期的 Web/数据库顾问和开发人员,现在是西北大学学习科学专业的博士生。他的博客位于 altneuland.lerner.co.il,您可以通过 reuven@lerner.co.il 与他联系。