前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【译】现代化的PHP开发--PDO

【译】现代化的PHP开发--PDO

作者头像
Lemon黄
发布2019-10-10 14:59:53
1.9K0
发布2019-10-10 14:59:53
举报
文章被收录于专栏:Lemon黄Lemon黄

源/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支持客户端prepared语句,而mysqli不支持。我们将在后面的章节中详细讨论客户端prepared语句。这意味着如果所选的数据库服务器不支持MySQLi,数据库将模拟prepared语句。
  • MySQL支持面向对象API和过程API,而PDO则使用面向对象API。
  • 使用PDO的最大优点是可以编写可移植的代码。它使开发人员能够轻松地切换数据库,而mysqli只支持mysql数据库。

所以,我们建议使用PDO来构建应用程序,因为:

  • PDO有利于开发者编写可移植的代码
  • PDO支持面向对象编程

总之,我们要强烈建议的是使用PDO,但也决不能禁止您使用MySQLi。

在下面的小节中,我们将从使用PDO运行查询的一些常见方法开始。然后我们将演示如何使用PDO执行各种MySQL 数据操作语句。最后,我们将重点介绍几个PDO APIs,它们的用途相同,但方式不同。

2 运行PDO查询

我们总结了四个类别的不同方法来运行PDO查询方式,它们按查询执行结果所涉及的步骤数量来分类。这些类别是试图为了简化记忆PDO API,这四种方式包括如下:

  • exec
  • query fetch
  • prepare execute fetch
  • prepare bind execute fetch

2.1 、建立数据库连接:

在我们进入以上每个查询类别之前,我们首先需要熟悉使用PDO建立数据库连接。这是PDO 的绝对基础,如下面代码所示,每段有数据库操作相关的代码都会用到它:

代码语言:javascript
复制
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:

这是运行查询的最简单形式。我们可以使用它快速运行一个查询,通常我们不希望它返回任何结果。

代码语言:javascript
复制
$dbh->exec('INSERT INTO customers VALUES (1, "Andy")');

尽管PDO::exec不返回与查询对应的结果,但它确实也返回了一些内容。无论使用PDO::exec运行什么查询,成功时都会返回受影响的行数。失败时还返回false。所以我们应该要有像如下的示例代码来验证是否运行查询成功:

代码语言:javascript
复制
if (FALSE === $dbh->exec('INSERT INTO customers VALUES (1, "Andy")')) {
    throw new MyException('Invalid sql query');
}

如果我们直接根据用户的输入来构建查询字符串,并手动处理安全性问题,那么就应该使用其他选项查询类型语句,这将在接下来讨论。

2.3、方式二,query fetch:

当运行诸如select语句之类的查询时,我们确实希望返回相应的结果。最简单的方法如下:

代码语言:javascript
复制
$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 接口吗,该接口是迭代器的基本接口,这也意味着它可以在循环等迭代语句中使用。以下,是一个简短的循环迭代输出示例代码:

代码语言:javascript
复制
foreach ($dbh->query('SELECT * FROM customers', PDO::FETCH_ASSOC) as $row) {
    echo $row['id'] . ' ' . $row['name'] . PHP_EOL;
}

您可能已经注意到,当调用 PDO::query 或者PDOStatement::fetch时,我们提供了一个标志参数。这个参数指定我们需要的数据结构类型。这些标志参数包括如下:

  • PDO::FETCH_ASSOC: 返回按列名索引的关联数组。
  • PDO::FETCH_NUM:返回数值索引数组。
  • PDO::FETCH_BOTH (default):返回按结果集中返回的列名和0索引列号索引的数组。(PDO::FETCH_ASSOC和PDO::FETCH_NUM的组合)。

还有很多参数选项。我们建议在php手册中快速查看它们。虽然这个参数是可选的,但是我们应该总是指定它,除非我们真的想要一个用列名和数字索引的数组。因为PDO::FETCH_BOTH需要占用两倍的内存。

2.4、方式三,prepare execute fetch:

我们经常需要接受用户的输入去运行数据库查询。以下有两者主要的问题,如果还是使用query fetch 的查询方法:

首先,我们必须确保传递给PDO::query的SQL语句是安全的。对于转义和引用的输入值必须得到很好的处理。其次,PDO::query在一个函数调用中执行SQL语句,这意味着如果我们需要多次运行同一个查询,它将使用多次资源。而这有一种更好的方法。

PDO首次引入prepare 语句。那么什么是prepare 语句呢?根据维基百科:

在数据库管理系统中,一个准备好的语句或参数化语句是用来重复执行相同或相似的数据库语句的一个特征。通常与SQL语句(如查询或更新)一起使用,准备好的语句采用模板的形式,在每次执行期间将某些常量值替换到模板中。

prepare语句解决了上面提到的两个问题。它不仅提高了运行多个类似查询的效率,而且还能处理好含有转义和引用的用户输入值。

下面是我们如何使用PDO实现prepare语句:

代码语言:javascript
复制
$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'];
    }
}

注意以下是我们归纳的步骤:

  • PDO::prepare用于创建包含变量参数的sql查询。参数命名约定是由冒号(:)前缀命名的变量或者用问号(?)替代。
  • PDOStatement::execute被调用来执行一个带有参数值的查询。当问号“?”在prepare语句中使用,表示的是编号的参数。我们可以使用数字索引数组绑定值。注意在foreach中,它在绑定值之后使用相同的语句执行查询。PDOStatement::execute执行失败返回false。我们可以使用PDOStatement::errorInfo()获取与操作相关联的错误信息。
  • PDOStatement::fetch用于获取具有所需数据结构的结果。

