使用 PDO

到目前为止,我们已经使用过 MySQL,您已经对它的功能有了很好的了解。不过,连接到客户端并手动执行查询并不是我们的目标。我们要实现的是,我们的应用程序可以自动利用数据库。为此,我们将使用一组 PHP 自带的类,让您可以连接到数据库并从代码中执行查询。

PHP 数据对象(PDO)是连接到数据库并允许与之交互的类。对于 PHP 开发人员来说,这是使用数据库的常用方法,尽管还有其他方法,我们在此不做讨论。PDO 允许你使用不同的数据库系统,因此你不必只局限于 MySQL。在下面的章节中,我们将讨论如何使用该类连接数据库、插入数据和检索数据。

连接到数据库

为了连接到数据库,最好将凭证(即用户和密码)与代码分开保存在配置文件中。在使用 Config 类时,我们已经有了 config/app.json 文件。让我们为数据库添加正确的凭证。如果默认情况下有配置,配置文件应该与此相似:

{
    "db": {
        "user": "root",
        "password": ""
    }
}

开发人员通常会指定与连接相关的其他信息,如主机、端口或数据库名称。这取决于你的应用程序是如何安装的,MySQL 是否运行在不同的服务器上等等,你可以自行决定在代码和配置文件中保留多少信息。

为了连接数据库,我们需要从 PDO 类中实例化一个对象。该类的构造函数需要三个参数: 数据源名称 (DSN),这是一个字符串,表示要使用的数据库类型;用户名;以及密码。我们已经从 Config 类中获得了用户名和密码,但仍需构建 DSN。

MySQL 数据库的格式之一是 <database type>:host=<host>;dbname=<schema name>。由于我们的数据库系统是 MySQL,运行在同一台服务器上,而模式名称是 bookstore,因此 DSN 将是 mysql:host=127.0.0.1;dbname=bookstore。让我们看看如何将所有内容整合在一起:

$dbConfig = Config::getInstance()->get('db');
$db = new PDO(
    'mysql:host=127.0.0.1;dbname=bookstore',
    $dbConfig['user'],
    $dbConfig['password']
);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

还请注意,我们将调用 PDO 实例的 setAttribute 方法。该方法允许你为连接设置一些选项;在本例中,它设置了来自 MySQL 的结果格式。该选项强制 MySQL 返回键为字段名称的数组,这比默认的根据字段顺序返回数字键更有用。现在设置该选项将影响使用 $db 实例执行的所有查询,而不是每次执行查询时都设置该选项。

执行查询

从数据库中检索数据的最简单方法是使用查询方法。该方法接受字符串形式的查询,并返回数组形式的行列表。我们来看一个例子:在数据库连接初始化后编写以下内容,例如在 init.php 文件中:

$rows = $db->query('SELECT * FROM book ORDER BY title');
foreach ($rows as $row) {
    var_dump($row);
}

该查询试图获取数据库中的所有图书,并按书名排序。这可能是一个函数(如 getAllBooks)的内容,我们在显示目录时会用到这个函数。每一行都是一个数组,其中包含作为键的所有字段和作为值的数据。

如果您在浏览器上运行该应用程序,您将得到以下结果:

image 2023 11 02 21 45 34 371

当我们要检索数据时,查询函数非常有用,但为了执行插入行的查询,PDO 提供了 exec 函数。该函数的第一个参数也是字符串,定义要执行的查询,但它返回一个布尔值,说明执行是否成功。一个很好的例子是尝试插入书籍。键入以下内容:

$query = <<<SQL
INSERT INTO book (isbn, title, author, price)
VALUES ("9788187981954", "Peter Pan", "J. M. Barrie", 2.34)
SQL;
$result = $db->exec($query);
var_dump($result); // true

这段代码还使用了一种新的字符串表示方法:heredoc。我们将在 <<<SQLSQL; 之间括起字符串,这两个字符串都在不同的行中,而不是引号。这样做的好处是可以在多行中写入字符串,并带有制表符或其他空格,PHP 也会尊重它。我们可以构建易于阅读的查询,而不是将它们写在一行中,或不得不将不同的字符串连接起来。请注意,SQL 是用标记来表示字符串的开始和结束,但您也可以使用任何您认为合适的文本。

