二、使用 PHP 数据对象:第一步

在上一章中,我们简要概述了什么是 PDO,如何使用 PDO 连接到您最喜欢的数据库,如何发出简单查询以及如何处理错误。既然您确信 PDO 是一件好事,并且正在考虑积极使用它,那么我们将深入研究它提供的所有功能。

在本章中,我们将更详细地介绍如何使用 PDO 和连接字符串(数据源名称)创建到数据库的连接、 PDOStatement类以及如何遍历结果集。我们还将创建一个小型的图书馆管理应用程序,该应用程序将允许我们管理您家图书馆的图书集合。该应用程序将能够列出书籍和作者以及添加和编辑它们。

我们将首先看看连接字符串,因为没有它们,我们将无法连接到任何数据库。然后,我们将创建一个示例数据库,本书中的所有示例都将基于此数据库。

我们将从简单的、想象中的汽车数据库出发,创建一个包含多个表的实际工作数据库。然而,现在我们将讨论书籍和作者的经典例子。我们选择这个例子是因为这样的实体更常见。关系模型将相对简单,因此,如果您已经在其他地方遇到过这样的数据库,那么您将能够轻松地遵循示例。

连接串

连接字符串或 PDO 文档中所称的数据源名称(缩写为 DSN)是 PHP 字符串,包含数据库管理系统名称、数据库本身名称以及其他连接参数等信息。

与使用传统方法创建数据库连接相比,它们的优势在于,如果更改数据库管理系统,则无需修改代码。可以在配置文件中定义连接字符串,该文件由应用程序处理。如果数据库(数据源)发生更改,只需编辑该配置文件,其余代码保持不变。

由于存在不同的数据库管理系统,PDO 中使用的连接字符串不同。但是,它们总是有一个公共前缀,表示底层数据库驱动程序。记住第一章中的 MySQL、SQLite 和 PostgreSQL 示例。三个连接字符串如下所示:

mysql:host=localhost;dbname=cars
sqlite:/path/to/cars.db
pgsql:host=localhost dbname=cars

正如我们所看到的,前缀(第一个分号之前的子字符串)始终保留 PDO 驱动程序的名称。因为我们不必使用不同的函数来创建与 PDO 的连接,所以这个前缀告诉我们应该使用哪个内部驱动程序。该驱动程序将解析字符串的其余部分,以进一步启动连接。在这些情况下,我们提供了数据库名称;用于 MySQL 和 PostgreSQL;我们还提供了服务器运行的主机名。(由于 SQLite 是一个本地数据库引擎,这样的参数没有意义。)

