PHP 支持多种数据库,如 MySQL、PostgreSQL、SQLite 和 Oracle 等,并且这些数据库都提供了用于 PHP 和相应数据库之间通信的扩展,如 mysqli
、sqlite3
等。这样造成的一个问题是如果项目中使用了多种数据库,需要安装并使用多种 PHP 数据库扩展和接口,增加了学习和维护的成本。为此,从 PHP 5.1 开始引入了一个新的扩展 —— PDO。
PDO 扩展
PDO(PHP Data Objects)是一系列 PHP 类,抽象了不同数据库的具体实现,只通过同一套接口就可以与不同的数据库通信,极大地降低了学习成本,同时提高开发效率。
注:尽管 PDO 为不同数据库提供了统一接口,但是仍然必须自己编写 SQL 语句,这是 PDO 的劣势所在。这样存在的一个隐患是不同的数据库 SQL 语句语法可能略有出入,所以在切换数据系统的时候需要注意这一点,建议尽可能编写符合 ANSI/ISO 标准的 SQL 语句。
数据库连接和DSN
PDO 的构造函数中有一个字符串参数,用于指定 DSN(Data Source Name)来提供数据库连接的详细信息:
<?php
try {
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=test;port=3306;charset=utf8',
'username',
'password'
);
} catch (PDOException $ex) {
echo 'database connection failed';
exit();
}
可见 DSN 主要包含信息包括:数据库主机名/IP地址、端口号、数据库名称、字符集。PDO构造函数的第二个参数是数据库用户名,第三个参数是该用户名对应密码。
Laravel 框架的数据库组件就是基于PDO实现的,底层使用工厂模式为不同的数据库创建相应的 PDO 实例,Laravel 默认使用 MySQL 数据库,对应的 PDO 实例通过 MySqlConnector
类的 connect()
方法创建,并且最终落地到 createPdoConnection
方法实现:

保证数据库凭证的安全
为了保证数据库凭证(用户名/密码)的安全,不能将其硬编码在代码中,尤其是可以公开访问的 PHP 文件。我们应该将其存放在一个位于文档根目录之外的配置文件中,然后在需要使用凭证的地方引入,正如 Laravel 所实现的那样(用户名密码信息位于 .env
,隔离在版本控制之外):
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
然后在 config/database.php
中引入:
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
预处理语句
现在我们建立了一个连接到数据库的 PDO 实例,通过这个实例就可以使用 SQL 语句从数据库读取数据,或者将数据写入数据库。
开发 PHP 应用时,我们经常需要从 HTTP 请求中获取动态值来定制 SQL 语句,如使用 /user?name=nonfu
来显示具体账户的资料信息,对应的 SQL 语句可能是:
SELECT * FROM user WHERE name = "nonfu";
初级 PHP 开发者可能会像这样构建这个 SQL 语句:
$sql = sprintf(
'SELECT * FROM user WHERE name = "%s"',
filter_input(INPUT_GET, 'name')
);
这样做就会有 SQL 注入隐患。所以,在基于用户请求参数构建 SQL 语句时,一定要过滤用户输入参数值。幸运的是,在 PDO 中,我们可以通过预处理语句和参数绑定来实现用户输入过滤,从而避免 SQL 注入。
预处理语句是一个 PDOStatement
实例,我们可以通过 prepare()
方法来返回该实例:
<?php
$sql = 'SELECT * FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
预处理语句会自动过滤 $name 的值,防止数据库遭受 SQL 注入攻击。
Laravel 框架中相应的底层实现位于 MySqlConnetion
的父类 Connection
的 select
方法中:

查询结果
有了预处理语句之后,就可以在数据库中执行 SQL 查询了,调用预处理语句的 execute()
方法后就会使用绑定的所有数据执行 SQL 语句,如果执行的是 INSERT
、UPDATE
或 DELETE
语句,执行完 execute()
方法工作结束了,如果执行的是 SELECT
语句,我们还期望数据库能返回匹配的结果。我们可以使用以下方法获取查询结果:
fetch()
fetchAll()
fetchColumn()
fetchObject()
fetch()
方法用于获取结果集的下一行,我们可以使用这个方法迭代大型结果集:
$sql = 'SELECT * FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();
while (($result = $statement->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
echo $result['name'];
}
我们在调用 fetch()
方法时,传入了 PDO::FETCH_ASSOC
参数,该参数决定如何返回查询结果,该参数支持以下常量:
PDO::FETCH_ASSOC:返回关联数组,数组的键是数据表的列名
PDO::FETCH_NUM:返回键为数字的数组
PDO::FETCH_BOTH:顾名思义,返回一个既有键为列名又有键为数字的数组
PDO::FETCH_OBJ:返回一个对象,对象的属性是数据表的列名
如果处理的小型结果集合,可以使用 fetchAll()
方法获取所有查询结果,Laravel 框架底层的 select()
方法中就使用了该方法来获取返回结果集,所以在获取大量结果时不能使用该方法:
$sql = 'SELECT * FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();
$results = $statement->fetchAll(PDO::FETCH_ASSOC);
if ($results) {
foreach ($results as $result) {
echo $result['name'];
}
}
如果只关心查询结果中的一列,可以使用 fetchColumn()
方法:
$sql = 'SELECT id, name FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();
while (($name = $statement->fetchColumn(1)) !== FALSE) {
echo $name;
}
我们还可以使用 fetchObject()
方法获取查询结果中的行,这个方法把行当做对象:
$sql = 'SELECT id, name FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();
while (($result = $statement->fetchObject()) !== FALSE) {
echo $result->name;
}
事务
事务是把一系列数据库操作当作一个逻辑单元执行,也就是说,事务中的一系列 SQL 语句要么都执行成功,要么都失败,事务的原子性能保证数据的一致性、安全性和持久性。
PDO 扩展中使用事务很容易,只需把想要执行的 SQL 语句放在 PDO 实例的 beginTransaction()
方法和 commit()
方法之间即可。
在 Laravel 框架中对事务操作进行了封装,我们可以这样调用:
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
}, 5);
相应的底层位于 ManagesTransactions
trait 中:

还可以这样调用:
DB::beginTransaction();
try {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
DB::commit();
} catch (PDOException $ex) {
var_dump($ex);
DB::rollback();
}
相应的底层实现同样位于 ManagesTransactions
,可自行查看。
0 条回复