使用 Alzabo 进行数据建模

作者:Reuven M. Lerner

在过去的几个月中,我们从不同的角度研究了服务器端 Java 编程。从 Servlet 到 JSP 再到 Enhydra 应用服务器,我们已经看到了几种不同的方法,可以使用开源 Java 技术创建动态的、数据库驱动的网站。

我最初计划本月继续沿着这个方向进行,研究 Enhydra 引人入胜的 DODS 对象关系建模软件。DODS 为关系数据库中的表提供了一个高级 Java 抽象层。DODS 方法自动转换为适当的 SQL,然后传递给数据库。结果是:您看到的是 Java 对象和方法,您的数据库看到的是表和 SQL,每个人都很高兴。

不幸的是,Lutris(Enhydra 的公司支持者)的员工所表达的帮助和善意,也无法与以色列海关和我们当地的 FedEx 分支机构相提并论。当我写这篇文章时,包含 DODS 附加说明的 CD 和书籍正放在仓库里,这迫使我暂时偏离了最初的计划。

然而,为本月文章调查 DODS 重新燃起了我对对象关系映射主题的兴趣。我见过的用于此目的的最有趣和易于使用的工具之一是 Alzabo,这是一组 Perl 模块,允许服务器端 Perl 程序员将他们的关系数据库模式包装在对象内部。(该项目以吉恩·沃尔夫科幻作品中的一种生物命名。)我对我所看到的印象深刻,并且相信许多 Perl 程序员也会同样高兴地发现如此强大的工具。

问题

程序员通过使用对象获得了许多好处,从可重用到继承再到封装。但是,尽管程序员已经大量采用面向对象编程,但对象数据库由于各种原因而不太受欢迎。相反,关系数据库在过去几年中变得越来越流行,其中放置了大量数据。那么,问题是我们如何在将数据存储为表的同时,将数据建模为对象。

一种可能性是将每个表建模为一个类,将每个表列建模为实例变量,并将每个表行建模为该类的实例。但是,任何尝试过这种方法的人都会很快发现,说起来容易做起来难,尤其是在创建 Web 应用程序时——我们如何连接两个表?当两个程序修改内存中的同一行,并且稍后才将这些更改提交到数据库时会发生什么?我们如何确保对类定义的更改反映在数据库中,反之亦然?

另一种可能性是将整个表读取到对象实例中,修改对象,并在调用特定方法时将其写出。这对于小表来说效果很好,但是当您的表变得有几兆字节(或千兆字节或太字节)大小时会发生什么?您的老板可能愿意为 Web 服务器购买更多内存,但前提是您没有浪费所有内存来读取整个表!此外,在对象内部建模表意味着您还必须创建一个像样的锁定机制,包括提交和回滚——大多数程序员都没有能力做到这一点。

当处理小型应用程序时,我们可以轻松地忽略这些问题。但是,随着应用程序和数据库规模的扩大,我们希望确保事情会按预期工作。在创建对象关系映射系统(如 Alzabo)时尤其如此。我和我的一位员工去年创建了一个简单的对象关系映射中间件层,并且对我们所做的工作感到非常满意——直到我们发现我们没有考虑到几乎所有边缘情况,最终导致了大量的异常和默认值。

对于我们 Perl 程序员来说幸运的是,Dave Rolsky 花时间坐下来梳理了所有这些问题,以及许多其他问题。Alzabo 为我们提供了一个面向对象的中间件层,消除了我们直接与数据库交互的需求。

但是 Alzabo 不仅仅是为您的数据库提供高级接口。它还为您提供了一种以编程方式修改数据库模式定义的方法,包括一个基于浏览器的表创建和维护工具,可以自动为您创建 SQL。此外,Alzabo 可以接受现有的数据库并对其进行逆向工程,使您可以在现有数据库和新数据库中使用 Alzabo。

安装 Alzabo

像大多数 Perl 模块一样,Alzabo 可以从 CPAN 下载。但是,安装 Alzabo 可能比其他模块更复杂,仅仅是因为 Alzabo 依赖于许多模块。Alzabo 不仅需要使用 DBI(用于数据库访问)和 DBD::mysql 或 DBD::Pg(用于 PostgreSQL),而且基于浏览器的模式创建工具还使用 HTML::Mason,而 HTML::Mason 又需要 mod_perl。如果所有这些都安装在您的系统上,那么安装 Alzabo 应该相对简单。