如果您想指定其他参数,应该参考数据库手册(www.php.net/pdo总是一个好的开始)。例如,MySQL PDO 驱动程序理解以下参数:

  • 主机-服务器运行的主机名(本例中为本地主机
  • 端口-数据库服务器监听的端口号(默认为3306】
  • dbname-数据库的名称(本例中为cars
  • unix_socket - the MySQL UNIX socket (instead of host and/or port).

    SQLite:前缀表示与 SQLite 3 数据库的连接。要连接到 SQLite 2 数据库,必须使用 SQLite2:前缀。请参见http://www.php.net/manual/en/ref.pdo-sqlite.connection.php 了解详情。

您可能已经注意到,不同的驱动程序使用不同的字符来分隔参数,例如 MySQL 中的分号和 PostgreSQL 中的空格。

创建样本数据库

假设你家里有一个很好的图书馆,你想让你的电脑帮助你管理它。您决定使用 PHP 和 PDO 创建一个基于 web 的数据库。从现在起,示例将针对 MySQL 和 SQLite 数据库。

数据模型

因为我们的数据库非常简单,所以我们只需要两个实体:作者和书籍。因此,我们将创建两个名称相同的表。现在,让我们想想这些实体中的每一个都有什么属性。

作者将有他们的名字、姓氏和一本简短的传记。该表需要有一个主键,我们称之为id。我们将使用它引用 books表中的作者。

书是作者写的。(有时他们是由一个以上的作者写的,但是我们会考虑这里只有一个作者写的书。)所以我们需要一个作者身份的字段,以及书名、ISBN 号、出版商姓名和出版年份。此外,我们还将简要介绍本书的内容。

我们需要为作者建立一个单独的表格,因为一个作者可能写了不止一本书。另外,我们的例子将非常简单,否则!因此,我们选择了两表数据库结构。如果我们要考虑一个以上的作者写的书,我们需要三个表,这将使这个例子非常复杂。

创建 MySQL 数据库

启动 MySQL 命令行客户端后,您将看到 mysql>提示,您可以在这里发出命令来创建数据库和其中的表:

mysql> create database pdo;
Query OK, 1 row affected (0.05 sec)
mysql> use pdo;
Database changed
mysql> create table books(
-> id int primary key not null auto_increment,
-> author int not null,
-> title varchar(70) not null,
-> isbn varchar(20),
-> publisher varchar(30) not null,
-> year int(4) not null,
-> summary text(2048));
Query OK, 0 rows affected (0.17 sec)
mysql> create table authors(
-> id int primary key not null auto_increment,
-> firstName varchar(30) not null,
-> lastName varchar(40) not null,
-> bio text(2048));
Query OK, 0 rows affected (0.00 sec)

如您所见,我们创建了一个数据库,并将其命名为 pdo。正如我们计划的那样,我们还创建了两个表:书籍和作者。现在让我们看看如何在 SQLite 中实现这一点。由于无法在 SQLite 命令行客户端内创建数据库,因此我们按如下方式启动它:

> sqlite3 pdo.db
sqlite> create table books(
...> id integer primary key,
...> author integer(11) not null,
...> title varchar(70) not null,
...> isbn varchar(20),
...> publisher varchar(30) not null,
...> year integer(4) not null,
...> summary text(2048));
sqlite> create table authors(
...> id integer(11) primary key,
...> firstName varchar(30) not null,
...> lastName varchar(40) not null,
...> bio text(2048));

如您所见,SQLite 的 SQL 略有不同,主键的声明没有使用 NOT NULLauto_increment选项。在 SQLite 中,声明为 INTEGER PRIMARY KEY的列会自动递增。现在,让我们在数据库中插入一些值。MySQL 和 SQLite 的语法是相同的,所以这里我们只展示 MySQL 命令行客户机示例。我们将从作者开始,因为我们需要他们的主键值来插入 books 表:

mysql> insert into authors(firstName, lastName, bio) values(
-> 'Marc', 'Delisle', 'Marc Delisle is a member of the MySQL
Developers Guide');
Query OK, 1 row affected (0.14 sec)
mysql> insert into authors(firstName, lastName, bio) values(
-> 'Sohail', 'Salehi', 'In recent years, Sohail has contributed
to over 20 books, mainly in programming and computer graphics');
Query OK, 1 row affected (0.00 sec)
mysql> insert into authors(firstName, lastName, bio) values(
-> 'Cameron', 'Cooper', 'J. Cameron Cooper has been playing
around on the web since there was not much of a web with which to
play around');
Query OK, 1 row affected (0.00 sec)

现在我们已经插入了三位作者,让我们添加一些书籍。但在此之前,我们应该知道哪个作者拥有哪个id。一个简单的 SELECT查询将帮助我们:

mysql> select id, firstName, lastName from authors;
+----+-----------+----------+
| id | firstName | lastName |
+----+-----------+----------+
| 1 | Marc | Delisle |
| 2 | Sohail | Salehi |
| 3 | Cameron | Cooper |
+----+-----------+----------+
3 rows in set (0.03 sec)

现在,我们终于可以使用这些信息添加三本书,每本书都由以下作者之一撰写:

mysql> insert into books(author, title, isbn, publisher, year, summary) values(
-> 1, 'Creating your MySQL Database: Practical Design Tips and
Techniques', '1904811302', 'Packt Publishing Ltd', '2006',
-> 'A short guide for everyone on how to structure your data and
set-up your MySQL database tables efficiently and easily.');
Query OK, 1 row affected (0.00 sec)
mysql> insert into books(author, title, isbn, publisher, year, summary) values(
-> 2, 'ImageMagick Tricks', '1904811868', 'Packt Publishing
Ltd', '2006',
-> 'Unleash the power of ImageMagick with this fast, friendly
tutorial, and tips guide');
Query OK, 1 row affected (0.02 sec)
mysql> insert into books(author, title, isbn, publisher, year,
summary) values(
-> 3, 'Building Websites with Plone', '1904811027', 'Packt
Publishing Ltd', '2004',
-> 'An in-depth and comprehensive guide to the Plone content
management system');
Query OK, 1 row affected (0.00 sec)

现在我们已经填写了 authorsbooks表,我们可以开始创建小型图书馆管理 web 应用程序的第一页了。

所使用的数据基于 Packt Publishing Ltd(为您带来这本书的出版商)出版的真实书籍。欲了解更多信息,请访问他们的网站http://www.packtpub.com

设计我们的代码

除了正确的数据模型外,良好的应用程序体系结构是应用程序的另一个关键因素。由于本章中我们将要开发的应用程序相对较小,所以此任务并不十分复杂。首先,我们将创建两个页面,列出书籍和作者。首先,我们应该考虑这些页面的外观。为了使我们的简单示例小巧紧凑,我们将在所有页面上显示一个标题,其中包含指向图书列表和作者列表的链接。稍后,我们将再添加两页,这将允许我们添加一位作者和一本书。

当然,我们应该创建一个公共 include 文件,该文件将定义诸如页眉和页脚显示以及到数据库的连接等公共功能。我们的示例非常小,因此我们将不使用任何模板系统,甚至不使用面向对象的语法。(事实上,这些主题超出了本书的范围。)因此,总结一下:

  • 所有通用函数(包括创建 PDO 连接对象的代码)将保存在一个包含文件(称为 common.inc.php)中)。
  • 每页将保存在一个单独的文件中,其中包括 common.inc.php文件。
  • 每个页面都将处理数据并显示数据(这样我们就不会分离数据处理和数据表示,正如人们所期望的那样,在设计模型-视图-控制器模式的应用程序中)。

现在我们有了这个小计划,我们可以从我们的 common.inc.php文件开始。正如我们刚才讨论的,现在,它将包含显示页眉和页脚的函数,以及创建连接对象的代码。让我们将 PDO 对象保存在一个名为 $conn的全局变量中,并调用页眉函数 showHeader()和页脚函数 showFooter()。此外,我们将在这个包含文件中保留数据库连接字符串、用户名和密码:

<?php
/**
* This is a common include file
* PDO Library Management example application
* @author Dennis Popel
*/
// DB connection string and username/password
$connStr = 'mysql:host=localhost;dbname=pdo';
$user = 'root';
$pass = 'root';
/**
* This function will render the header on every page,
* including the opening html tag,
* the head section and the opening body tag.
* It should be called before any output of the
* page itself.
* @param string $title the page title
*/
function showHeader($title)
{
?>
<html>
<head><title><?=htmlspecialchars($title)?></title></head>
<body>
<h1><?=htmlspecialchars($title)?></h1>
<a href="books.php">Books</a>
<a href="authors.php">Authors</a>
<hr>
<?php
}
/**
* This function will 'close' the body and html
* tags opened by the showHeader() function
*/
function showFooter()
{
?>
</body>
</html>
<?php
}
// Create the connection object
$conn = new PDO($connStr, $user, $pass);

如您所见,该文件非常简单,您只需更改 $user$pass变量的值(第 9 行和第 10 行)即可匹配您的设置。对于 SQLite 数据库,还必须更改第 8 行,使其包含适当的连接字符串,例如:

$connStr = 'sqlite:/www/hosts/localhost/pdo.db';

当然,您应该对此进行更改,以反映创建 SQLite 数据库的路径。此外, showHeader()函数简单地呈现 HTML 代码,并通过 htmlspecialchars()函数传递 $title变量的值,以便正确转义任何非法字符(例如小于号)。

将文件保存到 web 根目录。这同样取决于您的 web 服务器设置。例如,它可以是 C:\Apache\htdocs or /var/www/html

现在,让我们创建一个列出书籍的页面。我们必须发出查询,然后对结果进行迭代,以在自己的行中显示每本书。稍后,我们将创建一个页面,列出我们先前创建的数据库中的所有作者。完成此任务后,我们将查看结果集遍历。

让我们调用我们的文件 books.php并创建代码:

<?php
/**
* This page lists all the books we have
* PDO Library Management example application
* @author Dennis Popel
*/
// Don't forget the include
include('common.inc.php');
// Issue the query
$q = $conn->query("SELECT * FROM books ORDER BY title");
// Display the header
showHeader('Books');
// now create the table
?>
<table width="100%" border="1" cellpadding="3">
<tr style="font-weight: bold">
<td>Title</td>
<td>ISBN</td>
<td>Publisher</td>
<td>Year</td>
<td>Summary</td>
</tr>
<?php
// Now iterate over every row and display it
while($r = $q->fetch(PDO::FETCH_ASSOC))
{
?>
<tr>
<td><?=htmlspecialchars($r['title'])?></td>
<td><?=htmlspecialchars($r['isbn'])?></td>
<td><?=htmlspecialchars($r['publisher'])?></td>
<td><?=htmlspecialchars($r['year'])?></td>
<td><?=htmlspecialchars($r['summary'])?></td>
</tr>
<?php
}
?>
</table>
<?php
// Display footer
showFooter();

此文件应保存到 common.inc.php文件所在的目录中。正如您所看到的,代码中有更多的注释和 HTML,但是这里没有非常复杂的内容。正如我们之前决定的,代码包括 common.inc.php文件,然后呈现页眉,在第 10 行发出查询,呈现表头,最后迭代结果集中的每一行以输出每本书的详细信息。

与第一章一样,我们使用 PDOStatement对象的 fetch()方法(保存在 $q变量中)遍历 while行中的结果集。我们指示此方法将行作为按表列名索引的数组返回(通过指定 PDO::FETCH_ASSOC参数)。

在循环内部,我们呈现每一行的 HTML,在其中插入表中的列。循环结束后,我们关闭表并显示页脚。

现在是测试我们第一个 PDO 驱动的应用程序的时候了。启动浏览器并导航至 http://localhost/books.php。如果您做的每件事都正确(以便正确设置 web 服务器和数据库),您应该会看到一个类似于以下屏幕截图的表格(尽管您的页面可能看起来更宽,但我们在截图之前调整了窗口大小,使其适合打印页面):

Designing Our Code

一旦我们确保了我们的应用程序与 MySQL 一起工作,让我们看看它将如何与 SQLite 一起工作。为此,我们必须编辑 common.inc.php文件中的第 8 行,使其包含 SQLite DSN:

$connStr = 'sqlite:/www/hosts/localhost/pdo.db';

如果所有操作都正确,那么在刷新浏览器后,您应该会看到相同的屏幕。正如我们前面讨论的,当您开始使用另一个数据库系统时,只需更改一个配置选项。

现在,让我们为将列出作者的页面创建代码。创建一个名为 authors.php的文件,并将其放置在保存前两个文件的目录中。代码实际上与图书列表页面相同:

<?php
/**
* This page lists all the authors we have
* PDO Library Management example application
* @author Dennis Popel
*/
// Don't forget the include
include('common.inc.php');
// Issue the query
$q = $conn->query("SELECT * FROM authors ORDER BY lastName,
firstName");
// Display the header
showHeader('Authors');
// now create the table
?>
<table width="100%" border="1" cellpadding="3">
<tr style="font-weight: bold">
<td>First Name</td>
<td>Last Name</td>
<td>Bio</td>
</tr>
<?php
// Now iterate over every row and display it
while($r = $q->fetch(PDO::FETCH_ASSOC))
{
?>
<tr>
<td><?=htmlspecialchars($r['firstName'])?></td>
<td><?=htmlspecialchars($r['lastName'])?></td>
<td><?=htmlspecialchars($r['bio'])?></td>
</tr>
<?php
}
?>
</table>
<?php
// Display footer
showFooter();

该文件遵循相同的逻辑:包括 common.inc.php文件,然后发出查询并遍历结果集。如果您已正确完成所有操作,则只需单击浏览器中图书列表页面上的作者链接,即可获得以下页面:

Designing Our Code

如您所见,页面正确地显示了我们在本章开头添加的三位作者。如果要用 SQLite 测试这一点,请更改行#10 以包含 SQLite 连接字符串。刷新浏览器时,您应该会看到相同的页面,但现在基于 SQLite 数据库内容。

既然我们已经创建了这两个页面,并且看到使用 PDO 并不复杂,那么在扩展应用程序之前,让我们先看看一些理论。

PDO 语句和结果集

我们的示例使用了 PHP 数据对象中的两个主要类: PDO类,用于创建连接和发出查询;以及 PDOStatement类,用于循环结果集。我们将在后面的章节中介绍这些课程中的第一个。在这里,我们将检查 PDOStatement类,看看它还提供了哪些遍历结果集的其他方法。

我们已经知道, PDOStatement类的实例是从对 PDO::query()方法的调用中返回的。此类的主要目的是提供结果集的接口。事实上,我们已经使用了它最重要的方法来迭代结果集。我们只研究了一种获取样式(或返回行的模式),但 PDO 提供了几种样式。此类还可以提供有关结果集的附加信息,例如行数和列数,并将整个结果集提取到二维数组中。

让我们先看看一些不同的获取样式。我们已经知道返回按列名索引的数组的 PDO::FETCH_ASSOC模式。 PDOStatement对象的默认操作是返回一个由整数索引和列名索引的数组,即 PDO::FETCH_BOTH获取模式。我们还可以使用 PDO::FETCH_NUM获取样式仅请求一个整数索引数组。PDO 还支持使用 PDO::FETCH_OBJ模式获取作为对象的行。在这种情况下,对 PDO::fetch()method的调用将返回 stdClass内部类的一个实例,其属性填充了行的值。这在以下代码中发生:

$q = $conn->query('SELECT * FROM authors ORDER BY lastName,
firstName');
$r = $q->fetch(PDO::FETCH_OBJ);
var_dump($r);
//would print:
object(stdClass)#4 (4)
{
["id"]=>
string(1) "3"
["firstName"]=>
string(7) "Cameron"
["lastName"]=>
string(6) "Cooper"
["bio"]=>
string(112) "J. Cameron Cooper has been playing around on the web
since there was not much of a web with which to play around"
}

PDOStatement类还允许您为其 fetch()方法的所有后续调用设置一次 fetch 模式。这是通过 PDOStatement::setFetchMode()方法完成的,该方法接受任何 PDO::FETCH_ASSOC, PDO::FETCH_BOTH, PDO::FETCH_NUMPDO::FETCH_OBJ常量。考虑到这一点,我们可以重写 authors.php文件的第 23 行和第 24 行,如下所示:

// Now iterate over every row and display it
$q->setFetchMode(PDO::FETCH_ASSOC);
while($r = $q->fetch())
{

您可以在您的 authors.php文件副本上试用它,然后刷新浏览器以查看它是否有效。

您可能已经注意到 SQLite、MySQL 和 PGSQLPHP 扩展都提供类似的功能。事实上,我们可以使用任何 mysql_fetch_row(), mysql_fetch_assoc(), mysql_fetch_array()mysql_fetch_object()函数来实现相同的效果。这就是为什么 PDO 更进一步,使我们能够使用三种额外的获取模式。这三种模式只能通过 PDOStatement::setFetchMode()调用进行设置,如下:

  • PDO::FETCH_COLUMN allows you to instruct the PDOStatement object to return the specified column of every row. In this case, PDO::fetch() will return a scalar value. The columns are numbered starting with 0. This happens in the following code snippet:

    ```php $q = $conn->query('SELECT * FROM authors ORDER BY lastName, firstName'); $q->setFetchMode(PDO::FETCH_COLUMN, 1); while($r = $q->fetch()) { var_dump($r); } //would print: string(7) "Cameron" string(4) "Marc" string(6) "Sohail"

    ```

    这表明对$q->fetch()的调用确实返回标量值(而不是数组)。请注意,如果您只是查看包含作者列表的页面,则索引为 1 的列应该是作者的姓氏,而不是他们的名字。但是,我们的查询看起来像 SELECT * FROM authors,因此它还检索作者 ID,这些 ID 存储在第 0 列中。您应该意识到这一点,因为您可能会花费数小时寻找此类逻辑错误的来源。

  • PDO::FETCH_INTO can be used to modify an instance of an object. Let's rewrite our above example as follows:

    ```php $q = $conn->query('SELECT * FROM authors ORDER BY lastName, firstName'); $r = new stdClass(); $q->setFetchMode(PDO::FETCH_INTO, $r); while($q->fetch()) { var_dump($r); } //would print something like: object(stdClass)#3 (4) { ["id"]=> string(1) "3" ["firstName"]=> string(7) "Cameron" ["lastName"]=> string(6) "Cooper" ["bio"]=> string(112) "J. Cameron Cooper has been playing around on the web since there was not much of a web with which to play around" } object(stdClass)#3 (4) { ["id"]=> string(1) "1" ["firstName"]=> string(4) "Marc" ["lastName"]=> string(7) "Delisle" ["bio"]=> string(54) "Marc Delisle is a member of the MySQL Developer Guide" } object(stdClass)#3 (4) { ["id"]=> string(1) "2" ["firstName"]=> string(6) "Sohail" ["lastName"]=> string(6) "Salehi" ["bio"]=> string(101) "In recent years, Sohail has contributed to over 20 books, mainly in programming and computer graphics" }

    ```

    while循环中,我们没有分配 $r变量,即 $q->fetch(). $r的返回值已经通过循环前对 $q->setFetchMode()的调用绑定到此方法。

  • PDO::FETCH_CLASS can be used to return objects of a specified class. For every row, an instance of this class will be created with the properties named and assigned the values of the result set columns. Note that the class does not necessarily have these properties declared since PHP allows runtime creation of object properties. For example:

    ```php $q = $conn->query('SELECT * FROM authors ORDER BY lastName, firstName'); $q->setFetchMode(PDO::FETCH_CLASS, stdClass); while($r = $q->fetch()) { var_dump($r); }

    ```

    这将打印与上一示例类似的输出。此外,此获取模式允许您通过向其构造函数传递参数数组来创建实例:

    ```php $q->setFetchMode(PDO::FETCH_CLASS, SomeClass, array(1, 2, 3));

    ```

    (只有在定义了 SomeClass类的情况下,此操作才有效。)

    我们建议使用 PDOStatement::setFetchMode(),因为它更方便、更易于维护(当然,它还有更多的功能)。

    描述所有这些获取模式似乎有些过分,但在某些情况下,每种模式都很有用。事实上,你可能已经注意到,书单有些不完整。它不包含作者的姓名。我们将添加这个缺少的列,为了使我们的示例更加复杂,我们将使作者的名字可以点击,并将其链接到作者的个人资料页面(我们将创建)。此配置文件页面需要作者 ID,以便我们可以在 URL 中传递它。它将显示我们关于作者的所有信息,以及他们所有书籍的列表。让我们从作者的个人资料页开始:

    ```php <?php /* * This page shows an author's profile * PDO Library Management example application * @author Dennis Popel / // Don't forget the include include('common.inc.php'); // Get the author $id = (int)$_REQUEST['id']; $q = $conn->query("SELECT * FROM authors WHERE id=$id"); $author = $q->fetch(PDO::FETCH_ASSOC); $q->closeCursor(); // Now see if the author is valid - if it's not, // we have an invalid ID if(!$author) { showHeader('Error'); echo "Invalid Author ID supplied"; showFooter(); exit; } // Display the header - we have no error showHeader("Author: $author[firstName] $author[lastName]"); // Now fetch all his books $q = $conn->query("SELECT * FROM books WHERE author=$id ORDER BY title"); $q->setFetchMode(PDO::FETCH_ASSOC); // now display everything ?>

    Author

    First Name <?=htmlspecialchars($author['firstName'])?>
    Last Name <?=htmlspecialchars($author['lastName'])?>
    Bio <?=htmlspecialchars($author['bio'])?>

    Books

    <?php // Now iterate over every book and display it while($r = $q->fetch()) { ?> <?php } ?>
    Title ISBN Publisher Year Summary
    <?=htmlspecialchars($r['title'])?> <?=htmlspecialchars($r['isbn'])?> <?=htmlspecialchars($r['publisher'])?> <?=htmlspecialchars($r['year'])?> <?=htmlspecialchars($r['summary'])?>
    <?php // Display footer showFooter();

    ```

    将此文件命名为 author.php并将其保存到其余文件所在的目录中。

    以下是关于代码的一些注释:

  • 我们通过将作者的 ID(第 13 行)显式转换为整数来处理它,以防止可能的安全漏洞。我们稍后将 $id变量传递给查询文本,而不引用,因为这样做可以使用数值。

  • 我们将在以下章节中讨论对 $q->closeCursor(); $q = null在线 13 的调用。这里我们只需要注意,在同一连接对象上执行的查询之间调用此方法,然后将其设置为 null 是一个好主意。没有它,我们的例子就不起作用。还要注意的是,在最后一次查询之后,我们不需要这个。
  • 我们在这里还进行简单的错误处理:检查作者 ID 是否无效。如果无效,我们将显示一条错误消息,然后退出。(请参见第 22 至 27 行。)
  • 在第 25 行和第 27 行,我们使用作者的 ID 创建查询,并将获取模式设置为 PDO::FETCH_ASSOC。然后我们继续显示数据:首先我们呈现作者的详细信息,然后呈现他的所有书籍。

现在您可以返回浏览器并将其指向 URL: http://localhost/author.php?id=1

应出现以下屏幕:

PDO Statements and Result Sets

如您所见,页面上的所有内容都是正确的:作者的详细信息,我们首先归档(id=1,也是该作者的唯一一本书。现在让我们看看应用程序对提交的无效 ID 的反应。我们知道我们只有三位作者,所以除 1、2 或 3 之外的任何数字都是无效的。此外,非数字参数将计算为 0,这是无效的。如果我们将地址栏中的 URL 更改为 http://localhost/author.php?id=zzz。我们将以以下内容结束:

PDO Statements and Result Sets

您还应该在 common.inc.php中切换到 SQLite,并看到此页面也可用于此数据库。

现在,让我们修改现有的 books.php文件,添加一个带有作者个人资料页面链接的作者栏。我们必须连接两个表,其中书的 author字段等于作者的 ID 字段,并选择作者的 ID、名字和姓氏。因此,我们的查询将如下所示:

SELECT authors.id, authors.firstName, authors.lastName, books.* FROM authors, books WHERE author=authors.id ORDER BY title;

在继续进行更改之前,让我们在命令行客户机中运行此查询。我们还将修改客户端的此查询,因为其窗口不适合整行:

mysql> SELECT authors.id, firstName, lastName, books.id, title FROM
authors, books WHERE books.author=authors.id;
+----+-----------+----------+----+------------------------------+
| id | firstName | lastName | id | title |
+----+-----------+----------+----+------------------------------+
| 1 | Marc | Delisle | 1 | Creating your MySQL... |
| 2 | Sohail | Salehi | 2 | ImageMagick Tricks | | 3 | Cameron | Cooper | 3 | Building Websites with Plone |
+----+-----------+----------+----+------------------------------+
3 rows in set (0.00 sec)

如您所见,查询返回两个名为 id的列。这意味着我们将无法使用 PDO::FETCH_ASSOC模式,因为只有 id数组索引。这里我们有两个选项:要么使用 PDO::FETCH_NUM模式,要么使用别名检索 ID 字段。

让我们看看如何使用 PDO::FETCH_NUM:对页面进行编码

<?php
/**
* This page lists all the books we have
* PDO Library Management example application
* @author Dennis Popel
*/
// Don't forget the include
include('common.inc.php');
// Issue the query
$q = $conn->query("SELECT authors.id, firstName, lastName, books.*
FROM authors, books WHERE author=authors.id ORDER
BY title");
$q->setFetchMode(PDO::FETCH_NUM);
// Display the header
showHeader('Books');
// now create the table
?>
<table width="100%" border="1" cellpadding="3">
<tr style="font-weight: bold">
<td>Author</td>
<td>Title</td>
<td>ISBN</td>
<td>Publisher</td>
<td>Year</td>
<td>Summary</td>
</tr>
<?php
// Now iterate over every row and display it
while($r = $q->fetch())
{
?>
<tr>
<td><a href="author.php?id=<?=$r[0]?>">
<?=htmlspecialchars("$r[1] $r[2]")?></a></td>
<td><?=htmlspecialchars($r[5])?></td>
<td><?=htmlspecialchars($r[6])?></td>
<td><?=htmlspecialchars($r[7])?></td>
<td><?=htmlspecialchars($r[8])?></td>
<td><?=htmlspecialchars($r[9])?></td>
</tr>
<?php
}
?>
</table>
<?php
// Display footer
showFooter();

注意高亮显示的行,它们包含更改;文件的其余部分是相同的。如您所见,我们添加了对 $q->setFetchMode()的调用,并将循环更改为使用数字列索引。

如果我们导航回 http://localhost/books.php,我们将看到一个类似于此屏幕截图中的列表:

PDO Statements and Result Sets

我们可以点击每个作者进入他们的个人资料页面。当然,在 common.inc.php中更改回 SQLite 也应该起作用。

另一个更好的选择是在 SQL 代码中为列名使用别名。如果我们这样做,就不必在每次从表中添加或删除列时都处理数字索引和更改代码。我们只需将 SQL 更改为以下内容:

SELECT authors.id AS authorId, firstName, lastName, books.* FROM
authors, books WHERE author=authors.id ORDER BY title;

books.php的最终版本如下:

<?php
/**
* This page lists all the books we have
* PDO Library Management example application
* @author Dennis Popel
*/
// Don't forget the include
include('common.inc.php');
// Issue the query
$q = $conn->query("SELECT authors.id AS authorId, firstName,
lastName, books.* FROM authors, books WHERE
author=authors.id
ORDER BY title");
$q->setFetchMode(PDO::FETCH_ASSOC);
// Display the header
showHeader('Books');
// now create the table
?>
<table width="100%" border="1" cellpadding="3">
<tr style="font-weight: bold">
<td>Author</td>
<td>Title</td>
<td>ISBN</td>
<td>Publisher</td>
<td>Year</td>
<td>Summary</td>
</tr>
<?php
// Now iterate over every row and display it
while($r = $q->fetch())
{
?>
<tr>
<td><a href="author.php?id=<?=$r['authorId']?>">
<?=htmlspecialchars("$r[firstName] $r[lastName]")?></a></td>
<td><?=htmlspecialchars($r['title'])?></td>
<td><?=htmlspecialchars($r['isbn'])?></td>
<td><?=htmlspecialchars($r['publisher'])?></td>
<td><?=htmlspecialchars($r['year'])?></td>
<td><?=htmlspecialchars($r['summary'])?></td>
</tr>
<?php
}
?>
</table>
<?php
// Display footer
showFooter();

注意,我们将取回模式改回了 PDO::FETCH_ASSOC。此外,我们在第 34 行中使用 $r['authorId']访问作者 ID,因为我们在查询中将该列别名为 authorId

PDO 还允许我们将所有结果提取到一个数组中。我们可能需要它来进一步处理或传递给某些函数。但是,这应该只对小的结果集执行。对于像我们这样的应用程序,这是非常不鼓励的,因为我们只显示书籍或作者的列表。将一个大的结果集提取到一个数组中需要为整个结果分配内存,而在本例中,我们逐行显示结果,因此这只需要为一行分配内存。

此方法称为 PDOStatement::fetchAll()。生成的数组可以是二维数组,也可以是对象列表,这取决于获取模式。这个方法接受所有的 PDO::FETCH_xxxx常量,就像 PDOStatement::fetch()一样。例如,我们可以按照以下方式重写我们的 books.php文件,以获得相同的结果。以下是 books.php第 9 行至第 46 行的相关部分:

// Issue the query
$q = $conn->query("SELECT authors.id AS authorId, firstName,
lastName, books.* FROM authors, books WHERE
author=authors.id ORDER BY title");
$books = $q->fetchAll(PDO::FETCH_ASSOC);
// Display the header
showHeader('Books');
// now create the table
?>
<table width="100%" border="1" cellpadding="3">
<tr style="font-weight: bold">
<td>Author</td>
<td>Title</td>
<td>ISBN</td>
<td>Publisher</td>
<td>Year</td>
<td>Summary</td>
</tr>
<?php
// Now iterate over every row and display it
foreach($books as $r)
{
?>
<tr>
<td><a href="author.php?id=<?=$r['authorId']?>">
<?=htmlspecialchars("$r[firstName] $r[lastName]")?></a></td>
<td><?=htmlspecialchars($r['title'])?></td>
<td><?=htmlspecialchars($r['isbn'])?></td>
<td><?=htmlspecialchars($r['publisher'])?></td>
<td><?=htmlspecialchars($r['year'])?></td>
<td><?=htmlspecialchars($r['summary'])?></td>
</tr>
<?php
}
?>
</table>

注意这里突出显示的几行,我们将整个结果提取到第 5 行的 $books数组中,然后在第 21 行使用 foreach循环对其进行迭代。如果您运行修改后的页面,您将看到我们收到相同的结果。如果我们在 common.inc.php文件中更改为 SQLite 数据库,这也会起作用。

PDOStatement::fetchAll()方法还允许我们使用 PDO::FETCH_COLUMN模式选择单个列的值。如果我们想使用上一个示例中的查询获取整本书的标题,我们可以执行以下操作(注意列的数量和顺序):

$q = $conn->query("SELECT authors.id AS authorId, firstName,
lastName, books.* FROM authors, books WHERE
author=authors.id ORDER BY title");
$books = $q->fetchAll(PDO::FETCH_COLUMN, 5);
var_dump($books);

这将产生以下输出:

array(3)
{
[0]=>
string(28) "Building Websites with Plone"
[1]=>
string(66) "Creating your MySQL Database: Practical Design Tips and
Techniques"
[2]=>
string(18) "ImageMagick Tricks"
}

如您所见,当请求单个列时,此方法返回一维数组。

检索结果集元数据

正如我们在上一节中所看到的, PDOStatement类允许我们检索有关结果集中包含的数据的一些信息。此信息称为元数据,您可能已经以某种方式使用了其中的一些信息。

当然,关于结果集最重要的元数据是它包含的行数。我们可以使用行计数来增强用户体验,例如,对长结果集进行分页。我们的示例库应用程序仍然很小,到目前为止只有三本书,但随着数据库的增长,我们肯定需要一些工具来获取显示的每个表的总行数,并对其进行分页以便于浏览。

传统上,您会使用 mysql_num_rows(), sqlite_num_rows()函数或 pg_num_rows()函数(取决于您的数据库)来获取查询返回的行总数。在 PDO 中,负责检索行数的方法称为 PDOStatement::rowCount()。但是,如果要使用以下代码对其进行测试:

$q = $conn->query("SELECT * FROM books ORDER BY title");
$q->setFetchMode(PDO::FETCH_ASSOC);
var_dump($q->rowCount());

您将看到,对于 MySQL 和 SQLite,PDO 都返回 0。这是因为 PDO 的操作不同于传统的数据库扩展。文档中说,“如果关联的 PDOStatement类执行的最后一条 SQL 语句是 SELECT语句,则某些数据库可能会返回该语句返回的行数。但是,并非所有数据库都能保证这种行为,也不应该这样做

“MySQL 和 SQLite 驱动程序都不支持此功能,这就是为什么此方法的返回值为 0。我们将在第 5 章中了解如何计算使用 PDO 返回的行数(因此这是一个真正的可移植方法)。

RDBMS不知道在检索到最后一行之前,查询将返回多少行。这样做是出于性能考虑。在大多数情况下,带有 WHERE子句的查询只返回表中存储的部分行,数据库服务器会尽最大努力确保此类查询尽可能快地执行。这意味着,当他们发现与 WHERE子句匹配的行时,他们就开始返回行。这比到达最后一行时要早得多。这就是为什么他们真的不知道会提前返回多少行。 mysql_num_rows(), sqlite_num_rows()函数或 pg_num_rows()函数对已预取到内存中的结果集(缓冲查询)进行操作。PDO 的默认行为是使用无缓冲查询。我们将在第 6 章中讨论 MySQL 缓冲查询。

另一个有趣的方法是 PDOStatement::columnCount()方法,它返回结果集中的列数。当我们执行任意查询时,它非常方便。(例如,像 phpMyAdmin这样的数据库管理应用程序可以充分利用这种方法,因为它允许用户键入任意 SQL 查询。)我们可以通过以下方式使用它:

$q = $conn->query("SELECT authors.id AS authorId, firstName,
lastName, books.* FROM authors, books WHERE
author=authors.id ORDER BY title");
var_dump($q->columnCount());

这将显示我们的查询返回一个包含 10 列的结果集(来自books表的 7 列和来自authors表的 3 列)。

不幸的是,PDO 当前不允许您从它所属的结果集中检索表或特定列的名称。如果应用程序使用连接两个或多个表的查询,则此功能非常有用。在这种情况下,可以获取给定数字索引的每一列的表名,从 0 开始。但是,正确使用列别名可以消除使用此类功能的需要。例如,当我们修改图书列表页面以显示作者姓名时,我们为作者的 ID 列添加了别名以避免名称冲突。该别名清楚地将该列标识为属于 authors表。

总结

在本章中,我们使用 PDO 迈出了第一步,甚至创建了一个运行在两个不同数据库上的小型数据库驱动动态应用程序。现在,您应该能够使用构造连接字符串的规则连接到任何支持的数据库。然后,您应该能够对其运行查询,并遍历和显示结果集。

在下一章中,我们将讨论任何数据库驱动的应用程序错误处理的一个非常重要的方面。我们还将扩展我们的示例应用程序,使其能够添加和编辑书籍和作者,从而使其更加现实和有用。