Embperl 和数据库
那些读过我几篇“At the Forge”专栏文章的人都知道,我是 HTML/Perl 模板的忠实粉丝,它允许我们在单个文档中混合两者。在十月份,我介绍了 Embperl,一个可以作为独立 CGI 程序运行的模板系统,但也可以集成到 Apache 的 mod_perl 模块中。本月,我们将更仔细地研究 Embperl,探索它如何让我们编辑数据库中的记录。
使用模板有很多充分的理由。首先,通过将代码和设计放在同一个文档中,设计师和程序员可以各自修改他们负责的元素。当网站决定更改其设计时,程序员不再是瓶颈,就像 CGI 程序产生动态输出时的情况一样。
即使您不太可能更改动态生成的 HTML 页面的外观,Embperl(以及类似的内联模板机制,允许您混合代码和 HTML)也能让您将所有内容粘合在一起,使逻辑更易于理解。我编写过许多 CGI 程序,其中动态输出与静态输出相比相形见绌——但是因为即使生成的 HTML 页面的一部分必须随时间而改变,整个页面也必须在程序的管辖范围内。
自从我撰写了十月份对 Embperl 的介绍以来,该软件包已得到显着改进。也许最重要的变化是,最新版本的 Apache 1.3.1 和 mod_perl 1.15 让您在安装新版本的 Embperl 时无需重新编译所有内容。现在,Embperl 可以与 Apache 和 mod_perl 分开安装和升级,就像您从 CPAN 安装和升级其他 Perl 软件包一样。请参阅“资源”侧边栏,了解在哪里获取最新信息,包括关于 Apache、mod_perl 和 Embperl 的安装说明。
数据库是 Web 中越来越重要的组成部分。使用它们,我们可以创建定制化和个性化的站点,为人们带来他们需要的特定信息,而不是简单地将我们拥有的所有信息都交给他们。
此外,数据库旨在轻松存储和检索信息。如果文本文件和 DBM 文件对您的需求来说太不安全或结构化程度不够,请考虑使用关系数据库。关系数据库将其信息存储在表中,其中每个表都有列(描述各种字段)和行(每行存储一条记录)。使用多个表是“关系”部分的关键所在,它可以成为一个非常强大的工具。您可能可以在自己的程序中实现此功能,但这将非常复杂——而且,已经有人为您完成了这项工作。
关系数据库使用 SQL(结构化查询语言)进行操作,SQL 是 IBM 在 1970 年代开发的。您不是用 SQL 编写程序;相反,您编写“查询”来操作一个或多个表。使用 SQL,您可以创建表、修改其内容以及请求包含特定类型和数据片段的列和行的组合。
SQL 不是一种编程语言,因此必须通过编程语言创建并提交给数据库服务器。过去,每个数据库产品都需要自己的 Perl 版本才能允许访问;这导致了诸如 Oraperl、Sybperl 等版本。最近,通用的 DBI(数据库接口)产生了一个稳定且可移植的数据库引擎,它允许使用相同的接口访问任何关系数据库。特定于数据库的部分保留在 DBD(数据库驱动程序)中,由 DBI 动态加载。假设您坚持使用标准 SQL 而不是数据库供应商的专有扩展,您应该能够通过修改单个 Perl 语句来切换数据库品牌。
我在这些示例中使用的关系数据库是 MySQL,其作者将其描述为“大部分免费”的数据库。我已经使用 MySQL 相当长一段时间了,虽然它不具备其大型竞争对手的所有优化和锁定功能,但它的性能非常出色——而且更多的功能正在路上。有关 MySQL 的更多信息,请参阅“资源”侧边栏。
安装 Embperl 后,您需要告诉 Apache 哪些文档应使用 Embperl 解释,而不是作为纯 HTML 文档。在我的计算机上(运行修改后的 Red Hat Linux 5.1 版本),我将以下内容放入 srm.conf 配置文件中
Alias /embperl/ /usr/local/apache/share/embperl/
此外,我将以下内容放入 access.conf 配置文件中
<Location /embperl> SetHandler perl-script PerlHandler HTML::Embperl Options ExecCGI </Location>换句话说,我告诉 Apache,任何以 /embperl 开头的 URL 都指的是实际位于 /usr/local/apache/share/embperl 中的文件,并且 /embperl 中的任何文件都应由 HTML::Embperl 内容处理程序解释。重新启动 Apache 后,Embperl 就可以启动并运行了。
本月,我们将创建一个由单个表组成的数据库,即咨询业务的客户列表。此系统中的核心表之一是 Clients 表,其中包含有关每个客户的基本信息。
以下是创建此表所需的 SQL
CREATE TABLE Clients ( id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(40) NOT NULL, address1 VARCHAR(40) NOT NULL, address2 VARCHAR(40) NULL, city VARCHAR(40) NOT NULL, state VARCHAR(40) NULL, country VARCHAR(40) NOT NULL, zip VARCHAR(40) NULL, contact_name VARCHAR(40) NOT NULL, contact_phone1 VARCHAR(40) NOT NULL, contact_phone2 VARCHAR(40) NULL, contact_fax VARCHAR(40) NULL, initial_contact_date DATE NULL, dollars_per_hour TINYINT NOT NULL, UNIQUE (name) );
同样,我们不能直接将此 SQL 输入到关系数据库服务器中;我们必须使用已使用正确的客户端库编译的程序。MySQL 附带一个程序 (mysql),允许与数据库进行交互式通信;或者,我们可以使用 DBI 发送上述 SQL。
Clients 中的每一列都定义为 VARCHAR,即,可变长度文本字段。字段的长度由括号中的数字确定,我将其设置为 40 主要是为了使编程的其他元素更容易。(随着时间的推移,我希望使这些字段中的大多数都短得多。)
id 字段是特殊的,不仅因为我们将其定义为无符号整数(让我们可以选择包含多达 1600 万个不同的客户),而且因为它被设置为“主键”。就数据库而言,仅使用主键即可唯一标识每一行。我们将 id 设置为 AUTO_INCREMENT,这意味着 MySQL 将在存档文件中为第一个客户提供 ID 1,第二个客户 ID 2,依此类推。每个客户都将收到一个自动生成的唯一 ID 号。
我们还将 name 列声明为唯一,因为对于相关人员来说,拥有多个同名的客户可能会造成混淆。数据库将接受多个同名的列,只要 ID 号不同即可。但是,我们将通过在数据库中检查来避免出现两个名为“IBM”的客户的可能性。
您可能想知道为什么我们不使用 name 作为主键,因为它保证是唯一的。我们可以这样做,一切都会正常工作(可能稍微慢一点,因为文本字符串比整数大)。但是,请考虑如果客户更改其名称会发生什么——我们将不得不更新对该客户的所有引用,因为旧的引用将不再指向正确的位置。通过使我们的主键独立于客户更改的任何信息,我们可以继续跟踪客户,而不管信息如何更改。
现在我们已经定义了我们的表,我们将创建一个 Embperl 文档,允许我们插入新记录。(目前,我们的表是空的。)Embperl 文档在很大程度上与 HTML 文档相同,因此您可以使用 <H1>、<P> 和 <Blink> 标签以及常规文本,并且它会正常工作。
但是,您可以通过将 Perl 代码放在特殊的方括号内,将其插入到 Embperl 文档中。以下是 Embperl 理解的四种方括号类型
[- 代码 -]:评估代码。
[+ 代码 +]:评估代码,将最终值插入到 HTML 文档中。
[! 代码 !]:将代码评估为 [- 代码 -],但仅评估一次。
[$ 元代码 $]:评估 Embperl 元命令。
因此,我们可以包含以下语句
[- $foo = 5; -]$foo 将设置为 5——该值在多次调用中持续存在,因为 mod_perl 和 Embperl 会缓存此类值。相反,如果我们包含
[+ $foo = 5; +]那么“5”将出现在文档中括号所在的位置。如果您不熟悉“表达式的最终值”的概念,您可能希望以变量名称结束每个 Embperl 块。变量返回它们的值,因此如果您键入
[+ @reverse_list = reverse @list; $foo +]那么“5”将插入到该点的 HTML 文档中。
列表 1,add-client.html,是一个简单的 Embperl 文档,用于将客户端添加到数据库。它不检查我们交给它的数据——因为 MySQL 会为我们完成大部分工作——尽管它会向用户显示可能发生的任何数据库错误。
如果您是模板新手,可能需要一段时间才能理解包含 HTML 表单和处理表单所需程序的单个文件的概念。请考虑这与 CGI 程序生成可以从中获取输入的表单没有什么不同。
列表 1 包含两个部分:表单处理和表单创建。虽然 Embperl 在后者之前查看前者,但我们将首先查看创建,因为它通常更容易处理,尤其是在首次使用模板时。
除了 id 之外,我们将为表中的每一列创建一个 HTML 表单元素,因为 MySQL 会自动为我们生成 ID。稍后,我们将扩展此程序以处理表中行的编辑和删除,这意味着除了我们将要提交的“新”记录之外,我们还需要处理数据库中每一列和每一行的表单元素。
我的解决方案是为每个表单元素提供它所附加的列的名称,后跟一个连字符和 ID 号。id = 5 的行的“city”列将是一个名为“city-5”的元素,而 id = 30 的客户端的名称将是一个名为“name-30”的元素。由于 MySQL 从 1 开始自动递增 ID,我们可以对我们的新条目使用“name-0”、“address-0”等等。
在程序的早期,我们将定义 @colnames 数组,其中将包含数据库中列的名称
@colnames = (id name address1 address2 city state country zip contact_name contact_phone1 contact_phone2 contact_fax initial_contact_date dollars_per_hour);
现在我们已经定义了 @colnames,我们可以使用 Embperl 的元命令创建 HTML 表单。我们希望为每个元素创建一个条目(除了 id 之外,因为修改它会产生严重问题),因此我们将迭代 @colnames 的每个元素,添加必要的 HTML 并记住跳过 id。我的实现的这一部分如下所示
[$ foreach $column @colnames $] [$ if $column ne "id" $] <TR> <TD> [+ $column +] </TD> <TD> <input type="text" name="[+ $column +]-0" size="40" maxlength="40" > </TD> </TR> [$ endif $] [$ endforeach $]上面的代码看起来很像 Perl,这是有充分理由的。它使用 foreach 循环,该循环迭代数组 (@colnames) 的元素,并将数组的每个连续元素放入标量 ($column) 中。然后,我们可以通过将标量值放在 HTML 中适当位置的方括号加号中来使用它。
您可能不习惯在方括号美元符号中看到 endif 和 endforeach 元命令。这些告诉 Embperl if 和 foreach 元命令的范围在哪里结束,就像闭合花括号在标准 Perl 程序中所做的那样。
我们将每个字段的最大长度设置为“40”,就像我们表中的字段都定义为 VARCHAR(40) 一样。如果我们要修改表定义,以便将每列设置为更合理的尺寸(例如,name 可能更接近 60,而 contact_phone 更接近 15),我们还需要修改 HTML 表单中每个字段的大小。否则,用户会盲目地输入过多字符,并且他们的输入将被数据库服务器静默截断。如果您愿意,MySQL DBD (DBD::mysql) 具有可用于此类目的的 length 属性。
现在我们已经创建了表单,让我们考虑一下如何在收到表单后处理它。Embperl 文档将接收表单的名称-值对,就像它们被提交给 CGI 程序一样,尽管我们将不得不以稍微不同的方式提取它们。这些对在 %fdat 哈希中发送,其中哈希的键是提交的 HTML 表单元素的名称,哈希的值是这些值。我们可以使用 $fdat{"name-0"} 获取新客户端的名称,使用 $fdat{"contact_phone1-0"} 获取主要电话号码,依此类推。
将记录插入表中的模式如下
INSERT (column1, column2, column3) " VALUES ("value1", "value2", "value3")
我们将想要做类似这样的事情
INSERT (@columns) VALUES (%fdat)当然,生活并没有那么容易;我们必须首先创建一个新的数组 @insert_colnames,其中包含我们希望插入的列的名称——换句话说,除了 id 之外的所有内容
[- @insert_colnames = grep !/^id$/, @colnames; -]然后我们将其转换为逗号分隔的列表,这将是我们插入语句的第一部分所需要的
[- $insert_colnames = join ', ', @insert_colnames; -]完成此操作后,我们将使用 Perl 的内置 map 函数将 @insert_colnames 从列名数组转换为列值数组。然后,我们将生成的数组转换为标量,其中每个值都用逗号分隔并用双引号引起来
[- $values = join '", "', map {$fdat{$_ . "-0"}} @insert_colnames -]如果 @insert_colnames 由以下内容组成
(column1, column2, column3)上面 map 的使用会将其转换为
($fdat{"column1-0"}, $fdat{"column2-0"}, $fdat{"column3-0"})然后 join 会将其转换为
$fdat{"column1-0"}", "$fdat{"column2-0"}", "$fdat{"column3-0"})开头或结尾没有任何引号,但我们可以在最终构造查询时添加它们
[+ $sql = "INSERT INTO Clients ($insert_colnames) VALUES (\"$values\")"; +]我们在此处使用方括号加号是为了查看(并在必要时进行调试)我们发送到数据库的查询。不要忘记,如果我们使用双引号来利用变量插值,我们必须使用反斜杠转义我们希望在查询中发送的双引号。
我们最终使用以下语句发送该查询
[- $sth = $dbh->prepare($sql); -] [- $sth->execute; -]
如果有任何错误,请为用户打印它们
<P><B>[+ $sth->errstr +]</B></P>我们的新记录现在已插入到数据库中。
如果用户尚未提交任何表单元素,则整个表单处理部分是不必要的。在列表 1 中,您可以看到我们如何使用 Embperl if 元命令来排除对整个代码块的评估,如果用户已经做了一些事情。
第一次运行此程序时,如果一切似乎都正常工作 并且 您取回了原始表单,请不要感到惊讶。正如他们所说,这不是错误——这是一个功能!如果 Embperl 在 HTML 表单中找到与 %fdat 中的名称-值对匹配的字段,它会自动填写它们。您可以通过修改 EMBPERL_OPTIONS 位掩码字段来关闭此选项,这在 Embperl 文档中进行了描述。
现在我们已经了解了如何使用 Embperl 输入新记录,让我们扩展模板,使其允许我们修改和删除现有记录,以及添加新记录。您可以在存档文件 client-editor.html 的 列表 2 中看到此类模板的完整列表。
首要任务是从数据库中检索现有元素,并将它们转换为用户可以抓取的表单元素列表。正如我们之前看到的,如果我们为每个表单元素提供与其关联的列的名称,以及指示其记录 ID 号的数字,这将是最容易的。
首先要做的业务是从当前数据库中检索行。我们使用 SELECT 语句执行此操作,其语法如下所示
SELECT column1, column2, column3 FROM Tablename;
我们按如下方式设置我们的查询
[- $sql = "SELECT $colnames FROM Clients"; -]现在我们使用标准 DBI 语法准备和执行查询
[- $sth = $dbh->prepare ($sql) -] [- $sth->execute -]SELECT 的结果是一个表,我们可以通过多种不同的方式检索它。也许最简单的方法是将其作为数组引用抓取,然后将该数组引用转换为包含名称-值对的数组,继续获取数组引用直到用完为止。如果我们使用 Embperl 的 while 元命令,我们可以相当容易地做到这一点
[$ while ($record = $sth->fetchrow_arrayref) $]然后我们抓取 id 列
[- $recordid = $record->[0]; -]我们可以将该数组引用转换为数组,使用 Embperl 的 foreach 元命令来迭代每个元素,在表格行中打印除 id 之外的每个元素。如果我们将当前记录(行)号存储在 $recordid 中,将当前字段号存储在 $fieldcounter 中,我们可以通过迭代以下代码来创建它
<TR> <TD>[+ $colnames[$fieldcounter] +]</TD> <TD> <input type="text" name="[+ $colnames[$fieldcounter] . '-' . $recordid +]" size="50" maxlength="100" value="[+ $field +]" > </TD> </TR>我们还将添加一组三个单选按钮,以指示用户是否希望删除此记录、修改它或不执行任何操作。我们将“不执行任何操作”设置为默认值,因为我们不希望用户无意中删除任何元素。我们创建单选按钮,使用 modify- 词干,就像在普通 HTML 中一样。但是,我们将当前 ID 号添加到该词干
<P><input type="radio" value="nothing" name="modify-[+$recordid +]" checked> Do nothing <input type="radio" value="modify" name="modify-[+$recordid +]"> Modify this client <input type="radio" value="delete" name="modify-[+$recordid +]"> Delete this client </P>正如您在列表 2 中看到的,我们还在初始“新客户端”表单中添加了一个复选框,以指示用户是否有兴趣添加新客户端。此复选框可以在 HTML 中硬编码,因为我们仅允许用户从该表单添加新元素,伪 ID 为 0
<P><input type="checkbox" name="modify-0"> Add this new client <P>
正如 add-client.html(列表 1)分为处理部分(第一部分)和表单生成部分(第二部分)一样,我们的完整 client-editor.html(列表 2)也是如此。以上部分描述了我们将如何使用 SELECT 创建 HTML 表单,因此剩下的就是描述处理部分,它位于模板的顶部。
使用 add-client.html,我们可以假设用户想要添加新客户端。现在有四种可能性:添加新客户端、更新现有客户端、删除现有客户端以及完全不执行任何操作。虽然 add 只能对 modify-0(新记录)为真,但我们必须检查发送给我们的每一组 HTML 表单元素。当然,最简单的情况是 modify- 单选按钮设置为“不执行任何操作”时。
如果用户想要添加新记录,则将选中元素 modify-0。我们可以使用 Embperl if 元命令来检查它的存在
[$ if $fdat{"modify-0"} ne "" $]
换句话说,如果用户选中了 modify-0,我们将添加一个新记录,就像我们在 add-client.html 中所做的那样。
找出用户是否为其中一个记录选中了 modify 有点棘手。我们获取所有提交的表单元素的名称 (sort keys %fdat),并使用 grep 来抓取所有带有 modify- 词干的元素
[$ foreach $clientid (grep {($_ =~ /^modify-\d+$/) && ($fdat{$_} eq "modify@bb:1. )} (sort keys %fdat)) $]
如果以上内容看起来有点吓人,请记住 $_ 包含 grep 当前正在处理的标量的值。我们告诉 grep 仅返回那些与 modify-\d+(即,modify- 后跟一个或多个数字)匹配且其值为 modify 的数组元素。然后,我们获取 grep 返回的数组,并使用 Embperl 的 foreach 元命令对其进行迭代。
一旦进入 foreach 循环,我们如何创建 SQL 查询?我们首先必须抓取相关元素的 ID,以便我们仅更新相应的记录。我们通过给出
$clientid =~ m/(\d+)$/;
这会将 ID 值放入临时变量 $1 中。然后,我们结合使用 grep、map 和 join 来创建完成带有以下内容的 UPDATE 语句语法所需的名称-值对列表
UPDATE Clients SET name1="value1",name2="value2" WHERE id = $1我们使用 grep 来抓取除 id 之外的所有列名(再次,我们不想更改该值)。然后,我们通过 map 过滤该结果,将列名列表转换为 name="value" 对列表。最后,我们将该列表与逗号连接在一起,生成标量 $pairs
$pairs = join ', ', map {"$_ = '" . $fdat{$_ . "-$1"} . "'"} grep (!/^id$/, @colnames);然后我们可以按如下方式设置 SQL 查询
$sql = "UPDATE Clients SET $pairs WHERE id = $1";删除元素比更新更容易,因为我们不需要名称-值对。我们可以使用语句
$sql = "DELETE FROM Clients WHERE id = $1";其中 $1 与当前元素的编号匹配。
信不信由你,我们完成了。此客户端编辑器显然需要在其用户界面方面提供一些帮助,因为仍然有人可能输入非法值(例如,initial_contact_date 的错误 DATE 元素,或 TINYINT 列 dollars_per_hour 的分数)。如果您有超过三四个客户,此界面很快就会变得乏味。每个列都缺少真正描述性的名称,这使得一个程序看起来难以使用,但它比输入纯 SQL 更容易且更不容易出错。
但是,一旦您了解如何执行四个基本数据库操作:INSERT、SELECT、UPDATE 和 DELETE,改进界面就非常简单了。实际上,我们已经看到在 Embperl 中完成所有这些操作可能非常简单。鉴于我们已经看到的示例,创建替代界面应该不难。
更重要的是,此 Embperl 模板不仅对 Clients 表有用。通过修改 @columns 的值和表的名称,您可以使用相同的模板来修改几乎任何表中的任何记录。
我希望您喜欢这次 Embperl 和模板世界的漫步。现在有许多模板系统可用于执行类似的操作;即使您不习惯使用此类模板与数据库通信,也应考虑获取可用的软件包之一并尝试一下。它们的力量可能会让您相信它们的实用性。
