数据库
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 第五章