我成功安装了 Alzabo,没有遇到太多困难,我使用 CPAN 模块自动下载并安装了每个先决条件,然后安装了 Alzabo 本身。

我接受了软件配置和安装期间提出的几乎所有问题的默认值,除了 Alzabo 假设您用于 Mason 组件的 .mhtml 后缀。我通常给 Mason 组件简单的 .html 后缀;因为我的 Apache 配置不知道如何处理 .mhtml 扩展名,所以它将它们作为 Content-type text/plain 发送,在我的浏览器窗口中显示 Mason 组件的源代码。将已安装的 Mason 组件的后缀更改为 .html 在我的计算机上有效,但我本可以同样轻松地修改我的 Mason 或 Apache 配置。

Alzabo 在其自己的目录中跟踪每个模式,默认情况下称为 /usr/local/alzabo。在此目录内部是一个 schemata 目录,每个数据库模式都有一个子目录,Alzabo 正在建模这些模式。例如,appointments 模式将在 /usr/local/alzabo/schemas/appointments 中。

我的 Alzabo 安装中有两个小问题需要修复。首先,我必须更改 /usr/local/alzabo 的权限,以便我的 Web 用户可以读取和写入它。其次,我必须修改我的 PostgreSQL 启动脚本以包含 -i 选项,以便客户端可以通过网络连接。默认情况下,大多数 PostgreSQL 安装(包括 RPM 版本)都不会启用 -i,这意味着即使 pg_hba.conf(PostgreSQL 主机访问控制文件)中最宽松的配置也无法工作。虽然通常可以在不使用 UNIX 套接字的网络的情况下连接到 PostgreSQL,但 Alzabo 始终指定主机名,这反过来又需要网络连接,即使在本地计算机上也是如此。

要安装基于 Web 的模式生成器,Apache 服务器下的至少一个目录必须由 HTML::Mason 控制。Alzabo 安装脚本将在那里创建一个 new/alzabo 子目录,以及 Mason 组件,这些组件创建和修改您创建的模式定义。例如,我的工作站将其所有 Mason 组件都放在 /usr/local/apache/mason 中,该目录映射到以 /mason 开头的 URL。因此,我的 Alzabo Web 部分安装在 /usr/local/apache/mason/alzabo 中,可以通过 URL /mason/alzabo 访问。如果您尚未这样做,您可能希望告诉 Apache(通过 DirectoryIndex 指令)index.mhtml 是目录的可接受索引页。

编辑模式

现在我们已经安装了 Alzabo,让我们使用基于浏览器的设计工具创建一个简单的数据库模式。诚然,这不如商业或客户端工具那样流畅,但它确实做得很好。

首先创建一个新模式(在 PostgreSQL 和 MySQL 术语中称为数据库),您必须为其命名。该模式必须是 PostgreSQL 或 MySQL 中的合法数据库名称。我选择使用 PostgreSQL,因为它具有内置的引用完整性、外键、视图和触发器,以及更标准的 SQL 方言和以多种语言编写存储过程的能力。

让我们使用 Alzabo 创建一个简单的电话簿和约会日历。我们将跟踪我们认识的人、他们的地址和电话号码,以及我们与他们安排的约会。使用此数据库,我们可以了解在给定日期与我们会面的人,或者了解与给定人员的所有约会。

