源/https://www.startutorial.com/articles/view/modern-php-developer-pdo
译/Lemon黄
本文篇幅较长,可能花费您十来分钟
PHP数据对象(PHP Data Objects),简称为PDO,是为解决数据库访问问题而构建的PHP扩展。它提供了访问数据库的统一接口。
PDO为数据访问创建了一个抽象层,因此开发人员可以在不担心底层数据库引擎的情况下编写可移植代码。通俗地说,使用PDO你可以开发一个使用MySQL作为数据库存储的应用程序。如果您想在任何时间点切换到PostgreSQL数据库,您需要做的就是更改PDO驱动程序。而不需要更改其他代码。
PDO由三种主要类型的对象组成:PDO对象、PDOStatement对象和 PDOException对象。我们不应忽视PDO驱动程序,但这三种类型的对象一起构成PDO扩展的主接口。
1 为什么使用PDO
如果您以前开发过任何MySQL数据库驱动的应用程序,但从未尝试过PDO,您一定想知道使用PDO的好处是什么,尤其是将它与它的两个将要替代的方案进行比较时。之前的2中方案如下:
1.1、MySQL:
与MySQL交互的最早的方式是使用mysql扩展。它是在PHP 2.0.0中引入的,但是从PHP 5.5.0开始就被弃用了,并且已经在PHP7.0.0中被剔除了。考虑到在较新的PHP 版本中不支持此扩展,因此不建议使用此扩展。
1.2、MySQLi:
从PHP 5.0.0开始,mysql扩展的一个改进版本mysqli被引入。与mysql扩展相比,它带来了很多好处,如面向对象的接口、prepare语句、多语句、事务支持、增强的调试功能和嵌入式服务器支持。
MySQLi和PDO的主要区别在于:
所以,我们建议使用PDO来构建应用程序,因为:
总之,我们要强烈建议的是使用PDO,但也决不能禁止您使用MySQLi。
在下面的小节中,我们将从使用PDO运行查询的一些常见方法开始。然后我们将演示如何使用PDO执行各种MySQL 数据操作语句。最后,我们将重点介绍几个PDO APIs,它们的用途相同,但方式不同。
2 运行PDO查询
我们总结了四个类别的不同方法来运行PDO查询方式,它们按查询执行结果所涉及的步骤数量来分类。这些类别是试图为了简化记忆PDO API,这四种方式包括如下:
2.1 、建立数据库连接:
在我们进入以上每个查询类别之前,我们首先需要熟悉使用PDO建立数据库连接。这是PDO 的绝对基础,如下面代码所示,每段有数据库操作相关的代码都会用到它:
try {
$dbh = new PDO('mysql:host=localhost;dbname=customers', $user, $pass);
} catch (PDOException $e) {
die($e->getMessage());
}
为了建立数据库连接,我们用三个参数实例化了一个PDO 对象。第一个参数指定一个数据库源(称为DSN),它由PDO驱动程序名称、后跟冒号、其次是PDO驱动程序特定的连接语法组成。第二个和第三个参数是数据库用户名和密码。
如果连接失败,将产生异常。我们可以通过捕获异常来优雅的处理它。值得庆幸的是,在这种情况下,我们不再需要把连接放在if语句中,因为它有一个干净且易于阅读的代码库(try...catch语句)。
在下面的代码示例中,我们将忽略这段代码,以避免混乱。记住,在进行任何PDO操作之前,总是需要先建立连接。
2.2、方式一,exec:
这是运行查询的最简单形式。我们可以使用它快速运行一个查询,通常我们不希望它返回任何结果。
$dbh->exec('INSERT INTO customers VALUES (1, "Andy")');
尽管PDO::exec不返回与查询对应的结果,但它确实也返回了一些内容。无论使用PDO::exec运行什么查询,成功时都会返回受影响的行数。失败时还返回false。所以我们应该要有像如下的示例代码来验证是否运行查询成功:
if (FALSE === $dbh->exec('INSERT INTO customers VALUES (1, "Andy")')) {
throw new MyException('Invalid sql query');
}
如果我们直接根据用户的输入来构建查询字符串,并手动处理安全性问题,那么就应该使用其他选项查询类型语句,这将在接下来讨论。
2.3、方式二,query fetch:
当运行诸如select语句之类的查询时,我们确实希望返回相应的结果。最简单的方法如下:
$statement = $dbh->query('SELECT * FROM customers');
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
echo $row['id'] . ' ' . $row['name'] . PHP_EOL;
}
注意 $dbh->query()
和$statement->fetch()
,我们的类别命名就是通过PDO API的调用顺序来命名的。
因为PDO::query在成功时将结果集作为PDOStatement 对象返回(失败时将返回布尔值false,如果要验证,请执行与PDO::exec类似的检查)。PDOStatement 类实现可Traversable 接口吗,该接口是迭代器的基本接口,这也意味着它可以在循环等迭代语句中使用。以下,是一个简短的循环迭代输出示例代码:
foreach ($dbh->query('SELECT * FROM customers', PDO::FETCH_ASSOC) as $row) {
echo $row['id'] . ' ' . $row['name'] . PHP_EOL;
}
您可能已经注意到,当调用 PDO::query 或者PDOStatement::fetch时,我们提供了一个标志参数。这个参数指定我们需要的数据结构类型。这些标志参数包括如下:
还有很多参数选项。我们建议在php手册中快速查看它们。虽然这个参数是可选的,但是我们应该总是指定它,除非我们真的想要一个用列名和数字索引的数组。因为PDO::FETCH_BOTH需要占用两倍的内存。
2.4、方式三,prepare execute fetch:
我们经常需要接受用户的输入去运行数据库查询。以下有两者主要的问题,如果还是使用query fetch 的查询方法:
首先,我们必须确保传递给PDO::query的SQL语句是安全的。对于转义和引用的输入值必须得到很好的处理。其次,PDO::query在一个函数调用中执行SQL语句,这意味着如果我们需要多次运行同一个查询,它将使用多次资源。而这有一种更好的方法。
PDO首次引入prepare 语句。那么什么是prepare 语句呢?根据维基百科:
在数据库管理系统中,一个准备好的语句或参数化语句是用来重复执行相同或相似的数据库语句的一个特征。通常与SQL语句(如查询或更新)一起使用,准备好的语句采用模板的形式,在每次执行期间将某些常量值替换到模板中。
prepare语句解决了上面提到的两个问题。它不仅提高了运行多个类似查询的效率,而且还能处理好含有转义和引用的用户输入值。
下面是我们如何使用PDO实现prepare语句:
$users = ['Andy', 'Tom'];
$statement = $dbh->prepare('SELECT * FROM customers where name = :name');
foreach ($users as $user) {
$statement->execute([':name' => $user]);
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
echo $row['id'];
}
}
注意以下是我们归纳的步骤:
2.5、方式四,prepare bind execute fetch:
在以上代码,你可能会观察到一个小问题,在prepare 语句中如果有很多参数时会发生什么情况。我们可以轻松地创建如下的代码:
$statement->execute([':name' => $user, ':mobile' => $mobile, ':address' => $address ]);
这个列表可以一直列下去。这也使得代码很难阅读。但是,这里要注意的一件更重要的事情是,PHP会验证列表的值是否和数据库中的字段数据类型相匹配,列表这么多的数据,这就很容易产生不匹配错误。
解决这个问题可以使用PDOStatement::bindValue,如下是使用的方式:
$users = ['Andy', 'Tom'];
$statement = $dbh->prepare('SELECT * FROM customers where name = :name');
foreach ($users as $user) {
$statement->bindValue(':name', $user, PDO::PARAM_STR);
$statement->execute();
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
echo $row['id'];
}
}
我们使用PDOStatement::binValue来替代PDOStatement::execute来将值绑定到参数。它为我们的代码添加了一些如下所述的重要改进:
以上四种PDO查询方式绝对不是官方的:他们只是我们未为了方便使用而自定义命名的方式。没有必要严格遵守它们。事实上,大部分时间我们会把这些查询方式结合在一起。
3 PDO数据操作
让我们把学到的东西付诸行动。在本节中,我们将使用pdo来完成一些最常见的MySQL 任务。
3.1、创建简单的数据表:
开始之前,我们来创建一个可以演示的简单的数据表。
CREATE TABLE IF NOT EXISTS `customers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
3.2、插入数据:
我们第一个任务就是往数据表中插入数据。对于这个案例,假设我们通过POST请求接受来自于用户输入表单的数据,然后将表单中数据插入到customers 表中:
try {
$dbh = new PDO('mysql:host=localhost;dbname=inventory', 'root', 'root');
} catch (PDOException $e) {
die($e->getMessage());
}
$name = $_POST['name'];
$statement = $dbh->prepare('INSERT INTO customers (name) VALUES (:name)');
if (false === $statement) {
throw new Exception('Invalid prepare statement');
}
if (false === $statement->execute([':name' => $name])) {
throw new Exception(implode(' ', $statement->errorInfo()));
}
我们使用prepare->execute->fetch这种方式来处理数据库查询,但是我们删除了fetch部分,因为我们不希望它返回任何结果集。步骤解释如下:
3.3、更新数据:
第二个任务是更新数据表中已经存在的数据。假设用例与以前的情况相同,除了用户能够传递一个附加参数($id)
try {
$dbh = new PDO('mysql:host=localhost;dbname=inventory', 'root', 'root');
} catch (PDOException $e) {
die($e->getMessage());
}
$id = $_POST['id'];
$name = $_POST['name'];
$statement = $dbh->prepare('UPDATE customers SET name = :name WHERE id = :id');
if (false === $statement) {
throw new Exception('Invalid prepare statement');
}
if (false === $statement->execute([':name' => $name, ':id' => $id])) {
throw new Exception(implode(' ', $statement->errorInfo()));
}
正如您可能已经猜到的,除了附加参数$ id,该代码与前面的代码示例相同。
3.4、删除数据:
第三个任务是删除数据表中已经存在的数据。用户可以传递单个单数($id),并且能够删除$id对应的记录。
try {
$dbh = new PDO('mysql:host=localhost;dbname=inventory', 'root', 'root');
} catch (PDOException $e) {
die($e->getMessage());
}
$id = $_POST['id'];
$statement = $dbh->prepare('DELETE from customers WHERE id = :id');
if (false === $statement) {
throw new Exception('Invalid prepare statement');
}
if (false === $statement->execute([':id' => $id])) {
throw new Exception(implode(' ', $statement->errorInfo()));
}
同样,示例还是与之前的示例代码相似(prepare->execute->fetch)。这就是PDO的“美丽之处”——它的面向对象设计使代码易于编写。
3.5、查询数据:
最后一个任务是从customers中查询所有的数据记录,查询数据不会要求用户输入数据。
try {
$dbh = new PDO('mysql:host=localhost;dbname=inventory', 'root', 'root');
} catch (PDOException $e) {
die($e->getMessage());
}
$results = array();
$statement = $dbh->query('SELECT * FROM customers');
if (false === $statement) {
throw new Exception('Invalid query');
}
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
$results[] = $row['name'];
}
在本例中,我们使用了query->fetch这种方式。如前所述,在调用PDOStatement::fetch时,总是指定fetch模式是一个好习惯。
现在我们已经完成了使用PDO的各种任务的快速示例。这些示例非常简单,但它们向我们展示了PDO提供的一个非常容易使用和一致的API。
4 PDO API
到目前为止,我们已经演示了一些常见的 PDO APIs。PDO 还提供了比我们上面展示的更多的功能。在本节中,我们将最后一次探索PDO API,并尽量覆盖尽可能多的API。几乎不可能涵盖本主题的每一个方面,但请始终记住,当您有疑问时,可以参考官方手册页。
4.1、Fetch 方法:
我们讨论过了一个(PDOStatement::fetch)用于检索结果集的获取方法。事实上,PDOStatement 提供了三种额外的获取方法。
4.1.1、PDOStatement::fetchAll方法:
与PDOStatement::fetch相似,PDOStatement::fetchAll方法中的第一个参数也是一个标注参数,用于指定获取的数据模式。好的习惯,虽然是可选的,但我们应该要指定这个参数。不同于PDOStatement::fetch,PDOStatement::fetchAll会一次性返回所有结果。
$statement = $dbh->query('SELECT * FROM customers');
$result = $statement->fetchAll(PDO::FETCH_ASSOC);
print_r($result);
// Output
Array
(
[0] => Array (
[id] => 2
[name] => TEST2
)
[1] => Array (
[id] => 3
[name] => TEST2
)
[2] => Array (
[id] => 4
[name] => TEST2
)
)
这种方法会产生一个警告是,因为它一次加载所有的结果集,它可能会导致内存占用很大,这取决于有多少数据可用。所以应该注意使用这种方法。
4.1.2、PDOStatement::fetchColumn方法:
从所需列检索数据的一种简便方法是使用PDOStatement::fetchColumn。它从结果集的下一行返回一列。它类似于PDOStatement::fetch,但是它只返回下一个单独的列,而不是下一个结果集数组。
$statement = $dbh->query('SELECT id, name FROM customers');
while($result = $statement->fetchColumn(1)) {
echo $result . PHP_EOL;
}
PDOStatement::fetchColumn可以选择接受单个参数(列名)。该参数是指定从中检索数据的列从0开始的索引编号。当该参数被省略时,它默认为列编号0。使用这个方法需要注意两点:
4.1.3、PDOStatement::fetchObject方法:
此方法是PDO::FETCH_CLASS或PDO::FETCH_OBJ样式的 PDOStatement::fetch()的替代方法。其目的是使我们的代码在单独调用时更易于阅读,当调用此方法时,它将以php对象的形式返回下一个结果集:
$statement = $dbh->query('SELECT id, name FROM customers');
while($object = $statement->fetchObject()) {
print_r($object);
}
// Output
stdClass Object
(
[id] => 2
[name] => TEST2
)
stdClass Object
(
[id] => 3
[name] => TEST2
)
我们还可以将自定义PHP类作为第一个参数传递,PHP将实例化自定义PHP对象的一个实例,并将其检索并返回:
class MyClass
{
}
$statement = $dbh->query('SELECT id, name FROM customers');
while($object = $statement->fetchObject('MyClass')) {
print_r($object);
}
// Output
MyClass Object
(
[id] => 2
[name] => TEST2
)
MyClass Object
(
[id] => 3
[name] => TEST2
)
4.2、Bind 方法:
以前,我们使用过PDOStatement::bindValue来绑定。此方法将所需的值绑定到查询的占位符。不过,这种方法并不是完成这项任务的唯一方法。
4.3、绑定参数:
这个方法几乎与PDOStatement::bindValue相同,有些人可以互换地使用这两个方法也就不足为奇了,但是这两个方法之间有很大的不同,如果你不知道的话,这可能会花你一大笔精力。
与 PDOStatement::bindValue()不同,该变量被绑定为引用,并且仅在调用 PDOStatement::execute()时才被评估。
我们通过举个例子来了解一下:
$user = 'Andy';
$statement = $dbh->prepare('SELECT * FROM customers where name = :name');
$statement->bindValue(':name', $user, PDO::PARAM_STR);
$user = 'Tom';
$statement->execute();
echo $statement->fetchColumn(1);
// Output Andy
$user = 'Andy';
$statement = $dbh->prepare('SELECT * FROM customers where name = :name');
$statement->bindParam(':name', $user, PDO::PARAM_STR);
$user = 'Tom';
$statement->execute();
echo $statement->fetchColumn(1);
// Output Tom
你能看出区别吗?这两个代码是相同的,除了一个是使用$statement->bindParam,另一个是使用$statement->bindValue。但他们结果完全不同。
PDOStatement::bindParam将变量$user绑定为引用。在PDOStatement::execute被调用时,$user如果发生改变,那么结果也会发生改变,因为在这里$user是一个引用。
一定要了解这两者的区别,并根据自己的需要进行选择。不鼓励在没有适当考虑的情况下改变这两种方法。
4.4、绑定列
与 PDOStatement::bindValue和PDOStatement::bindParam不同,此方法不是绑定变量到prepare 语句的方法。事实上,它恰恰相反:它将结果集中的列绑定到php局部变量。
这是一个有趣的观察方法。之前,我们讨论了一个方法PDOStatement::fetchObject,可以将结果集返回为定义的对象。在这里,使用PDOStatement::bindColumn,我们可以将结果集中的列绑定到变量。
$statement = $dbh->prepare('SELECT id, name FROM customers');
$statement->bindColumn('name', $name);
$statement->execute();
while ($statement->fetch(PDO::FETCH_ASSOC)) {
echo $name . PHP_EOL;
}
指定表列的第一个参数,既接受字符串列名,也接受字符串编号作为值。所以下面的内容也是有效的。
$statement->bindColumn(1, $name);
5 PDO 条件
在最后一节中,我们将讨论与使用PDO时的一些技巧。
5.1、IN 子句:
在prepare 语句中构建IN子句是一项有趣的任务。看看下面的代码,想象一下这就是我们需要构建的:
$users = ['Andy', 'Tom'];
$statement = $dbh->prepare('SELECT * FROM customers where name IN :name');
$statement->execute($user);
乍一看,这似乎是合法的。仔细看看。它不起作用,因为prepare语句只接受标量类型(例如string、int等)。
最终的任务是构建一个包含相同问号的,以逗号分隔的字符串(?)来绑定数组变量。这就是我们如何构建一个合法的子句串。
$users = ['Andy', 'Tom'];
$placeholder = implode(',', array_fill(0, count($users), '?'));
$statement = $dbh->prepare('SELECT * FROM customers where name IN '. $placeholder); $statement->execute($users);
5.2、通配符:
在构建like子句时,我们可能会这样做:
$name = 'Andy';
$statement = $dbh->prepare('SELECT count(*) FROM customers where name LIKE %:name%');
$statement->bindValue(':name', $name);
然而,这在PDO中是行不通的。我们需要将通配符移到变量本身:
$name = '%Andy%';
$statement = $dbh->prepare('SELECT count(*) FROM customers where name LIKE :name');
$statement->bindValue(':name', $name);