PHP应用可以在很多种数据库中持久保存信息,例如 MYSQL、 Postgresql、 Sqlite?和 Oracle。这些数据库都提供了用于在PHP和数据库之间通信的扩展。例如, MYSQL使用
的是mysq1i扩展,这个扩展向PHP语言添加了很多mysq1i*()函数: Sqlitet使用的是 Sqlite3扩展,这个扩展向PHP语言添加了 Solite3、 Sqlite3stmt和 Solite3 Result:类。
如果在项目中使用多种数据库,需要安装并学习多种PHP数据库护展和接口。这增加了 认知和技术负担。
正是基于这个原因,PHP原生提供了PDO扩展。PDo( PHP Data Objects的简称,意思是 PHP数据对象”)是一系列PHP类,抽象了不同数据库的具体实现,只通过一个用户 界面就能与多种不同的SQL数据库通信。不管使用哪种数据库系统,使用一个接口就能 编写和执行数据库查询。
警告:虽然PDO扩展为不同数据库提供了統一接口,但是我们仍然必须自己编写SQL语句。这是 PDO的劣势所在。各种数据库都会提供专属的特性,而这些特性通常需要独特的SQL句 法。我建议使用PDO时编写符合ANSI1SO标准的SQL语句,这样如果更换数据库系统 SQL语句不会失效。如果确实必须使用专属的数据库特性,记住,更换数据库系统时要更 新SQL语句。
首先,我们要选择最适合应用的数据库系统;然后,安装数据库,创建模式,还可以加 载初始的数据集;最后,在PHP中实例化PD类。PD实例的作用是把PHP和数据库连接 起来
PD类的构造方法有一个字符串参数,用于指定DSN( Data Source Namel的简称,意思 是“数据源名称”),提供数据库连接的详细信息。DSN的开头是数据库驱动器的名称 (例如mysq1或 sqlite),后面跟着:符号,然后是剩下的内容。不同数据库使用的DSN 连接字符串有所不同,不过一般都包含以下信息:
主机名或IP地址。端口号数据库名。字符集注意:各种数据库使用的DNS格式参见httpphp.met/manual/pdo,drivers.php
PD0类构造方法的第二个和第三个参数分别是数据库的用户名和密码。如果数据库需要 认证,要提供这两个参数
示例5-18使用PDO连接到了一个名为acme的 MYSQL数据库。这个数据库的P地址是 127.0.0.1,监听的是 MYSQL使用的标准端口3306。这个数据库的用户名是josh,密码 是 sekrit。连接使用的字符集是utf8。
示例5-18:PDO类的构造方法
<?php try { $pdo = new PDO( 'mysql:host=127.0.0.1;dbname=books;port=3306;charset=utf8', 'USERNAME', 'PASSWORD' ); } catch (PDOException $e) { // Database connection failed echo "Database connection failed"; exit; }PD0类构造方法的第一个参数是DSN。这个DSN以mysq1:开头,因此PDO会使用PD0 展中的 MYSQL驱动器连接 MYSQL数据库。在:符号之后,我们指定了几个使用分号分开 的键值对,设置host、 dbname、port和 charset
建议:如果连接数据库失败,PDO构造方法会抛出PD0 Exception异常。创建PDO连接时要预期会 出现这种异常,并捕获这种异常。
保证数据库凭据的安全
示例5-18只是用于演示,这么做并不安全。绝对不能在PHP文件中硬编码数据库凭据, 尤其不能在可公开访问的PHP文件中这么做。如果由于缺陷或服务器配置出错,让HTTP
客户端看到了原始的PHP代码,那么数据库凭据就暴露了,所有人都能看到。我们应 把数据库凭据保存在一个位于文档根目录之外的配置文件中,然后在需要使用凭据的 PHP文件中导入。
建议:凭据也不能纳入版本控制。我们要使用, signore文件保护凭据。否则 现在代码仓库中,别人就能看到。如果使用的是公开仓库,后果更严死,密凭据会公开出
在下面这个例子中,我们在 settings. php文件中保存数据库连接凭据。这个文件保存在项 目的根目录中,但在文档根目录之外。 index.pp文件保存在文档根目录中,通过Web服 务器可以公开访问。 index. php文件使用了保存在 settings. php文件中的凭据。
[项目根目录 settings. php public_html/ <— 文档根目录 index. php
settings. php文件的内容如下:
<?php $settings = [ 'host' => '127.0.0.1', 'port' => '3306', 'name' => 'books', 'username' => 'root', 'password' => 'root', 'charset' => 'utf8' ];示例5-19是 index.php文件的内容。这个文件导入了 settings.php文件,然后建立了一个 PDO数据库连接。
示例5-19:在PDO构造方法中使用外部设置
<?php require 'settings.php'; try { $pdo = new PDO( sprintf( 'mysql:host=%s;dbname=%s;port=%s;charset=%s', $settings['host'], $settings['name'], $settings['port'], $settings['charset'] ), $settings['username'], $settings['password'] ); } catch (PDOException $e) { // Database connection failed echo "Database connection failed"; exit; }这样做更安全。如果idex,php文件泄露了,数据库凭据仍然安全。
现在我们建立了到一个数据库的PDO连接,通过这个连接可以使用SQL语句从数据库中 读取数据,或者把数据写入数据库。不过这还不算完事。开发PHP应用时,我经常需要 使用从当前HTTP请求中获取的动态信息定制SQL语句。例如,使用/user?email=johna example.com这个URL显示具体用户账号的资料信息。这个URL使用的SQL语句可能是:
SELECT id FROM users WHERE email -"johneexample com";初级PHP开发者可能会像下面这样构建这个SQL语句:
$sql= sprintf( SELECT id FROM users WHERE email =%s", filter_input(INPUT GET, "email) );这么做不好,因为SQL语句使用了HTP请求査询字符串中的原始输入数据。这么做等于 是为黑客打开了大门,让他们对你的PHP应用做坏事。你听说过 Bobby Tablesf的事没 (hrp:/ liked. com327)?在SQL语句中使用用户的输入时,一定要过滤。我们很幸运 PDO扩展通过预处理语句和参数绑定把过滤输人这项操作变得特别简单。
预处理语句是 Pdostatement实例。不过,我很少直接实例化 Pdostatement:类,而是通过 PDO实例的 prepare()方法获取预处理语句对象。这个方法的第一个参数是一个SQL语 句字符串,返回值是一个 Pdostatement实例
<?php $sql = 'SELECT id FROM users WHERE email = :email'; $statement = $pdo->prepare($sql);注意,在这个SQL语句中,: email是具名占位符,可以安全地绑定任何值。在示例520 中,我在sstatement’实例上调用midvalue()方法,把HTTP请求查询字符串的值定 到:emai1占位符上。
示例5-20:在预处理语向上绑定电子邮件地址
$sql = 'SELECT id FROM users WHERE email = :email'; $email = filter_input(INPUT_GET, 'email'); $statement = $pdo->prepare($sql); $statement->bindValue(':email', $email);预处理语句会自动过滤emai1的值,防止数据库受到SQL注入攻击。一个SQL语句字得 串中可以有多个具名占位符,然后在预处理语句上调用 bindvalue()方法绑定各个占位 符的值。
在示例5-20中,具名占位符:emai1的值是字符串。如果修改SQL语句,想使用数值ID 査找用户该怎么办呢?此时,我们必须向预处理语句的 bindvalue()方法传入第三个参 数,指定占位符要绑定的数据是什么类型。如果不传入第三个参数,顶处理语句假定要 绑定的数据是字符串 。
示例5-21对示例5-20做了修改,现在我们不使用电子邮件地址查找用户,而是使用数值 ID。数值D从HTTP査询字符串中名为id的参数中获取
例5-21:在预处理语向上绑定ID
<?php $sql = 'SELECT email FROM users WHERE id = :id'; $userId = filter_input(INPUT_GET, 'id'); $statement = $pdo->prepare($sql); $statement->bindValue(':id', $userId, PDO::PARAM_INT);statement= spdo->prepare($sql) ssql =" SELECT email FROM users WHERE id =: id i Suserid filter_input(INPUT GET, 'id)
sstatement->bindvalue(: id, Userid, PDO: PARAM_INT)
在这个示例中, midvalue()方法的第三个参数是PDO: PARAM INT常量,告诉PDO要绑 定的数据是整数。指定数据类型的PDO常量还有:
PDO::PARAM_BOOL PDO::PARAM_NULL PDO:: PARAM_INT PDO:: :PARAM_STR (默认值)注意:全部PDO常量参见hrtp: l/php net/manual/pdo constants php
现在有了预处理语句,可以在数据库中执行SQL查询了。调用顶处理语句的 execute() 方法后会使用绑定的所有数据执行SQL语句。如果执行的是 INSERT、 UPDATE或 DELET 「E 语句,调用 execute()方法后工作就结束了。如果执行的是5 ELECTI语句,我们可能期 望数据库能返回匹配的记录。我们可以调用预处理语句的 fetch()、 fetchAll() fetchColumn()和 fetchObject()方法,获取査询结果:
PDOStatement实例的 fetch()方法用于获取结果集合中的下一行。我会使用这个方法选 代大型结果集合,如果可用内存放不下整个结果集合,特别适合使用这个方法。
示例5-22:把预处理语向获取的结果当成关联数组处理
<?php //构建并执行SQL査询 $sql = 'SELECT id FROM users WHERE email = :email'; $email = filter_input(INPUT_GET, 'email'); $statement = $pdo->prepare($sql); $statement->bindValue(':email', $email, PDO::PARM_INT); $statement->execute(); // 迭代结果 while (($result = $statement->fetch(PDO::FETCH_ASSOC)) !== false) { echo $result['email']; }在这个示例中,在预处理语句实例上调用 fetch()方法时,我把这个方法的第一个参数 设为PDo:: FETCH AS SOC常量。这个参数决定 fetch()和 fetch11()方法如何返回查询结 果。可以使用的常量如下:
PDO::FETCH_ASSOC 让 fetch()和 fetchAll()方法返回一个关联数组。数组的键是数据库的列名。
PDO::FETCH_NUM 让 fetch()和 fetchAll()方法返回一个键为数字的数组。数组的键是数据库列在查询中的索引。
PDO::FETCH_BOTH 让 fetch()和 fetchAll()方法返回一个即有键为列名又有键为数字的数组,等于是 FETCH_ASSOC 和 FETCH_NUM的合并。
PDO::FETCH_OBJ 让 fetch()和 fetchAll()方法返回一个对象,对象的属性是数据库的列名。
注意:获取PDO语句结果的详细说明参见hrp: //php net/manual/pdostatement fetch.php。
如果处理的是小型结果集合,可以使用预处理语句的 fetch1()方法获取所有查询结果 (如示例5-23所示)。除非十分确定可用内存能放得下整个查询结果,否则,我通常不 建议使用这个方法。
示例5-23:让预处理语向获取所有结果,把结果保存到关联数组中
//构建并执行SQL查询 $sql = 'SELECT id, email FROM users WHERE email = :email'; $email = filter_input(INPUT_GET, 'email'); $statement = $pdo->prepare($sql); $statement->bindValue(':email', $email, PDO::PARM_INT); $statement->execute(); // 迭代结果 $results = $statement->fetch(PDO::FETCH_ASSOC); foreach ($results as $result) { echo $result['email']; }如果只关心查询结果中的一列,可以使用预处理语句的 fet column()方法。这个方 法的作用与 fetch()方法类似,返回査询结果中下一行的某一列(如示例5-24所示)・ fetchcolumr()方法只有一个参数,用于指定所需列的索引。
建议:查询结果中列的顺序与SQL查询语句中指定的列顺序一致。
示例5-24:让预处理语向获取一列,且一次获取一行,把结果保存到关联数组中
<?php /构建并执行SQL查询 $sql = 'SELECT id, email FROM users WHERE email = :email'; $email = filter_input(INPUT_GET, 'email'); $statement = $pdo->prepare($sql); $statement->bindValue(':email', $email, PDO::PARM_INT); $statement->execute(); // 迭代结果 while (($email= $statement->fetchColunm(1)) !== false) { echo $email; }在示例5-24中,emai1列出现在SQL査询语句的第二位,因此在查询结果的行中是第二 列、所以我传入 fetchcolumn()方法的参数是数字1(列的索引从零开始)。
我们还可以使用预处理语句的 fetchobject()方法获取査询结果中的行,这个方法把行 当成对象,对象的属性是SQL查询结果中的列(如示例5-25所示)。
示例5-25:把预处理语句获取的行当成对象
//构建并执行SL查询 $sql ="SELECT id, email FROM users HERE email = :email"; $statement = $pdo->prepare($sql); $email = filter_input(INPUT_GET, "email"); $statement->bindValue(":email", $email, PDO::PARAM_INT); $statement->execute(); //选代结果 while (($result = $statement->fetchObject() !== false) { echo $result->name; }PDO扩展还支持事务。事务是指把一系列数据库语句当成单个逻辑单元(具有原子性) 执行。也就是说,事务中的一系列SQL查询要么都成功执行,要么根本不执行。事务的 原子性能保证数据的一致性、安全性和持久性。事务还有个很好的副作用一提升性 能,因为事务其实是把多个査询排成队列,一次全部执行。
注意:不是所有数据库都支持事务。详细信息参见数据库的文档和相应的 PHP PDO駆动器
在PDO扩展中使用事务很容易。构建和执行SQL语句的方式完全和示例5-25展示的 样,不过唯有一处区别:要把想执行的SQL语句放在PDO实例的 begintransaction0)方 法和 commit()方法之间。 begintransaction()方法的作用是让PDO把后续SQL查询语 句排入队列,而不是立即执行这些SQL语句。 commit()方法的作用是执行原子事务队列 中的査询。如果事务中有一个査询失败了,事务中的所有査询都无效。记住,事务中
sQL査询要么都成功执行,要么根本不执行
原子性对数据完整性至关重要。下面举个处理银行账户交易的例子。这段代码可以把钱 存入账户,如果账户中有足够的余额,也可以取钱。示例5-26从一个账户中转50美元到 另一个账户,没有使用数据库事务。
示例5-26: 执行数据库查询时没使用事务
<?php require 'settings.php'; // PDO connection try { $pdo = new PDO( sprintf( 'mysql:host=%s;dbname=%s;port=%s;charset=%s', $settings['host'], $settings['name'], $settings['port'], $settings['charset'] ), $settings['username'], $settings['password'] ); } catch (PDOException $e) { // Database connection failed echo "Database connection failed"; exit; } // Statements $stmtSubtract = $pdo->prepare(' UPDATE accounts SET amount = amount - :amount WHERE name = :name '); $stmtAdd = $pdo->prepare(' UPDATE accounts SET amount = amount + :amount WHERE name = :name '); // Withdraw funds from account 1 $fromAccount = 'Checking'; $withdrawal = 50; $stmtSubtract->bindParam(':name', $fromAccount); $stmtSubtract->bindParam(':amount', $withDrawal, PDO::PARAM_INT); $stmtSubtract->execute(); // Deposit funds into account 2 $toAccount = 'Savings'; $deposit = 50; $stmtAdd->bindParam(':name', $toAccount); $stmtAdd->bindParam(':amount', $deposit, PDO::PARAM_INT); $stmtAdd->execute();这么写看起来可以,其实不然。如果从账户1中取了50美元后,还没把50美元存入账户 2,此时服务器突然停机了怎么办?主机商可能会出现电力故障,受到火灾或水灾等灾 难的侵害。从账户1中取出的这50美元怎么办?这50美元不会存入账户2,而是就这么消 失了。我们可以使用数据库事务保证数据的完整性(如示例5-27所示)
示例5-27:使用事务执行数据库查询
<?php require 'settings.php'; // PDO connection try { $pdo = new PDO( sprintf( 'mysql:host=%s;dbname=%s;port=%s;charset=%s', $settings['host'], $settings['name'], $settings['port'], $settings['charset'] ), $settings['username'], $settings['password'] ); } catch (PDOException $e) { // Database connection failed echo "Database connection failed"; exit; } // Statements $stmtSubtract = $pdo->prepare(' UPDATE accounts SET amount = amount - :amount WHERE name = :name '); $stmtAdd = $pdo->prepare(' UPDATE accounts SET amount = amount + :amount WHERE name = :name '); // Start transaction $pdo->beginTransaction(); // Withdraw funds from account 1 $fromAccount = 'Checking'; $withdrawal = 50; $stmtSubtract->bindParam(':name', $fromAccount); $stmtSubtract->bindParam(':amount', $withDrawal, PDO::PARAM_INT); $stmtSubtract->execute(); // Deposit funds into account 2 $toAccount = 'Savings'; $deposit = 50; $stmtAdd->bindParam(':name', $toAccount); $stmtAdd->bindParam(':amount', $deposit, PDO::PARAM_INT); $stmtAdd->execute(); // Commit transaction $pdo->commit();示例5-27在一个数据库事务中处理取钱和存钱操作,这样能保证两个操作都成功或都 败,从而保持数据的一致性。
来源: Modern PHP 第五章