2.5、方式四,prepare bind execute fetch:

在以上代码,你可能会观察到一个小问题,在prepare 语句中如果有很多参数时会发生什么情况。我们可以轻松地创建如下的代码:

代码语言:javascript
复制
  
$statement->execute([':name' => $user, ':mobile' => $mobile, ':address' => $address ]);

这个列表可以一直列下去。这也使得代码很难阅读。但是,这里要注意的一件更重要的事情是,PHP会验证列表的值是否和数据库中的字段数据类型相匹配,列表这么多的数据,这就很容易产生不匹配错误。

解决这个问题可以使用PDOStatement::bindValue,如下是使用的方式:

代码语言:javascript
复制
$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来将值绑定到参数。它为我们的代码添加了一些如下所述的重要改进:

  • 可读性:它使代码易于为其他开发人员阅读,因为它指示了参数应该接受的确切数据类型。
  • 维护性:指定传递变量的数据类型的第三个参数,防止PHP产生不兼容的数据类型,容易产生错误。从长远来看,这也使得代码更易于维护,因为未来的开发人员一眼就能看到数据类型。

以上四种PDO查询方式绝对不是官方的:他们只是我们未为了方便使用而自定义命名的方式。没有必要严格遵守它们。事实上,大部分时间我们会把这些查询方式结合在一起。

3 PDO数据操作

让我们把学到的东西付诸行动。在本节中,我们将使用pdo来完成一些最常见的MySQL 任务。

3.1、创建简单的数据表:

开始之前,我们来创建一个可以演示的简单的数据表。

代码语言:javascript
复制
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 表中:

代码语言:javascript
复制
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部分,因为我们不希望它返回任何结果集。步骤解释如下:

  • 第一步是,创建数据库连接。
  • 第二步,创建一个prepare语句。注意,我们通过try...catch来处理出现异常情况。
  • 最后,我们执行prepare语句。对可能出现的异常也进行了处理。我们可以通过调用PDOStatement::errorInfo方法输出有用的信息。

3.3、更新数据:

第二个任务是更新数据表中已经存在的数据。假设用例与以前的情况相同,除了用户能够传递一个附加参数($id)

代码语言:javascript
复制
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对应的记录。

代码语言:javascript
复制
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中查询所有的数据记录,查询数据不会要求用户输入数据。

代码语言:javascript
复制
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会一次性返回所有结果。

代码语言:javascript
复制
$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,但是它只返回下一个单独的列,而不是下一个结果集数组。

代码语言:javascript
复制
$statement = $dbh->query('SELECT id, name FROM customers');
while($result = $statement->fetchColumn(1)) {
    echo $result . PHP_EOL;
}

PDOStatement::fetchColumn可以选择接受单个参数(列名)。该参数是指定从中检索数据的列从0开始的索引编号。当该参数被省略时,它默认为列编号0。使用这个方法需要注意两点:

  • PDOStatement::fetchColumn在到达结果集末尾时将返回boolean false,因此不应使用它从数据库中检索boolean类型。
  • PDOStatement::fetchColumn在调用它时将指针向前移动一步,因此无法从同一行检索另一列。(显然,当我们使用不同的列号调用指针时,它已经移动到下一行了)。

4.1.3、PDOStatement::fetchObject方法:

此方法是PDO::FETCH_CLASS或PDO::FETCH_OBJ样式的 PDOStatement::fetch()的替代方法。其目的是使我们的代码在单独调用时更易于阅读,当调用此方法时,它将以php对象的形式返回下一个结果集:

代码语言:javascript
复制
$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对象的一个实例,并将其检索并返回:

代码语言:javascript
复制
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()时才被评估。

我们通过举个例子来了解一下:

代码语言:javascript
复制
$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,我们可以将结果集中的列绑定到变量。

代码语言:javascript
复制
$statement = $dbh->prepare('SELECT id, name FROM customers');
$statement->bindColumn('name', $name);
$statement->execute();
while ($statement->fetch(PDO::FETCH_ASSOC)) {
    echo $name . PHP_EOL;
}

指定表列的第一个参数,既接受字符串列名,也接受字符串编号作为值。所以下面的内容也是有效的。

代码语言:javascript
复制
$statement->bindColumn(1, $name);

5 PDO 条件

在最后一节中,我们将讨论与使用PDO时的一些技巧。

5.1、IN 子句:

在prepare 语句中构建IN子句是一项有趣的任务。看看下面的代码,想象一下这就是我们需要构建的:

代码语言:javascript
复制
$users = ['Andy', 'Tom'];
$statement = $dbh->prepare('SELECT * FROM customers where name IN :name');
$statement->execute($user);

乍一看,这似乎是合法的。仔细看看。它不起作用,因为prepare语句只接受标量类型(例如string、int等)。

最终的任务是构建一个包含相同问号的,以逗号分隔的字符串(?)来绑定数组变量。这就是我们如何构建一个合法的子句串。

代码语言:javascript
复制
$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子句时,我们可能会这样做:

代码语言:javascript
复制
$name = 'Andy';
$statement = $dbh->prepare('SELECT count(*) FROM customers where name LIKE %:name%');
$statement->bindValue(':name', $name);

然而,这在PDO中是行不通的。我们需要将通配符移到变量本身:

代码语言:javascript
复制
$name = '%Andy%';
$statement = $dbh->prepare('SELECT count(*) FROM customers where name LIKE :name');
$statement->bindValue(':name', $name);
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Lemon黄 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档