要创建此模式,我们将 Web 浏览器指向我们前面提到的 Mason 目录下的 URL alzabo/schema(在我的计算机上,我将浏览器指向 https:///mason/alzabo/schema。)这将打开模式创建/编辑页面,我们可以在其中编辑现有模式、创建新模式或逆向工程现有模式。虽然最后一个选项是最有趣的,允许您使用 Alzabo 访问旧数据库,但我们将创建一个新模式。我输入了名称(我选择了 addressbook,因为没有更好的想法),并指示我们希望使用 PostgreSQL 作为我们的后端数据库。

单击“提交”后,出现了几种可能性:我可以向此模式添加新表,删除整个模式或检查 Alzabo 将自动生成的 SQL。当然,现在没有任何 SQL 可以显示。随着时间的推移,我们将看到此 SQL 显着增长。

但是,由于 Alzabo 尚未创建任何 SQL,这并不意味着后端没有完成任何工作。实际上,Alzabo 在 /usr/local/alzabo/schemas 中自动创建了 addressbook 目录,其中包含三个文件:addressbook.create.alz 和 addressbook.runtime.alz(都以二进制格式存储)以及 addressbook.rdbms,其中包含单词 PostgreSQL。通过这种方式,Alzabo 跟踪存储模式的数据库服务器。

进入 addressbook 模式后,我通过在“添加表”文本字段中输入“People”并单击“提交”来添加“People”表。(PostgreSQL 忽略表名和列名的大小写,但我喜欢 Joe Celko 的约定,即表名首字母大写,列名全部小写,SQL 保留字全部大写。)

在我的 People 表中,我创建了列,每列的数据类型都不同。Alzabo 提供了潜在数据类型的菜单,但如果需要,我们可以输入自己的数据类型;这在 PostgreSQL 中尤其有用,PostgreSQL 允许我们创建自己的数据类型。

我通常更喜欢在此类表中使用合成主键,为每一行赋予其自身的值。在 PostgreSQL 中,我们使用 SERIAL 数据类型来实现此目的。但是您会注意到,Alzabo 选择列表中不存在此类数据类型。您可能会想指示这是一个 INTEGER 列,并在列编辑器的底部标记“sequenced”复选框。但是,这样做将创建一个 INTEGER 列,以及一个完全不相关的 PostgreSQL 序列对象。相反,要获得合成主键,您必须在列类型 <select> 列表下方的文本字段中手动输入 SERIAL。

一个额外的复选框让您可以指示列是否为主键,并在列列表中自动用“pk”标记它。第三个复选框允许您指示列是否可以包含 NULL 值,这是一种微妙的方式,提醒新的数据库设计人员,NULL 会使生活复杂化,应尽可能避免。

要创建外键 (REFERENCES) 或 CHECK 子句,请将其添加到 HTML 表单底部的“attributes”文本字段中。请记住,此时您只是在 Perl 中建模模式,这意味着将来您可以自由添加和删除此类子句,而无需向数据库发送 ALTER TABLE 查询。您还可以使用 Alzabo 编辑器在一个或多个列上创建索引。

您可以使用 Alzabo 表和列编辑器来创建许多表和列,并在它们之间使用一组分层菜单和列表进行移动。Alzabo 显示甚至在每列旁边放置了“<”和“>”标记,允许您在特定定义中相对于彼此移动它们。

当您使用基于浏览器的模式编辑器时,我建议您偶尔预览 Alzabo 生成的 SQL。这不仅可以确保 Alzabo 做对了(正如我们在 SERIAL 列中看到的那样),而且还可以让您更好地了解模式正在创建的底层细节。

在您完成创建模式后,使用“SQL 预览”页面中的“执行 SQL”按钮将您的 SQL 发送到数据库服务器。如果数据库服务器返回任何错误,Alzabo 将生成冗长而详细的错误消息,描述发生了什么。

在某些情况下,您可能需要修复表或列定义,而在另一些情况下,您可能需要确保服务器以正确的权限运行。还要确保您已定义一个 PostgreSQL 用户(使用命令行 createuser 程序创建),其名称与 Apache 运行的用户名匹配,除非您在 HTML 表单中显式命名另一个用户。

从程序中使用我们的模式

从模式编辑器中执行 SQL 后,您有两种方法可以访问数据。当然,您可以直接使用 DBI(或来自另一种语言的类似接口)访问它,创建和执行 SQL 查询。

例如,假设我使用以下表创建了我的 addressbook 模式

CREATE TABLE People (
    person_id    SERIAL    NOT NULL,
    first_name   TEXT      NOT NULL,
    last_name    TEXT      NOT NULL,
    birthday     DATE      NOT NULL,
    PRIMARY KEY(person_id)
);

为了使事情更有趣一点,让我们用一些值填充我们的表

INSERT INTO People (first_name, last_name, birthday)
VALUES ('Reuven', 'Lerner',
'1970-Jul-14');
INSERT INTO People (first_name, last_name, birthday)
VALUES ('Atara Margalit', 'Lerner-Friedman',
        '2000-Dec-16');
列表 1 包含一个简单的 Perl 程序,该程序使用 DBI 检索我们的 addressbook 表中与命令行中输入的 SQL 模式匹配的人员的姓名(和生日)。(SQL 模式比 UNIX 正则表达式简单得多——只有两个字符:% 匹配零个或多个字符,_ 匹配恰好一个字符。)

列表 1. retrieve-today-birthday.pl,它使用 DBI 检索我们的 addressbook 表中生日是今天的人员的姓名。

我们检索用户在命令行上的输入,并在其前后放置 % 符号,以确保该字符串将匹配,无论它出现在 first_name 或 last_name 列中的哪个位置。然后我们连接到数据库,启用 AutoCommit(正如 DBI 文档鼓励我们做的那样)并激活 RaiseError 和 PrintError 诊断辅助功能。

最后,我们在 $sql 变量中创建我们的 SQL 查询,确保使用占位符(“?”)而不是直接插入变量。这不仅降低了有人破坏我们的 SQL 的风险,而且某些数据库驱动程序将利用我们后续查询中的占位符,从而提高我们的速度。

在 Alzabo 中重写

让我们使用 Alzabo 而不是直接使用 DBI 重写此程序。我们不会自己编写 SQL 或自己连接到数据库。相反,我们将创建一个新的模式对象,命名我们使用 Alzabo 交互式工具创建的模式。此对象有许多方法,使我们能够执行许多原本需要使用 DBI 完成的任务。

正如您从列表 2 中看到的那样,在连接到数据源之前,两个版本之间没有太多区别。在程序的 DBI 版本中,我们使用 DBI->connect 连接到数据源本身。然而,在 Alzabo 中,我们连接到一个模式,该模式可能附加到数据库,并将其分配给对象 $schema。

列表 2. retrieve-birthday-alzabo.pl,列表 1 中程序的 Alzabo 实现。

使用 $schema,我们检索与我们的表之一关联的表对象

my $people = $schema->table("People");

现在我们有了一个映射到我们的 People 表的对象,我们可以从表中检索选定的行。检索行的最简单方法是使用 rows_where 方法。这返回一个 Alzabo::Runtime::RowCursor 类型的单个对象

my $row_cursor =
    $people->rows_where
        (where => [[$people->column('first_name'),
                   'LIKE', $look_for_name],
                 'or',
                 [$people->column('last_name'),
                  'LIKE',
                  $look_for_name]]);
Alzabo 的 WHERE 子句通常由一个三元素列表组成:一个列对象、一个比较运算符和一个值或第二个列对象。我们可以将 first_name 列与 Zaphod 的相等性进行比较,使用
where => [$table->column('first_name'), '=',
'Zaphod']
在列表 2 中,我们使这稍微复杂了一些,使用 OR 布尔运算符链接两个数组引用
where => [[$people->column('first_name'),
           'LIKE', $look_for_name],
          'or',
          [$people->column('last_name'),
           'LIKE', $look_for_name]]
Alzabo 非常智能,可以意识到其 WHERE 子句的第一个和第三个元素是数组引用,并且它将上述代码转换为适当的 SQL。

一旦我们有了 RowCursor 对象,我们就可以使用 next_row 方法迭代遍历每一行

while (my $row = $row_cursor->next_row)
{
   my $first_name = $row->select('first_name');
   my $last_name = $row->select('last_name');
   my $birthday = $row->select('birthday');
   print "$first_name $last_name
          (birthday: $birthday)\n";
   $rows_returned++;
}
缓存和异常

如果 Alzabo 仅提供一组创建 SQL 的方法,那么它就不会是一个非常强大的工具。但是,Alzabo 提供了缓存和异常处理作为其工具套件的一部分,这在某些方面使数据库的使用更加容易。

Alzabo 的缓存功能将表保存在内存中,而不是每次我们从数据库服务器请求值时都返回到数据库服务器。显然,缓存不适用于经常更改的表,但对于很少更改的表,您可以激活缓存并享受速度的显着提升。

您可以通过在程序中加载 Alzabo::ObjectCache 模块来激活缓存。RowCursor 对象(我们用于在列表 2 中检索行)在 next_row 方法的每次迭代中返回 Row 对象。有关可供您使用的不同类型的缓存以及与它们相关的问题的信息,请参阅 Alzabo::Runtime::Row 和 Alzabo::ObjectCache 的文档。

Alzabo 还使用 Perl 的内置异常处理系统,这意味着如果出现问题,它会调用“die”。因此,您应该将使用 Alzabo 的程序(或其中的单个调用)包装在“eval”块中

# Try to run this code
eval {
    my $row_cursor =
        $people->rows_where(
            where => [[$people->column('first_name'),
                       'LIKE', $look_for_name],
                     'or',
                     [$people->column('last_name'),
                      'LIKE',
                      $look_for_name]]);
};

您可以通过检查特殊的 Perl 变量 $@ 来找出是否出了问题,如果在之前的 eval 中发生错误,则会设置该变量。但是 Alzabo 使用 Exception::Class 对象(可从 CPAN 获得)在 Perl 中进行更复杂的异常处理。$@ 变量未设置为描述错误的文本字符串,而是设置为适当异常类的实例。因此,您可以使用 UNIVERSAL::isa 测试 $@,以确定它到底是什么类型的对象,以及您的代码中发生了什么类型的问题。安装在您 Mason 控制的 Apache 内容目录中的 alzabo 目录下的 Mason 组件 common/exception 详细演示了如何执行此操作。

问题

与任何试图弥合对象关系差距的工具一样,Alzabo 显然也有相关的成本。首先,SQL 是一种相当标准的用于处理关系数据库的手段。使用 Alzabo 意味着您将远离该标准,转而采用与任何其他方法都不兼容的不同解决方案。我不反对做事的新方法,并且使用 Alzabo 有许多显着的优势。也就是说,对于以新的、非标准的方式做旧的、标准的事情,我始终持谨慎态度。

虽然我通常更喜欢使用手工制作的 SQL 创建表,但这种技术无法扩展到 10 或 20 个以上的表,而不会迫使我在 Emacs 缓冲区中疯狂滚动。Alzabo 基于 Web 的模式设计工具确实可以更轻松地跟踪大量表,以创建它们之间的关系并修改它们。我最近花了半个小时试图记住 Oracle 的语法与 PostgreSQL 的语法有何不同,并且本可以从像 Alzabo 这样的工具中获益匪浅。

正如我们之前看到的那样,即使在那些查询包含 OR 和 AND 运算符的情况下,在 Alzabo 中创建基于相等的复杂查询也并不困难。Alzabo::Runtime::Table 对象包含一个 function 方法,该方法旨在执行任意 SQL 函数。但是,我发现很难,甚至在某些情况下不可能创建 Alzabo WHERE 子句,这些子句可以让我创建基于多个函数调用的 SQL 查询。我承认我对 Alzabo 来说还是个新手,只尝试了一两个小时,但是我在 SQL 中花了 20 秒编写的查询在 Alzabo 中不应该花费更长的时间。

将对象映射到关系数据库时,更困难的问题之一与连接有关。当处理表时,连接很有意义,但是当处理对象时,其含义就不那么明显了。Alzabo 确实对连接有一些内置支持,但它被标记为很大程度上是新的和实验性的。

最后,任何中间件层也都存在速度权衡。当我从命令行执行列表 1 和列表 2 时,速度差异非常明显,这在很大程度上归因于使用 Alzabo 导入了大量 Perl 类。在 mod_perl 环境(Alzabo 旨在在此环境中发光)中,速度差异会小得多,因为大部分时间都花在了从磁盘加载不同模块上。由于 mod_perl 在执行程序之前仅编译一次程序,因此 Alzabo 和原始 DBI 调用之间的速度差异可能没有那么大。

结论

Alzabo 提供了一种相对简单的方法来围绕您的关系数据库表包装对象。这里有很多好消息:数据建模工具非常复杂,有很多不错的功能,这些方法在很大程度上是有意义的,并且文档内容丰富且通常编写良好。正如开源社区长期以来所说的那样,使用现有的、经过实战检验的开源工具几乎总是比推出一个新的、专有的、解决相同问题的软件包要好。

但是,在对象内部包装关系数据库表总是充满危险和问题,Alzabo 也不例外:连接仍然笨拙,并且不清楚如何创建某些查询。Alzabo 在这里没有错;当使用两种以不同方式看待世界的技术时,这是一个固有的问题。

可以肯定的是,将来我将在我的某些服务器端程序中使用 Alzabo,特别是那些需要比我原本可以提供的更复杂的缓存和异常处理的程序。

下个月,如果海关允许,我们将回到我们的服务器端 Java 之旅,将 Enhydra 的 DODS 包与 Alzabo 及其同类产品进行比较。

资源

Data Modeling with Alzabo
电子邮件:reuven@lerner.co.il

Reuven M. Lerner 拥有一家小型咨询公司,专门从事 Web 和互联网技术。他与妻子 Shira 和女儿 Atara Margalit 一起住在以色列的莫迪因。您可以通过 reuven@lerner.co.il 或 ATF 主页 http://www.lerner.co.il/atf 与他联系。

加载 Disqus 评论