锻造坊 - Rails 和数据库

作者:Reuven M. Lerner

上个月,我们开始关注 Ruby on Rails,这是一个 Web 开发框架,在短时间内引起了广泛关注。Rails 的成功很大程度上归功于 Web/数据库开发人员可以轻松完成各种任务。事实上,Rails 的粉丝经常吹嘘他们的应用程序几乎没有配置文件,这使得程序员可以专注于开发,而不是后勤。

本月,我们开始研究 Rails 如何与关系数据库协同工作。即使您不会在自己的 Web 开发工作中使用 Rails,Rails 解决许多不同问题的方式也非常优雅,并且很可能会影响未来几代面向对象关系的技术。

问题

Rails 的数据库方面试图解决一个看似简单的问题。Web 应用程序应该在哪里以及如何存储持久信息?我们可能想要构建的几乎任何 Web 应用程序,从购物车到日历/日记,都需要在某处存储其信息。而且由于 Web 应用程序在服务器上而不是在用户的桌面上运行,我们需要跟踪许多不同用户的数据,而不仅仅是一个用户的数据。

早在 Web 开发的旧时代,那时应用程序远没有现在复杂,我们中的一些人使用了基本的文本文件。但我们很快发现,关系数据库在几乎所有层面上都是一种改进。关系数据库旨在提供快速、安全和灵活地访问我们想要的数据——只要我们可以将数据表示为二维表格。

但正如最后一个句子听起来那么简单,将数据从程序移动到数据库既不简单也不直接。当然,简单的事情确实非常简单;跟踪客户的银行余额,甚至他们支票簿中的最新交易都不是什么大问题。但是,编程世界中日益重要的对象与数据库世界中重要的表格之间存在很大差异。考虑一下数据库程序员在表示任意深度的层次结构时所经历的曲折,您就会开始理解对象和表格之间的映射可能非常复杂。

基本上有三种方法可以弥合对象和表格之间的差距:手动处理、用对象替换表格以及使用自动映射工具。手动方法可能是最常见和最流行的方法,它仅仅意味着程序员将 SQL 查询插入到代码中。为了获取购物车的内容,我们执行类似这样的 Perl 代码

# Send the shopping-cart query
my $sql = "SELECT item_id, item_name,
                  item_price, item_quantity
             FROM ShoppingCart
            WHERE user_id = ?";
my $sth = $dbh->prepare($sql);
$sth->execute($user_id);

my $total_cost;

print "<table>
              <th>Name</th>
              <th>Price</th>
              <th>Quantity</th>\n";

# Iterate over the elements of the shopping cart
while (my $rowref = $sth->fetchrow_arrayref())
{
    my ($item_id, $item_name, $item_price,
            $item_quantity) = @$rowref;

    $total_cost += $item_price * $item_quantity;

    print "<tr><td>$item_name</td>
               <td>$item_price</td>
               <td>$item_quantity</td></tr>\n";
}

print "<tr><td>Total cost:</td>
           <td>$total_cost</td></tr>
       </table>\n";

最初几次编写这样的代码时,感觉还不错。但过了一段时间,它开始让你感到厌烦。当您只想获取购物车中的元素时,为什么要编写这么多 SQL?即使您将 SQL 包装在对象内部,您也会发现自己在项目的过程中创建了许多这样的对象。

编写 Zope(一个基于 Python 的 Web 应用程序框架)的人员认为,虽然关系数据库有其用武之地,但解决此问题的真正方法是尽可能避免对象-表格转换,而是选择对象数据库。因此,ZODB(Zope 对象数据库)允许您存储和检索 Python 对象作为层次结构的一部分。如果您可以用 Python 对象表示数据,ZODB 可以轻松地持久保存该数据。

但当然,ZODB 也有其自身的问题。首先,您只能从 Python 中使用它;相比之下,关系数据库通常可以从多种语言访问。尽管 ZODB 现在具有多版本并发控制 (MVCC)、事务和许多其他功能,但它只是存储一组对象的事实意味着您无法轻松地排序、搜索或执行“连接”,而这些是关系世界的基石。

对象-关系映射器

