本专栏的 নিয়মিত读者不会惊讶地听到我热爱 Ruby on Rails 和 PostgreSQL。Rails 大约八年来一直是我的主要服务器端 Web 开发框架,并且它成功为大量咨询和个人项目提供了解决方案。至于 PostgreSQL,我已经使用它大约 15 年了,并且我仍然对其在那段时间内获得的功能感到惊讶。PostgreSQL 不再仅仅是一个关系数据库。它还是一个平台,支持多种数据类型的存储和检索,构建在坚如磐石、符合 ACID 标准的事务核心之上。

当我开始使用 Ruby on Rails 进行开发时,大多数其他开发人员(包括 37Signals 的核心 Rails 开发人员)都在使用 MySQL。因此,Rails 没有为 PostgreSQL 特定的功能提供任何支持。实际上,我最喜欢的 Rails 功能之一一直是数据库迁移,它允许开发人员逐步更改数据库模式。这种平台独立性的缺点是特殊功能经常被忽略,事实上,为了服务于最低公分母,PostgreSQL 的许多功能被忽略或降级为第三方 gem。

在过去的几年里,PostgreSQL 的受欢迎程度不断提高,无论是在整体上还是在 Rails 社区内。这部分归因于 PostgreSQL 提供的庞大(且不断增长)的功能集。但是,我猜测这也与 Oracle 现在拥有 MySQL 以及流行的 Heroku 托管服务的增长有关。Heroku 是否适合您的应用程序是一个应该根据具体情况做出的决定。然而,Heroku 为小型数据集提供免费层级,并且默认使用 PostgreSQL,这一事实使其成为学习 Rails、小型应用程序以及许多想要外包托管的人的热门选择。

由于 PostgreSQL 日益普及,最新(4.x)版本的 Ruby on Rails 包含了对许多 PostgreSQL 功能的广泛内置支持。在本文中,我将从 Rails 开发人员和 PostgreSQL 管理员及 DBA 的角度介绍许多这些功能。即使您不是 Rails 或 PostgreSQL 用户,我希望这些示例也能让您有机会思考您可以而且应该从数据库中期望多少,而不是从应用程序内部处理它。

UUID 作为主键

数据库开发人员首先学到的事情之一是需要主键,这是一个保证唯一且已索引的字段,可以用来识别完整的记录。这就是为什么许多国家都有身份证号码;使用该号码,政府机构、银行和医疗保健系统可以快速调出您的信息。主键的常用标准是整数,可以在 PostgreSQL 中使用 SERIAL 伪类型定义


CREATE TABLE People (
    id     SERIAL PRIMARY KEY,
    name   TEXT,
    email  TEXT
);

当您在 PostgreSQL 中使用 SERIAL 类型时,实际上会创建一个“序列”对象,您可以在其上调用“nextval”函数。该函数保证为您提供序列中的下一个数字。虽然您可以将其定义为步长大于一,或者在完成时回绕,但最常见的情况是使用序列来递增 ID 计数器。当您要求 PostgreSQL 向您展示此表的定义方式时,您可以看到“id”字段的定义是如何扩展的


\d people
                          Table "public.people"

+--------+---------+--------------------------------------------+
| Column |  Type   |                 Modifiers                  |
+--------+---------+--------------------------------------------+
| id     | integer | not null default 
                     ↪nextval('people_id_seq'::regclass) |
| name   | text    |       |
| email  | text    |       |

+--------+---------+--------------------------------------------+
Indexes:
    "people_pkey" PRIMARY KEY, btree (id)

因此,您可以看到“id”列没有什么特别之处,只是它有一个默认值。如果您在 INSERT 语句中未指定“id”的值,PostgreSQL 将在序列上调用 nextval。这样,您可以确保“id”列始终具有唯一值。

但是,如果您不想使用整数怎么办?我一直偏爱它们,但使用 UUID(通用唯一 ID)是很常见且流行的。UUID 的优点之一是它们(或多或少)保证在计算机之间是唯一的,允许您合并来自多个服务器的记录。如果您使用整数主键执行此操作,您很可能拥有多个 ID 为 5 或 10 的记录。但是使用 UUID,这种情况的可能性要小得多。

