Modern PHP 数据库-PDO扩展

数据库

PHP应用可以在很多种数据库中持久保存信息,例如 MYSQL、 Postgresql、 Sqlite?和
Oracle。这些数据库都提供了用于在PHP和数据库之间通信的扩展。例如, MYSQL使用

的是mysq1i扩展,这个扩展向PHP语言添加了很多mysq1i*()函数: Sqlitet使用的是
Sqlite3扩展,这个扩展向PHP语言添加了 Solite3、 Sqlite3stmt和 Solite3 Result:类。

如果在项目中使用多种数据库,需要安装并学习多种PHP数据库护展和接口。这增加了
认知和技术负担。

PDO扩展

正是基于这个原因,PHP原生提供了PDO扩展。PDo( PHP Data Objects的简称,意思是
PHP数据对象”)是一系列PHP类,抽象了不同数据库的具体实现,只通过一个用户
界面就能与多种不同的SQL数据库通信。不管使用哪种数据库系统,使用一个接口就能
编写和执行数据库查询。

警告:虽然PDO扩展为不同数据库提供了統一接口,但是我们仍然必须自己编写SQL语句。这是
PDO的劣势所在。各种数据库都会提供专属的特性,而这些特性通常需要独特的SQL句
法。我建议使用PDO时编写符合ANSI1SO标准的SQL语句,这样如果更换数据库系统
SQL语句不会失效。如果确实必须使用专属的数据库特性,记住,更换数据库系统时要更
新SQL语句。

数据库连接和DSN

首先,我们要选择最适合应用的数据库系统;然后,安装数据库,创建模式,还可以加
载初始的数据集;最后,在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 第五章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值