第三种选择,即拥有对象-关系映射器,变得越来越流行。基本思想非常简单。您的程序使用对象,这些对象会自动转换为关系数据库中的行、列和表格。

多年来,对象-关系映射器遇到了各种各样的困难,尤其是在处理复杂数据集时。但它们现在变得越来越健壮和令人印象深刻;虽然我没有使用过 Hibernate(适用于 Java 程序员)和 SQLObject(适用于 Python 程序员),但它们确实提供了此类服务,而 Alzabo(在本专栏几年前描述过)为 Perl 程序员提供了此类服务。当正确实施时,对象-关系映射器提供了两全其美的优势,包括关系数据库的所有速度、跨语言和维护优势,以及在代码中处理对象的灵活性和一致性。

大约一年前,当 Rails 突然出现在 Web 开发领域时,其支持者吹捧 Rails 允许您以几乎零配置和极少代码生成 Web/数据库应用程序。事实上,情况确实如此,这要归功于几个不同的功能。然而,使这成为可能的关键功能之一是称为 ActiveRecord 的复杂对象-关系映射器。

ActiveRecord 是一个 Ruby 类,传统上用作 Rails 应用程序中模型类的父类。您可能还记得,Rails 使用传统的模型-视图-控制器 (MVC) 范例来构建 Web 应用程序。与某些 MVC 应用程序框架不同,Rails 使这些差异变得明确,在应用程序的 app 目录中创建模型、视图和控制器子目录。Rails 中的模型类不必从 ActiveRecord 继承,在这种情况下,它的功能类似于任何其他数据结构或类。但如果它确实从 ActiveRecord 继承(更准确地说,是从 ActiveRecord::Base 继承),则该对象知道如何从关系数据库的表格中存储和检索其值。

此时,您可能会问:“等一下——仅仅继承怎么可能提供对象-关系映射?我不需要配置任何东西吗?” 简短的回答,令人惊讶的是,“不”。当然,存在一个小的权衡,如果您不小心,可能会伤到您的自尊心。Rails 能够实现这种魔力,是因为它强制所有程序都遵守一组特定的约定。事实上,“约定优于配置”是 Rails 的口头禅之一。如果您愿意按照公认的约定命名表格、列和对象,Rails 将会给予您丰厚的回报。如果您坚持使用自己的约定,或者您想将 Rails 连接到现有的一组表格,您可能会发现自己即使是实现最简单的应用程序也很困难。

连接

那么,我们如何将 Rails 连接到我们的数据库呢?我看到的大部分文档都使用流行的开源 MySQL 数据库作为示例;我强烈偏爱 PostgreSQL,因此在我的示例中使用它。但是,您很快就会看到,在 Rails 中,后端数据库的选择几乎是不可见的。

如果您尚未这样做,请安装 Ruby Gems 包,然后使用 gem 命令安装 Rails、其所有依赖类和 postgres-pr

$ gem install --remote rails
$ gem install --remote postgres-pr

现在我们使用 rails 命令创建一个新的 Rails 应用程序。如果您仍然没有上个月开始的 Weblog 应用程序,您可以通过键入以下内容来创建它

$ rails blog