从理论上讲,PostgreSQL 一直支持使用 UUID 作为主键。毕竟,您可以只使用文本字段,并让您的应用程序生成并插入 UUID。但这将责任推给了应用程序,这实际上是不合适的。更好的解决方案是使用 PostgreSQL 的 uuid-ossp 扩展,该扩展已随数据库的最新几个版本一起发布。在现代版本的 PostgreSQL 中,您可以发出 SQL 命令


CREATE EXTENSION "uuid-ossp";

请注意,您必须在此处使用双引号,因为标识符中有一个 - 字符。双引号告诉 PostgreSQL 完全按照您编写的方式保留标识符(不要将其与用于文本字符串的单引号混淆)。

另请注意,扩展仅安装在您发出 CREATE EXTENSION 命令的数据库中。因此,如果您将扩展添加到“foo_development”数据库,它不会自动出现在“foo_production”数据库中。为了确保扩展存在于所有数据库中,请将其添加到“template1”,所有新数据库都从中复制而来。

一旦您成功安装了扩展(数据库将通过回显您的命令 CREATE EXTENSION 来确认),您就可以使用它了。与许多 PostgreSQL 扩展一样,uuid-ossp 定义了一种新的数据类型和知道如何使用它的函数。例如,您现在可以调用 uuid_generate_v1() 函数,返回类型为“uuid”的数据


select uuid_generate_v1();
+--------------------------------------+
|           uuid_generate_v1           |
+--------------------------------------+
| 6167603c-276b-11e3-b71f-28cfe91f81e7 |
+--------------------------------------+
(1 row)

如果您想使用 UUID 作为主键,您可以按如下方式重新定义表


CREATE TABLE People (
    id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v1(),
    name TEXT,
    email TEXT
);

如您所见,在这里您已将 SERIAL 类型替换为 UUID 类型(由扩展定义),并指示 PostgreSQL 在未提供 UUID 值时调用 UUID 生成函数。如果您将行插入到此表中,您将看到 UUID 确实已生成


INSERT INTO People (name, email)
VALUES ('Reuven', 'reuven@lerner.co.il');

SELECT * FROM People;
+--------------------------------------+--------+---------------------+
|                  id                  |  name  |        email        |
+--------------------------------------+--------+---------------------+
| 9fc82492-276b-11e3-a814-28cfe91f81e7 | Reuven | reuven@lerner.co.il |
+--------------------------------------+--------+---------------------+

现在,如果您直接在数据库级别工作,那么这一切都很棒。但是 Rails 迁移应该提供一个抽象层,允许您通过 Ruby 方法调用来指定数据库更改。从 Rails 4 开始,这是可能的。我可以使用以下命令创建一个新的 Rails 应用程序


rails new pgfun -d postgresql

这将创建一个新的“pgfun”Rails 应用程序,使用 PostgreSQL 作为后端数据库。然后我在命令行创建一个合适的数据库用户(在该数据库上赋予该用户超级用户权限)


createuser -U postgres -s pgfun

然后我创建开发数据库


createdb -U pgfun pgfun_development

现在您已准备好创建您的第一个迁移。我在此处使用内置的 Rails scaffold 机制,它将为我创建一个迁移(以及控制器、模型和视图)


rails g scaffold person name:text email:text

请注意,我没有指定主键列。这是因为 Rails 通常假设会有一个名为“id”的数字列,其中将包含主键。但是,您将通过打开在 db/migrations 中创建的迁移文件来更改它。默认情况下,迁移如下所示


class CreatePeople < ActiveRecord::Migration
  def change
    create_table :people do |t|
      t.text :name
      t.text :email

      t.timestamps
    end
  end
end

通过将附加参数传递给 create_table(在块之前,在第一行中),您可以指示您希望主键为 UUID


class CreatePeople < ActiveRecord::Migration
  def change
    create_table :people, id: :uuid do |t|
      t.text :name
      t.text :email

      t.timestamps
    end
  end
end

这样,您的主键仍然称为“id”,但它的类型将为 UUID。您可以运行迁移以确保


bundle exec rake db:migrate

果然,该表已按您可能喜欢的方式定义


\d people
                              Table "public.people"

+------------+-----------------------------+--------------------------+
|   Column   |            Type             |         Modifiers        |
+------------+-----------------------------+--------------------------+
| id         | uuid                        | not null default 
                                             ↪uuid_generate_v4() |
| name       | text                        |               |
| email      | text                        |               |
| created_at | timestamp without time zone |               |
| updated_at | timestamp without time zone |               |

