具体参考《PHP核心技术与最佳实践》的5.1章 《什么是PDO》
1. PDO的介绍
PHP针对每种数据库都有一个独立的模块、一组独立的函数。这样的结构和设计让PHP兼容多种数据库变得困难。一旦要将一个应用移到另外一种数据库环境中,或者是需要添加新的数据库支持,就不得不重新编写和数据库相关的操作。通常编写多个类,用适配器模式来实现。在这个历史背景下PDO出现了。PDO(PHP Data Objects)提供了一个通用接口访问多种数据库,即抽象的数据模型支持连接多种数据库。有了PDO使代码变得更简洁、更安全。
在PHP中,连接MySQL数据库的通常有3种方式:
- MySQL系列函数:最常用,是过程式风格的一组应用(不建议,在PHP7.0已废除)
- MySQLi系列函数:是MySQL函数的增强改进版,提供了过程化和面向对象两种风格的API,增加了预编译和参数绑定等新的特性
- PDO:从语法上讲,PDO更接近MySQLi
具体的可以参考:【连接数据库】PHP7的连接数据库的三种方法【原创】
相比MySQLi,PDO的优势在于支持多种数据库,而MySQLi只能支持MySQL,所以一般更推荐使用PDO来对数据库进行操作。
PDO提供了一个数据访问抽象层,这就意味着不管使用哪种数据库,都可以用同样一组API对数据进行操作,保证了可抽象性和访问接口的一致性。
开启PDO很容易,一般来说安装好PHP默认都会开启PDO,如果没有则去php.ini中找到以下语句,把前面的分号去掉即可
;extension=php_pdo.dll
2. PDO的使用
使用PDO的第一步是配置数据源,之后的用法和MySQL扩展操作数据库的方法没有什么区别了,
PDO的操作主要有PDO::query()、PDO::exec()、PDO::prepare()
- PDO::query():主要是用于有记录结果返回的操作,特别是SELECT操作
- PDO::exec():主要是针对没有结果集合返回的操作,比如INSERT、UPDATE、DELETE等操作,它返回的结果是当前操作影响的列数
- PDO::prepare():主要是预处理操作,需要通过$rs->execute()来执行预处理里面的SQL语句,这个方法可以绑定参数,功能比较强大
以下是PDO的示例:
<?php
/**
* PDO 的使用实例
*/
try {
// 配置PDO的数据源
$dsn = 'mysql: host=localhost; dbname=php_book';
// 构造方法
$db = new PDO($dsn, 'root', '123456');
// 设置异常可捕获
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("SET NAMES 'UTF8'");
// 插入到日志中
$sql = "INSERT INTO users(name, email, password, created_at) values ('pdo_test', '8888@qq.com', 'bbb', now())";
$db->exec($sql);
// 使用预处理语句
$insert = $db->prepare("INSERT INTO users(name, email, password, created_at) values (?, ?, ?, now())");
$insert->execute(array('pdo_test1', '8448657@qq.com', 'aaa'));
// 异常
$insert->execute(array('pdo_test2', '8448657@qq.com', 'aaa', 9, 10));
$sql = "select name, email, password, created_at from users";
$query = $db->prepare($sql);
$query->execute();
var_dump($query->fetchAll(PDO::FETCH_ASSOC));
} catch (PDOException $e) {
echo $e->getMessage();
}
注意:使用PDO从MySQL数据库查询出来的数据都是string类型的,在某些特殊应用下,可能需要转换格式
3. PDO的参数绑定和预编译
PDO最大的特点就是引入参数绑定和预编译。
下面是从数据库中查询某条记录:
<?php
$pdo = new PDO('mysql: host=localhost; dbname=php_book');
$pdo->query("SELECT name FROM users WHERE id = " . $_GET['id']);
这是一段糟糕的代码。插入一个原始的请求参数到 SQL 请求中。这将让被黑客轻松地利用[SQL 注入]方式进行攻击。想一下如果黑客将一个构造的 id 参数通过像 http://domain.com/?id=1%3BDEL… 这样的 URL 传入。这将会使 $_GET[‘id’] 变量的值被设为 1;DELETE FROM users 然后被执行从而删除所有的 user 记录!因此,你应该使用 PDO 限制参数来过滤 ID 输入。
上面的代码可优化为:
<?php
$pdo = new PDO('mysql: host=localhost;dbname=php_book');
$stmt = $pdo->prepare('SELECT name FROM users WHERE id = :id');
$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
在MySQL应用中,为了防止注入攻击,通常在PHP中使用intval、addslashes等函数对传入的参数进行转义,转变为SQL中合法的参数类型,这种方法较复杂,而使用PDO的bindParam方法会变得很快捷,只需要在函数中指定第三个参数,即可对传入的参数进行转换,转换为需要的类型拼接到原生的SQL语句中
比如:
<?php
/**
* PDO 的最大特点是引入参数绑定和预编译
* 参数绑定:通过绑定变量来执行准备好的语句
* 两种绑定参数的方式
*/
$calories = 150;
$colour = 'red';
// 配置PDO的数据源
$dsn = 'mysql: host=localhost; dbname=php_book';
// 构造方法
$db = new PDO($dsn, 'root', '123456');
// 执行预处理语句(第一种绑定变量的方式)
$sth = $db->prepare('SELECT name, colour, calories FROM fruit WHERE calories > :calories AND colour = :colour');
// 绑定变量,将变量转化为int类型
$sth->bindParam(':calories', $calories, PDO::PARAM_INT);
// 绑定变量,将变量转化为string类型
$sth->bindParam(':colour', $colour, PDO::PARAM_STR, 12);
// 执行
$sth->execute();
var_dump($sth->fetchAll(PDO::FETCH_ASSOC));
// 执行预处理语句(第二种绑定变量的方式)
$sth = $db->prepare('SELECT name, colour, calories FROM fruit WHERE calories > ? AND colour = ?');
// 绑定变量,将变量转化为int类型
$sth->bindParam(1, $calories, PDO::PARAM_INT);
// 绑定变量,将变量转化为string类型
$sth->bindParam(2, $colour, PDO::PARAM_STR, 12);
// 执行
$sth->execute();
var_dump($sth->fetchAll(PDO::FETCH_ASSOC));
预编译负责两件事,转义和软解析提速。程序要支持预编译,除了要数据库支持外,还需要驱动支持(PDO和MySQLi均支持)
4. PDO事务处理
一个事务中所有的工作在提交时,即使是分阶段执行,也要保证安全的应用于数据库,不被其他的连接干扰,事务工作可以在请求发生错误时自动取消。
事务的主要特性:原子性、一致性、独立性、持久性(Atomicity,Consistency,Isolation,Durability,ACID)。典型运用就是通过把批量的改变保存,然后立即执行,这样就能提高效率,一旦事务不成功,将会回滚到初始状态,保证数据的一致性。
SQL通常工作在自动提交模式下,这意味着执行的每个查询都有自己隐含的事务处理,无论是数据库支持事务还是因数据库不支持而不存在事务,DML语句执行的结果都将立即生效而不可更改。比如在MySQL中执行一条update语句,其功能将会立即生效并且是永久性不可更改性的。而在Oracle数据库中,默认是事务模式,要delete一条数据,数据并不会被永久性删除,只有执行了commit命令后才会生效。
PDO中使用beginTransaction()方法来创建事务。在一个事务中,使用commit()或者是rollback()方法来结束事务,具体应用哪种方法这取决于事务中代码运行是否成功。脚本结束或者一个连接要关闭时,如果还有一个未处理完的事务,PDO自动将其回滚。这对于脚本意外终止情况来说是一个安全方案,如果没有明确提交事务,它将假设发生一些错误,为数据的安全执行回滚。
自动回滚仅发生于通过beginTransaction()建立的事务。如果用手动方式执行一个开始事务的查询,PDO无法知道他的情况故无法回滚。
代码如下:
<?php
try {
$conn = new PDO('mysql: host=localhost; dbname=php_book', 'root', '123456');
// 开启事务
$conn->beginTransaction();
for($i = 0; $i < 1000000; $i++) {
$conn->exec("insert into `users` values(null, 'username')");
}
// 提交事务
$conn->commit();
} catch(PDOException $ex) {
// 执行回滚
$conn->rollBack();
}
注意:因为使用了事务,要么成功要么失败,如果发现第一条执行了,但是第二条没有执行或者是失败了,则应该检查一下表类型是否为MyISAM,MyISAM引擎是不支持事务的,需要改用InnoDB或者其他的支持事务的引擎。