第一次使用此代码运行应用程序时,查询将成功执行,因此结果将是布尔值 true。但是,如果再次运行,结果将是 false,因为我们插入的 ISBN 是相同的,但我们将其限制设置为唯一。

知道查询失败是有用的,但如果我们知道原因就更好了。PDO 实例中的 errorInfo 方法会返回一个数组,其中包含上次错误的信息。键 2 包含描述,因此它可能是我们会经常使用的。用以下代码更新之前的代码:

$query = <<<SQL
INSERT INTO book (isbn, title, author, price)
VALUES ("9788187981954", "Peter Pan", "J. M. Barrie", 2.34)
SQL;
$result = $db->exec($query);
var_dump($result); // false
$error = $db->errorInfo()[2];
var_dump($error); // Duplicate entry '9788187981954' for key 'isbn'

结果是查询失败,因为 ISBN 条目重复。现在,我们可以为客户编写更有意义的错误信息,或仅用于调试。

预编译语句

当你需要运行总是相同的快速查询时,前两个函数非常有用。但是,在第二个示例中,您可能会注意到查询字符串并不是很有用,因为它总是插入相同的书。虽然您确实可以用变量替换这些值,但这并不是好的做法,因为这些变量通常来自用户端,可能包含恶意代码。最好首先对这些值进行消毒。

PDO 提供了准备语句的功能,也就是参数化查询。您可以为查询中要更改的字段指定参数,然后为这些参数赋值。让我们先看下面的一个示例:

$query = 'SELECT * FROM book WHERE author = :author';
$statement = $db->prepare($query);
$statement->bindValue('author', 'George Orwell');
$statement->execute();
$rows = $statement->fetchAll();
var_dump($rows);

该查询是一个普通的查询,只是其中的 :author 代替了我们要查找的作者字符串。这是一个参数,我们将使用前缀 : 来标识它们。prepare 方法将查询作为参数,并返回一个 PDOStatement 实例。该类包含多个方法,用于绑定值、执行语句、获取结果等。在这段代码中,我们只使用了其中的三个,如下所示:

  • bindValue:它需要两个参数:查询中描述的参数名称和要分配的值。如果提供的参数名称不在查询中,则会出现异常。

  • execute:这将把查询发送到 MySQL,并用提供的值替换参数。如果有任何参数未赋值,该方法将抛出异常。和它的兄弟 exec 一样,execute 将返回一个布尔值,说明查询是否成功执行。

  • fetchAll:如果是 SELECT 查询,该方法将从 MySQL 获取数据。作为查询,fetchAll 将以数组形式返回所有记录的列表。

如果您尝试一下这段代码,就会发现结果与使用查询时非常相似;不过,这次的代码更加动态,因为您可以将其重复用于任何需要的作者。

image 2023 11 02 22 17 14 073

除了使用 bindValue 方法外,还有另一种将值绑定到查询参数的方法。您可以准备一个数组,其中 key 是参数的名称,value 是要为其赋值的值,然后将其作为 execute 方法的第一个参数发送。这种方法非常有用,因为通常您已经准备好了这个数组,不需要多次调用 bindValue 来获取其内容。添加此代码以进行测试:

$query = <<<SQL
INSERT INTO book (isbn, title, author, price)
VALUES (:isbn, :title, :author, :price)
SQL;
$statement = $db->prepare($query);
$params = [
    'isbn' => '9781412108614',
    'title' => 'Iliad',
    'author' => 'Homer',
    'price' => 9.25
];
$statement->execute($params);
echo $db->lastInsertId(); // 8

在上一个示例中,我们创建了一个新账簿,几乎包含了所有参数,但我们没有指定 ID,这是我们希望的行为,因为我们希望 MySQL 为我们选择一个有效的 ID。但是,如果你想知道插入行的 ID,该怎么办呢?你可以在 MySQL 中查询具有相同 ISBN 的图书,这样返回的记录中就会包含 ID,但这似乎很费事。相反,PDO 提供了 lastInsertId 方法,它可以返回主键最后插入的 ID,从而省去了一次额外的查询。