+------------+-----------------------------+--------------------------+
Indexes:
    "people_pkey" PRIMARY KEY, btree (id)

但是,如果您仔细观察,您会发现 Rails 迁移生成的默认值与之前手动生成的默认值之间存在差异。区别在于用于生成 UUID 的函数——在手动版本中,您生成了一个“版本 1”UUID,该 UUID 基于创建它的计算机的 MAC 地址。相比之下,Rails 使用“版本 4”UUID 算法,该算法是完全随机的。v4 UUID 的优点是该数字更随机,从而降低了某人可以猜测它的机会。但是,由于数据是随机的,因此 PostgreSQL 对其进行索引的速度会较慢。如果您想告诉 Rails 使用 v1 函数,请在迁移中添加一行


class CreatePeople < ActiveRecord::Migration
  def change
    create_table :people, id: false do |t|
      t.primary_key :id, :uuid, default: 'uuid_generate_v1()'
      t.text :name
      t.text :email

      t.timestamps
    end
  end
end

请注意,如果您想运行修改后的迁移,那么最简单和最好的方法可能是删除并重新创建“people”和“schema_migrations”表。Rails 会记住哪些迁移已经应用,即使您修改了文件,它也不会重新运行迁移


\d people
                              Table "public.people"

+------------+-----------------------------+--------------------------+
|   Column   |            Type             |              Modifiers   |
+------------+-----------------------------+--------------------------+
| id         | uuid                        | not null default
                                              ↪uuid_generate_v1() |
| name       | text                        |               |
| email      | text                        |               |
| created_at | timestamp without time zone |               |
| updated_at | timestamp without time zone |               |
+------------+-----------------------------+--------------------------+
Indexes:
    "people_pkey" PRIMARY KEY, btree (id)

有了这个默认设置,您的“people”表现在将使用 UUID。

数组

数组是 Rails 现在原生支持的另一个 PostgreSQL 功能。PostgreSQL 多年来一直支持数组,虽然我个人觉得语法有点难以处理,但毫无疑问,数组可以简化一些数据库设计。(但我应该注意到,数组应该是最后的手段,因为它们往往会导致非规范化的数据库设计,可能导致不必要的数据重复。)例如,如果我想为我的博客创建一个“posts”表,然后允许人们存储一个或多个社交标签,我可以将其定义为


create table posts (
    id       UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v1(),
    headline TEXT,
    body     TEXT,
    tags     TEXT[]
);

请注意与“tags”列关联的数据类型。通过在 TEXT 类型后使用方括号,我已指示该列可以包含零个或多个文本字符串。例如


INSERT INTO Posts (headline, body, tags)
VALUES ('my headline', 'my body', '{general, testing}');

请注意,数组值作为字符串插入,其第一个和最后一个字符是大括号。现在,您可以使用方括号从数组中获取信息,记住与许多语言不同,PostgreSQL 从 1 开始索引数组


SELECT headline, body, tags[1], tags[2] FROM Posts;
+-------------+---------+---------+---------+
|  headline   |  body   |  tags   |  tags   |
+-------------+---------+---------+---------+
| my headline | my body | general | testing |
+-------------+---------+---------+---------+
(1 row)

请注意,您可以如何通过使用索引分别检索每个标签元素。如果您尝试使用没有值的索引,您将得到 NULL。您还可以使用 ANY 运算符来查找分配了特定标签值的行


select headline, body, tags from posts where 'general' = ANY(tags);
+-------------+---------+-------------------+
|  headline   |  body   |       tags        |
+-------------+---------+-------------------+
| my headline | my body | {general,testing} |
+-------------+---------+-------------------+

请注意,ANY 运算符必须位于比较的右侧。否则,您将收到来自 PostgreSQL 的语法错误。

在早期版本的 Ruby on Rails 中,对 PostgreSQL 数组几乎没有或根本没有支持。但是从 Rails 4 开始,就支持这种功能了。您不仅可以定义一个列来包含数组,还可以使用 ActiveRecord 来操作它。首先,让我们为资源创建一个 scaffold


rails g scaffold post headline:text body:text tags:string

这将生成必要的文件。但是,暂时不要运行迁移;您首先需要将“tags”从字符串转换为字符串数组,并将您的 ID 转换为 UUID


