/ php

ModernPHP PDO

简介

PDO(PHP Data Objects,PHP数据对象)是一系列的PHP类,抽象了不同数据库的具体实现,提供了统一的接口来操作不同的数据库。

数据库连接

PDO类的构造方法的第一个参数为字符串,指定了DSN(Data Source Name,数据源名称),提供数据连接的详细信息。DSN的开头是数据库驱动器的名称(例如mysql或sqlite),然后接一个":"号,后面是其他连接信息(主机名、端口号,数据库名和字符集等),更多内容可以查看pdo.drivers。PDO类构造方法的第二个参数和第三个参数分别是数据库的用户名和密码。举一个使用的列子:

<?php
try {
    $pdo = new PDO(
			'mysql:host=127.0.0.1;dbname=books;port=3306;charset=utf8',
			USERNAME,
			PASSWORD
		);
} catch (PDOException $e) {
    echo "Database connection failed";	
    exit;
}

当然,我们不建议使用硬编码的方式写入数据库的连接信息,这样一旦数据库连接出错,很可能就暴露给用户你的PHP代码。所以我们更建议将数据库连接信息写入一个配置文件,然后在主程序中引入配置文件。下面给出一个配置文件的样例:

<?php
$setting = [
	'host'=> '127.0.0.1',
	'port'=> 3306,
	'dbname'=> 'users',
	'username'=> 'USERNAME',
	'password'=> 'PASSWORD',
	'charset'=> 'utf8'
];

预处理语句

在SQL语句中使用用户输入的数据一定要过滤,PDO通过预处理语句和参数绑定将过滤输入这项操作变得简单很多。

预处理语句是PDOStatement的实例。一般可以使用PDO实例的prepare()方法获取预处理语句对象。

<?php 
$sql = 'SELECT * FROM users WHERE email=:email';
$statement = $pdo->prepare($sql);

$email = filter_input(INPUT_GET, 'email');
$statement->bindParam(':email', $email);

在上面的SQL语句中,:email是具名占位符,可以在$statement实例中通过bindValue()方法绑定为我们需要的值。预处理语句可以自动过滤$email的值,防止SQL注入的发生。在一个SQL语句中可以存在多个多个具名占位符,bindParam()方法提供了第三个参数,指定需要绑定值的数据类型(例如PDO::PARAM_INT更多常量),默认的数据类型是字符串。

查询结果

有了预处理语句之后,就可以在数据库中执行SQL查询操作了。调用预处理对象的execute()方法后会使用绑定的所有数据执行SQL语句。如果执行的操作是UPDATE,INSERT或DELETE,execute()方法后工作就结束了(当然还可以通过该函数返回TRUE或FALSE判断操作成功与否)。但是如果是SELECT操作,我们还要去获取返回的数据,这时可以通过预处理对象的fetch()fetchAll()fetchColumn()fetchObject()方法获取查询结果。

fetch()用于获取结果数据集的一行,这个方法适用于迭代大型的数据集。fetchAll()可以获取整个数据集,fetch()fetchAll()的第一个参数为PDO类的返回方式常量,决定了如何返回查询结果,常用的常量有:

  • PDO::FETCH_ASSOC:返回一个以数据库列名为键的关联数组。
  • PDO::FETCH_NUM:返回一个以数字为键的关联数组。
  • PDO::FETCH_OBJ:返回一个属性名为数据库列名的对象。
  • PDO::FETCH_BOTH:返回一个键既包含数据库列名也包含数组的关联数组。

举一个使用的例子:

<?php
$sql = 'SELECT id, name FROM users WHERE email=:email';
$statement = $pdo->prepare($sql);

$email = filter_input(INPUT_GET, 'email');
$statement->bindParam(':email', $email);
$statement->execute();

while (($result = $statement->fetch(PDO::FETCH_ASSOC)) != false) {
	echo $result['name'];
}

事务

事务是指把一系列数据库语句当成单个逻辑单元来处理。也就是说,事务中的一系列sql语句要么都执行成功,要么根本不执行,也就是说事务具有原子性。事务的原子性也保证了数据的一致性,安全性和持久性。事务还能提升性能,因为它实际上是把多个查询排成队列,一次全部执行。

PDO支持事务,而且使用方便,你只要把构建和执行sql的操作放在PDO实例的beginTransaction()commit()中。beginTransaction()方法的作用是把后续生成的sql语句排入队列,commit()方法执行原子事务队列中的sql语句。

下面演示一个未使用事务的例子,其操作是从A账户的钱转入B账户:

<?php
require '../setting.php';

try {
	$pdo = new PDO(
		sprintf(
				'mysql:host=%s;dbname=%s;port=%s;charset=%s',
				$setting['host'],
				$setting['dbname'],
				$setting['port'],
				$setting['charset']
			),
			$setting['username'],
			$setting['password']
		);
} catch (PDOException $e) {
	echo "Database connection failed";
	exit;
}

$stmtSub = $pdo->prepare('
	UPDATE accounts 
	SET amount = amount - :amount
	WHERE name = :name
');
$stmtAdd = $pdo->prepare('
	UPDATE accounts 
	SET amount = amount + :amount
	WHERE name = :name
');

$fromAccount = 'A';
$toAccount = 'B';
$money = 50;

// 从A账户取钱
$stmtSub->bindParam(':amount', $money);
$stmtSub->bindParam(':name', $fromAccount);
$stmtSub->execute();

// 钱存入B账户
$stmtAdd->bindParam(':amount', $money);
$stmtAdd->bindParam(':name', $toAccount);
$stmtAdd->execute();

这里有一个问题是,当从A账户取钱的操作成功后,发生了一些不可抗拒的错误,导致了后面的把钱加到B账户的操作没有完成,那么这50元就凭空消失了,这显然是不合理的。使用事务我们可以解决这样的问题。下面是使用事务的方式:

<?php
// 开始事务
$pdo->beginTransaction();

$fromAccount = 'jack';
$toAccount = 'rose';
$money = 50;

// 从A账户取钱
$stmtSub->bindParam(':amount', $money);
$stmtSub->bindParam(':name', $fromAccount);
$stmtSub->execute();

// 钱存入B账户
$stmtAdd->bindParam(':amount', $money);
$stmtAdd->bindParam(':name', $toAccount);
$stmtAdd->execute();

// 提交事务
$pdo->commit();

ModernPHP 系列全集:传送门