在许多 Web/数据库框架中,每个页面或程序都必须每次连接到数据库。在 Rails 中,底层系统为我们连接到数据库,自动将数据库连接与 ActiveRecord 对象类绑定在一起。配置保存在应用程序目录下的 config/database.yml 中。不,这不是拼写错误;扩展名是 yml (YAML,或 Yet Another Markup Language,或 YAML Ain't a Markup Language),这是一种简化的文本格式,比 XML 更易于阅读、编写和解析。

传统上,每个 Rails 应用程序使用三个不同的数据库,每个数据库分别用于开发、测试和生产。这三个数据库的前缀都反映了应用程序名称,后缀反映了其用途(开发、测试或生产)。例如,这是博客应用程序的 database.yml 文件

development:
  adapter: postgresql
  database: blog_development
  host: localhost
  username: blog
  password:

test:
  adapter: postgresql
  database: blog_test
  host: localhost
  username: blog
  password:

production:
  adapter: postgresql
  database: blog_production
  host: localhost
  username: blog
  password:

请注意,数据库适配器名称是 postgresql,即使我使用了 postgres-pr gem 连接到它。另请注意,数据库由名为 blog 的用户访问。为了使其正常工作,我现在必须在 PostgreSQL 中创建 blog 用户(不是作为 Linux 用户)

$ /usr/local/pgsql/bin/createuser -U postgres blog
Shall the new user be allowed to create databases? (y/n) y
Shall the new user be allowed to create more new users? (y/n) n
CREATE USER

现在我们已经创建了 blog 用户,我们使用它来创建三个数据库

$ /usr/local/pgsql/bin/createdb -U blog blog_development
CREATE DATABASE
$ /usr/local/pgsql/bin/createdb -U blog blog_test
CREATE DATABASE
$ /usr/local/pgsql/bin/createdb -U blog blog_production
CREATE DATABASE

最后,我们应该在数据库中创建一个表格。我们现在只使用开发数据库,但我们遵循约定,将表格定义写入 blog/db 目录中的名为 create.sql 的文件中

CREATE TABLE Blogs (
id           SERIAL   NOT NULL,
title        TEXT     NOT NULL,
contents     TEXT     NOT NULL,

 PRIMARY KEY(id)
);

我已经提到了在使用 ActiveRecord 对象-关系映射器时遵循 Rails 约定的重要性,而上面的表格定义(尽管看起来很简单)已经揭示了其中的两个约定。首先,每一行都有一个名为 id 的唯一 ID 字段。(PostgreSQL 默认情况下遵循 SQL 标准,表格和列名称不区分大小写。)在 PostgreSQL 中,我们通过将其声明为 SERIAL 类型来确保每一行都具有唯一的 id 值。如果您像我一样,一直使用更明确的名称(例如,blog_id)作为主键,那么您需要更改才能与 Rails 一起使用。

另一个约定,也是一个更难注意到的约定是,我们的表格名称是 Blogs,一个复数词。从 ActiveRecord::Base 派生的类会自动映射到数据库表格,表格名称相同,但复数形式。因此,如果我们在 models/blog.rb 中创建一个从 ActiveRecord::Base 继承的 blog 类,它会自动映射到数据库中的 blogs 表格。如您所见,您选择的名称会影响代码的可读性;请务必选择在多种不同上下文中都有意义的名称,包括单数和复数。(在这种情况下,我选择的词语确实不太合适,因为 Blogs 表格的每一行都代表一个帖子,而不是一个 Weblog。)

但情况会变得更好——我们不需要自己创建 blog.rb,至少最初不需要。我们可以要求 Rails 使用 script/generate 为我们创建它。script/generate 可用于创建模型、控制器或视图;在这种情况下,我们创建我们的模型

ruby script/generate model blog

您将看到一些如下所示的输出

exists  app/models/
exists  test/unit/
exists  test/fixtures/
create  app/models/blog.rb
create  test/unit/blog_test.rb
create  test/fixtures/blogs.yml

如果我们打开 app/models/blog.rb,我们会看到它几乎是空的

class Blog < ActiveRecord::Base
end

尽管我们可以(并且将会)向我们的 Blog 类添加新方法,但实际上我们可以将其保持原样。这是因为 ActiveRecord 为我们的类提供了足够的骨架方法,我们可以不用它们也能应付。

虽然我们现在有了一个自动映射到数据库中 Blogs 表格的 Ruby 类,但这很不错,但我们仍然需要通过 Web 访问我们的表格。这意味着我们需要创建一个控制器类,因为控制器(MVC 中的 C)是 Rails 中处理传入 HTTP 请求的组件。我们可以自动生成一个控制器

ruby script/generate controller blogadmin

不幸的是,此控制器与我们的类完全无关。尽管我们可以自己建立这样的连接,但事实上我们正处于应用程序定义的最初阶段,这意味着我们可以采取一些捷径,要求 Rails 生成一整套脚手架或基本类,这将完成我们想要的许多事情。创建这样的脚手架是快速启动 Rails 开发甚至用于处理新项目的好方法。与此同时,生成脚手架意味着清除您已经编写的类定义。因为(到目前为止)我们只使用了默认类,所以这应该不是什么大问题。

我们使用以下命令生成脚手架应用程序

ruby script/generate scaffolding Blog Admin

(您应该回答“Y”或“a”来替换一个或所有现有文件,视情况而定。)

这将创建一个名为 Admin 的控制器类,该类为我们提供对 Blog 类的基本访问权限。后者随后连接到数据库中的 Blogs 表格。

仅在脚手架就位的情况下,我们现在可以启动服务器

ruby script/server

然后,我们将浏览器指向应用程序,地址为 /admin URL:http://localhost:3000/admin。

果然,我们看到——除了允许我们向 Blogs 表格添加新条目的几个链接之外,什么也没有。如果您单击添加,您现在将看到一个表单,允许您创建新的 Weblog 条目。这些自动生成的页面位于 app/views 子目录中。特别是,查看 app/views/admin 中的 new.rhtml 和 list.rhtml。当然,您可以更改这些视图——在生产应用程序中,您将会这样做。但是,对于开始接触 Rails,或者只是尝试一个应用程序想法,这确实非常有用。

现在,当您转到添加页面时,您可能会惊讶地发现 Blogs 表格中的每一列都有一个字段,但 id 除外。这是自动生成的脚手架代码的一些聪明之处的结果;它查看了表格定义,并决定要显示哪种类型的输入区域。如果我们向 Blogs 表格添加另一列来表示 Weblog 条目的添加时间会发生什么?(毕竟,内容未按日期顺序排序的 Weblog 不会很有用。)

为了节省时间,我们只需进入并修改我们的表格定义,使用 ALTER TABLE 命令

$ psql -U blog blog
% ALTER TABLE Blogs ADD COLUMN posted_at
        TIMESTAMP NOT NULL DEFAULT NOW();

如果您查看表格定义(使用 psql 客户端程序中的 \d 命令),您会看到它现在有一个名为 posted_at 的新列。Rails 中的命名约定扩展到列的名称;DATE 类型的列应命名为 xxx_on,TIMESTAMP 类型的列(即日期和时间)应命名为 xxx_at。

我们现在需要重新生成我们的脚手架代码,清除可能存在的任何先前版本(在这种特定情况下是可以接受的)

ruby script/generate scaffolding Blog Admin

接下来,重新启动服务器并返回到新的博客页面。您将看到它已更改,因此现在包含一个发布于字段。此外,您不能在那里输入任意文本;一个完整的日期输入选择列表已就位。如果您曾经编写过代码来处理 Web 应用程序中日期的输入,那么仅这一点就应该是一个令人愉快的改变。

最后,花一些时间探索应用程序(使用您的 Web 浏览器)以及在您添加、修改和删除行时数据库中发生的更新。甚至没有编写一行 Ruby 代码,您应该会发现自己能够使用基于 Web 的表单来修改数据库。如果您想冒险一点,您甚至可以修改 list.rhtml,它会向您显示当前的博客条目列表。

结论

许多 Web/数据库框架都在努力提供一个持久存储层,该层可以与编程语言本身干净地接口。嵌入式 SQL 代码在小规模上还不错,但即使是中等规模的应用程序也可能导致在其他面向对象的应用程序中间出现大量 SQL 查询。Rails 解决方案在两者之间取得了平衡,我发现这种平衡非常令人满意,它迫使我对我进行非常小的、逻辑上的更改,以换取大量的时间节省。

当然,当您只需要担心列类型和单个表格时,创建对象-关系映射器并不难。此外,您很快就会发现,就目前而言,我们简单的博客应用程序存在几个问题。首先,它有一个管理界面,但没有向外界显示博客的方法!此外,它不以任何时间顺序显示博客条目。下个月,我们将看到如何解决这些问题,以及 Rails 如何通过模型定义中的几行简单代码来强制执行数据完整性。

本文的资源: /article/8526

Reuven M. Lerner,一位长期的 Web/数据库顾问和开发人员,现在是西北大学学习科学项目的研究生。他的 Weblog 位于 altneuland.lerner.co.il,您可以通过 reuven@lerner.co.il 与他联系。

加载 Disqus 评论