class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts, id: false do |t|
      t.primary_key :id, :uuid, default: 'uuid_generate_v1()'
      t.text :headline
      t.text :body
      t.string :tags, array:true, default:[]

      t.timestamps
    end
  end
end

现在您将拥有一个 UUID 作为主键,但您还将定义 tags 为数组。运行迁移后,您将看到以下内容


\d posts
                               Table "public.posts"

+------------+-----------------------------+--------------------------+
|   Column   |            Type             |          Modifiers       |
+------------+-----------------------------+--------------------------+
| id         | uuid                        | not null default
                                             ↪uuid_generate_v1() |
| headline   | text                        |               |
| body       | text                        |               |
| tags       | character varying(255)[]    | default '{}'::character
                                             ↪varying[]   |
| created_at | timestamp without time zone |               |
| updated_at | timestamp without time zone |               |
+------------+-----------------------------+--------------------------+
Indexes:
    "posts_pkey" PRIMARY KEY, btree (id)

从数据库的角度来看,一切似乎都很棒;您可以执行 INSERT


INSERT INTO Posts (headline, body, tags, created_at, updated_at)
VALUES ('my headline', 'my body', '{general, testing}', now(), now());

果然,您可以在数据库中看到该帖子。然而,神奇之处在于 ActiveRecord 允许您将 PostgreSQL 数组视为 Ruby 数组。例如,您可以说


Post.first.tags.each {|t| puts t}

这告诉 Rails 请求 ActiveRecord 获取 Posts 表中的第一条记录,并调用其“tags”列,该列作为字符串的 Ruby 数组返回。然后,您可以迭代这些字符串,打印它们(或以其他方式操作它们)。虽然这不是很有效或明智,但您也可以执行以下操作


Post.all.select {|p| p.tags.member?('foo')}

更有效的方法是使用您之前看到的 ANY 运算符,以字符串形式传递给 PostgreSQL


Post.where("'general' = ANY(tags)").first

不幸的是,似乎无法使用标准 Ruby <<(追加)运算符向 PostgreSQL 数组添加元素。相反,如果您想通过 ActiveRecord 向数组添加一个或多个元素,则必须手动执行此操作


p.update_attributes(tags: ['general', 'testing', 'zzz'])

这有点烦人,但并非致命,尤其对于包含此功能的第一个版本而言。

总结

Rails 4 虽然不如 Rails 3 那样打破与前代的兼容性,但确实引入了大量新功能。对我来说,此功能最有趣的领域之一是转向 PostgreSQL,ActiveRecord 迁移和功能放弃了其某些平台独立性。在本文中,我展示了现在可用的两个功能,即 UUID 和数组。但是,还有其他功能,例如对 INET(即 IP 地址)数据类型、JSON(PostgreSQL 9.3 对 JSON 的支持甚至比过去更好)、范围甚至 hstore(一种构建在 PostgreSQL 之上的类似 NoSQL 的存储系统)的本机支持。

没有任何技术(包括 PostgreSQL)在任何时候都适合所有人。但是,以我的经验,PostgreSQL 提供了出色的性能、功能和稳定性,以及一个很棒的社区,可以回答问题并力求正确性。Rails 4 接受了许多这些功能这一事实很可能会让更多人接触到 PostgreSQL,这对于使用开源产品的 Web 和数据库开发人员来说只能是好事。

资源

PostgreSQL 主页位于 https://postgresql.ac.cn。从该站点,您可以下载软件、阅读文档并订阅电子邮件列表。最新版本 9.3 包含大量应该让许多开发人员兴奋的好东西。

Ruby on Rails,在撰写本文时为 4.0.0 版,可在 https://rubyonrails.cn 获得。您可能需要使用 Ruby 版本 2.0,尽管已知版本 1.9.3 可以与 Rails 4 一起使用。您还需要包含“pg”Ruby gem,它连接到 PostgreSQL。有关 Rails 的出色文档可在 https://guides.rubyonrails.cn 的“Guides”系列中找到。

一篇不错的博客文章,从 PostgreSQL 的角度描述了 Rails 4 的许多更新,网址为 http://blog.remarkablelabs.com/2012/12/a-love-affair-with-postgresql-rails-4-countdown-to-2013

Reuven M. Lerner 是一位长期从事 Web 开发的开发人员,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并在 http://lerner.co.il/newsletter 发布程序员免费每周新闻通讯。Reuven 的 Twitter 账号是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论