PDO数据抽象简介及MySQL,PostgreSQL,ODBC,Oracle应用

PDO(PHP Data Objects Layer)是PHP 5新出来的东西,在PHP 6都要出来的时候,
PHP 6将默认使用PDO来处理数据库,将把所有的数据库扩展移到了PECL,
那么默认就是没有了我们喜爱的php_mysql.dll之类的了,那怎么办捏,我们只有与时俱进了,
提供一个公共的数据库系统接口,它使用C语言做底层开发,运行速度比较快。

PDO以PHP 5.1为基础进行设计,设计沿承PHP的特点,
以简洁易用为准,从严格意义上讲,
PDO应该归为PHP 5的SPL库之一,
而不应该归于数据抽象层,
因为其本身和MySQL和MySQLi扩展库的功能类似。

PHP 5.1 发布时附带一个全新的数据库连接层PDO(PHP Data Objects)。
它与ADODB和Pear DB等数据库抽象层不同,
它提供的是如何存取数据库和处理查询结果,
效率也更高,还可以通过预处理语句来防止sql注入。

目前支持的数据库:
? DBLIB: FreeTDS / Microsoft SQL Server / Sybase
? Firebird (http://firebird.sourceforge.net/): Firebird/Interbase 6
? MYSQL (http://www.mysql.com/): MySQL 3.x/4.x
? OCI (http://www.oracle.com): oracle Call Interface
? ODBC: ODBC v3 (IBM DB2 and unixODBC)
? PGSQL (http://www.postgresql.org/): PostgreSQL
? SQLITE (http://www.postgresql.org/): SQLite 3 and SQLite 2

PDO的目标

提供一种轻型、清晰、方便的 API
统一各种不同 RDBMS 库的共有特性,但不排除更高级的特性。
通过 PHP 脚本提供可选的较大程度的抽象/兼容性。

PDO的特点:

性能。PDO 从一开始就吸取了现有数据库扩展成功和失败的经验教训。因为 PDO 的代码是全新的,
所以我们有机会重新开始设计性能,以利用 PHP 5 的最新特性。
能力。PDO 旨在将常见的数据库功能作为基础提供,同时提供对于 RDBMS 独特功能的方便访问。
简单。PDO 旨在使您能够轻松使用数据库。API 不会强行介入您的代码,同时会清楚地表明每个函数调用的过程。
运行时可扩展。PDO 扩展是模块化的,使您能够在运行时为您的数据库后端加载驱动程序,
而不必重新编译或重新安装整个 PHP 程序。例如,PDO_OCI 扩展会替代 PDO 扩展实现 oracle 数据库 API。还有一些用于
MySQL、PostgreSQL、ODBC 和 Firebird 的驱动程序,更多的驱动程序尚在开发。

2  PDO的安装

PDO本身结果是模块化的,它被分成一个公共核心,以及一个或多个驱动程序扩展,
公共核心提供了在脚本(PDO本身)中使用的API,驱动程序扩展则为
PDO和本地RDBMS客户机API库架起一座桥梁,用来访问指定的数据库系统。
比如,IBM DB2用户会希望使用PDO_ODBC驱动程序
,Oracle用户会用Oci8_PDO接口,MySQL用户则会用pdo_mysql驱动程序。
PDD的核心在PHP 5.2下默认为开启状态,驱动程序除pdo_sqlite之外,都需要手工打开。

下面是在FreeBSD环境下使用Ports安装PDO核心驱动程序的步骤:
cd /ports/database/pecl-PDO
make install
安装后,它会自动修改php.ini配置文件,如果没有该项则自行加入:
extension=pdo.so
安装PDO MySQL驱动程序:
cd /ports/database/pecl-PDO_MYSQL/
make install
修改php.ini文件,在刚才的项后加入该段:
extension=pdo_mysql.so
使用apachectl –k restart命令重新启动Apache后,即可完成PDO的安装了。

在Win32环境中,由于PHP 5.1版本以上的压缩包里已经自带PDO扩展库文件,
因此只要在php.ini文件中打开该扩展即可,不需要再安装。
php5.0.x则要到pecl.php.net下载,放到你的扩展库,就是PHP所在的文件夹的ext文件夹下;
手册上说5.0之前的版本不能运行PDO扩展。

配置:
修改你的php.ini配置文件,使它支持pdo.(php.ini这个东西没有弄懂的话,先弄清楚,要修改调用你的phpinfo()函数所显示的那个php.ini)

extension=php_pdo.dll前面的分号去掉,分毫是php配置文件注释符号,这个扩展是必须的。
往下还有
;extension=php_pdo.dll
;extension=php_pdo_firebird.dll
;extension=php_pdo_informix.dll
;extension=php_pdo_mssql.dll
;extension=php_pdo_mysql.dll
;extension=php_pdo_oci.dll
;extension=php_pdo_oci8.dll
;extension=php_pdo_odbc.dll
;extension=php_pdo_pgsql.dll
;extension=php_pdo_sqlite.dll
各各扩展所对应的数据库是:

Driver name Supported databases
PDO_DBLIB FreeTDS / Microsoft SQL Server / Sybase
PDO_FIREBIRD Firebird/Interbase 6
PDO_INFORMIX IBM Informix Dynamic Server
PDO_MYSQL MySQL 3.x/4.x
PDO_OCI oracle Call Interface
PDO_ODBC ODBC v3 (IBM DB2, unixODBC and win32 ODBC)
PDO_PGSQL PostgreSQL
PDO_SQLITE SQLite 3 and SQLite 2

你要使用哪种数据库,只要把相应的扩展前的注释符号”;”去掉就可以了。

3  PDO连接数据库
其实,PDO与其他数据库接口和数据库抽象层使用区别不大,首先创建一个连接句柄:
PDO提供了统一的接口:PDO对象。
$db=new PDO(
“driver_name:dbname=db_name;host=hostname/IP;[charset=char_type]”,   //(1)连接字符串
“db_username”,  //(2)db用户名
“db_password”   //(3)db密码
);
说明,PDO有三个参数
(1)连接字符串:
driver_name是使用的PDO驱动,可以为:mysql, mssql, sybase, dblib, firebird, oci, odbc, pgsql, sqlite, sqlite2;
db_name是数据库名称;
hostname/IP是指要连接到哪里,如果是本地则为localhost。
[charset=char_type]是可选的,用来设置字符类型。
(2)db用户名
(3)db密码
example:
<?php
// 连接MySQL数据库的账号
$login  = “root”;
$passwd = “root”;
$db = new PDO(‘mysql:host=localhost;dbname=test’,$login, $passwd);
//如果连接失败,则抛出异常
try {
foreach($db->query(‘select * from test’) as $row){ //查询数据库
print_r($row);
}
$db=null;//关闭数据库连接
} catch (PDOException $e) {
echo $e->getMessage();
}
?>
连接之前,应先确认已经加载了PDO模块。如果试图处理一个无效的连接字符串:
$db = new PDO(
“this_is_not_a_pdo_module:dbname=pdo;host=localhost”,
“foo”,
“bar”
);
echo “Successfully created a PDO object”;
?>
PHP将会返回以下错误:
Fatal error: Uncaught exception ‘PDOException’ with message ‘could not find driver’
所以,我们可以用一种优雅的方式来处理,即抛出PDO异常来处理错误(但并不是所有情况都是)。
try
{
$db = new PDO(
“this_is_not_a_pdo_modul:dbname=pdo;host=localhost”,
“postgres8”,
“postgres8”
);
}
catch( PDOException $e )
{
die( $e->getMessage() );
}
echo “Successfully created a PDO object”;
?>
我们会得到
could not find driver

SQLSTATE[HY000] [7] FATAL: database “pdo2” does not exist
如果数据库不存在(不同的错误会返回不同的提示信息)。

(1)使用持久连接pconnect
持久连接的好处是能够避免在每个页面命中时都打开和关闭数据库服务器连接,速度更快,
如Oracle数据库的一个进程创建了两个连接,
PHP则会把原有连接与新的连接合并共享为一个连接。pdo_connect.php脚本如下:
<?php
//连接MySQL数据库的账号
$login  = “root”;
$passwd = “root”;
$opt = array(PDO::ATTR_PERSISTENT => TRUE);
try {
$db = new PDO(‘mysql:host=localhost;dbname=test,$login,$passwd,$opt);
} catch (PDOException $e) {
echo $e->getMessage();
}
?>
(2)使用DSN- ODBC方式连接数据库
一个DSN(数据源名称)是一个标识符,定义为一个ODBC的数据源驱动。格式为:
Database name数据库名称
Directory目录
Database driver数据库驱动
User ID登录
Password密码
在UNIX系统下,DSN的配置通常存储在ini文件中,使用PDO读取文件配置,代码如下:
<?php
ini_set(“pdo.dsn.dbserver”, “mysql::test”);
try {
$db = new PDO(“dbserver”);
} catch (PDOException $e) {
echo $e->getMessage();
}
?>

4  使用PDO查询
使用PDO进行查询执行,可以使用两种方法。
第一种方法是预处理句柄(Prepared Statements),推荐使用,速度快而且安全。请看下例:
<?php
require_once(‘pdo_connect.php’);
$rs = $db->prepare(“Select * FROM test”);
$rs->execute();
while($row = $rs->fetch()){
print_r($row);
}
?>

Prepared预处理语句的作用是,编译一次,可以多次执行,可以有效防止SQL注入,
在执行单个查询时快于直接使用query()/exec()的方法。
1.绑定参数
使用Prepared预处理语句做Insert操作时的参数需要赋一个名字,以及绑定一个变量。
$stmt = $db->prepare(“Insert INTO users VALUES(:name,:pass,:mail)”);
foreach (array(‘name’,’pass’,’mail’) as $v){
$stmt->bindParam(‘:’.$v,$$v); }
$fp = fopen(“./users.csv”, “r”);
while (list($name,$pass,$mail) = fgetcsv($fp,4096)){
$stmt->execute();
}
}
2.绑定结果列
结果列可以绑定为变量,请看下面例子。
$qry = “Select :type, :data FROM images LIMIT 1”;
$stmt = $db->prepare($qry);
$fp = fopen(tempname(“/tmp”, “LOB”), “w”);
$stmt->bindColumn(‘:type’,$type);
$stmt->bindColumn(‘:type’,$fp, PDO::PARAM_LOB);
$stmt->execute(PDO::FETCH_BOUND);
header(“Content-Type: “.$type);
fflush($fp);
fseek($fp, 0, SEEK_SET);
fpassthru($fp);
fclose($fp);
第二种方法就是直接执行。

直接执行常见于直接查询操作或更新数据库操作,可以使用exec()方法,请看下面的例子。
$db = new PDO(“DSN”);
$db->exec(“Insert INTO foo (id) VALUES(‘bar’)”);
$db->exec(“Update foo SET id=‘bar’”);
该方法返回的是操作影响的行数,若执行错误,则返回False值。
在一些Update的查询执行后,若没有影响到列,则返回0值,我们可以根据它返回的值或布尔值来进行相关处理。例如:
$qry = “Update foo SET id=‘bar’”;
$res = $db->exec($qry) or die(); //错误的返回
if (!$res) //未执行成功
if ($res !== FALSE) // 执行正确并返回
一个完整的例子:
<?php
$dsn = “mysql:host=localhost;dbname=test”;
$db = new PDO($dsn, ‘root’, ”);
//如果为持续性连接,则修改为下面样式
//$db = new PDO($dsn, ‘root’, ”, array(PDO::ATTR_PERSISTENT => true));
$count = $db->exec(“Insert INTO foo SET id = NULL,name = ‘john’,gender=’male’,time=NOW()”);
echo $count;
$db = null;
?>
5  错误与异常处理
设置属性
1) PDO有三种错误处理方式:
? PDO::ERRMODE_SILENT不显示错误信息,只设置错误码
? PDO::ERRMODE_WARNING显示警告错
? PDO::ERRMODE_EXCEPTION抛出异常
可通过以下语句来设置错误处理方式为抛出异常
$db->setAttribute(PDO::ATTR_ERRMODE, );
example:
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
当设置为PDO::ERRMODE_SILENT时可以通过调用errorCode() 或errorInfo()来获得错误信息,当然其他情
况下也可以。
2) 因为不同数据库对返回的字段名称大小写处理不同,所以PDO提供了PDO::ATTR_CASE设置项(包括
PDO::CASE_LOWER,PDO::CASE_NATURAL,PDO::CASE_UPPER),来确定返回的字段名称的大小写。
3) 通过设置PDO::ATTR_ORACLE_NULLS类型(包括PDO::NULL_NATURAL,PDO::NULL_EMPTY_STRING,
PDO::NULL_TO_STRING)来指定数据库返回的NULL值在php中对应的数值。

PDO提供两个方法来取得错误信息:
?  errorCode()——SQL语句错误,如:42000 == 语法错误;
?  errorInfo()——更详细的错误信息。
如下所示为错误信息内容:
array(
[0] => 42000,
[1] => 1064
[2] => Syntax Error
)
1.面向过程的处理
<?php
$db = new PDO(‘mysql:host=localhost;dbname=test’, $user, $pass);
$rs = $db->query(“Select aa,bb,cc FROM foo”);
if ($db->errorCode() != ‘00000’){
print_r($db->errorInfo());
exit;
}
$arr = $rs->fetchAll();
print_r($arr);
$db = null;
?>

PDO和PDOStatement对象有errorCode()和errorInfo()方法,如果没有任何错误,
errorCode()返回的是00000;否则,就会返回一些错误代码。errorInfo()返回的是一个数组,
包括PHP定义的错误代码和MySQL的错误代码及错误信息。数组结构如下:
Array
(
[0] => 42S22
[1] => 1054
[2] => Unknown column ‘aaa’ in ‘field list’

)

每次执行查询以后,errorCode()的结果都是最新的,所以我们可以很容易地自己控制错误信息显示。
2.面向对象的处理
标准的错误句柄,应该是一个面向对象方法来扩展PDO,以允许错误句柄取得系统异常。
请看下面的例子,如果查询出错,将抛出异常。
$db->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION
);
一个完整的例子:
<?php
try {
$db = new PDO(‘mysql:host=localhost;dbname=test’, $user, $pass);
$db = null;
} catch (PDOException $e) {
print “Error: ” . $e->getMessage() . “<br/>”;
die();
}
?>
使用PHP 5的异常处理:这里利用PHP 5面向对象的异常处理特征,如果里面有异常,
就调用PDOException来初始化一个异常类。
PDOException异常类的属性结构如下:
<?php
class PDOException extends Exception{
//错误信息,可以调用 PDO::errorInfo() 或 PDOStatement::errorInfo()来访问
public $errorInfo = null;
//异常信息,可以使用 Exception::getMessage() 来访问
protected $message;
//SQL状态错误代码,可以使用 Exception::getCode() 来访问
protected $code;
}
?>
这个异常处理类使用了PHP 5的异常处理类,下面简单地看一下PHP 5内置的异常处理类结构。
<?php
class Exception{
//属性
protected $message = ‘Unknown exception’; //异常信息
protected $code = 0; //用户自定义异常代码
protected $file; //发生异常的文件名
protected $line; //发生异常的代码行号
//方法
final function getMessage(); //返回异常信息
final function getCode(); //返回异常代码
final function getFile(); //返回发生异常的文件名
final function getLine(); //返回发生异常的代码行号
final function getTrace(); //backtrace()数组
final function getTraceAsString(); //已格式化成字符串的 getTrace() 信息
}
?>
相应的,在代码中可以合适地调用getFile()和getLine()来进行错误定位,以更方便地进行调试。
17.3.6  取得查询结果
PDO最大的特点之一是它的灵活性,本节将介绍如何取得查询结果,包括:
?  数组(数值或关联数组);
?  字符串(单列的结果集);
?  对象;
?  回调函数。
1.快取一行
FetchColumn是为应用程序取得一个仅包含单列的数据,代码如下:
<?php
$u = $db->query(“Select id FROM users Where login=‘login’ AND password=‘password’”);
fetch(PDO::FETCH_COLUMN)
if ($u->fetchColumn()) { //返回一个字符串
//登录成功
} else {
//验证失败
}

?>

2.取得一个标准对象
还可以将取得的一行作为一个标准类stdClass的对象实例,其中列名=属性名。
<?php
$res = $db->query(“Select * FROM foo”);
while ($obj = $res->fetch(PDO::FETCH_OBJ)) {
// $obj == instance of stdClass
}
?>
3.存取为一个类
PDO允许将结果保存为一个类,例子如下:
<?php
$res = $db->query(“Select * FROM foo”);
$res->setFetchMode(
PDO::FETCH_CLASS,
“className”,
array(‘optional’=‘Constructor Params’)
);
while ($obj = $res->fetch()) {
// $obj == instance of className
}
?>
4.从一个类取得常量
PDO允许查询的结果可以被用来生成一个目的类。
<?php
$res = $db->query(“Select * FROM foo”);
$res->setFetchMode(
PDO::FETCH_CLASS |
PDO::FETCH_CLASSTYPE
);
while ($obj = $res->fetch()) {
// $obj == instance of class who’s name is
// found in the value of the 1st column

}

?>

5.存取为一个对象
PDO还允许获取数据到一个已经存在的对象。

<?php

$u = new userObject;

$res = $db->query(“Select * FROM users”);

$res->setFetchMode(PDO::FETCH_INTO, $u);

while ($res->fetch()) {
// 取得的记录集将放在$u这个对象变量中,在此显示
}

?>
6.存取为关联数据
PDO实现了迭代器(Iteator)接口,允许一个方法实现迭代的功能。
<?php
$res = $db->query(
“Select * FROM users”,
PDO::FETCH_ASSOC
);
foreach ($res as $row) {
// $row是一个关联数组,可以直接显示,如$row[‘id’]
}
?>

7.fetchAll()方法
PDO也提供了和ADODB类似的fetchAll()方法,它允许从一个结果集中取得数据,然后放于关联数组中。

<?php
$db->setAttribute(PDO::ATTR_CASE, PDO::CASE_UPPER);
$qry = “Select * FROM users”;
$res = $db->query($qry)->fetchAll(PDO::FETCH_ASSOC);
?>

或者获取到索引数组里:
<?php
$res = $db->query(“Select * FROM foo”);
$result_arr = $res->fetchAll();
print_r($result_arr);
?>
数字索引数组比较浪费资源,请尽量使用关联数组,这样可以在内存中使用相当密集的大型结果集。
相关说明如下:

setAttribute()方法用于设置部分属性,主要属性有:PDO::ATTR_CASE、PDO::ATTR_ERRMODE等,这里需要设置的是PDO::ATTR_CASE,
就是使用关联索引获取数据集时,关联索引是大写还是小写,有如下几个选择:
?  PDO::CASE_LOWER——强制列名是小写;
?  PDO::CASE_NATURAL——列名按照原始的方式;
?  PDO::CASE_UPPER——强制列名为大写。
我们使用setFetchMode方法来设置获取结果集的返回值的数据类型,类型有:
?  PDO::FETCH_ASSOC——关联数组形式;
?  PDO::FETCH_NUM——数字索引数组形式;
?  PDO::FETCH_BOTH——两种数组形式都有,这是默认的;
?  PDO::FETCH_OBJ——按照对象的形式,类似于以前的 mysql_fetch_object()。
当然,一般情况下,我们使用PDO::FETCH_ASSOC取得关联数组。具体使用哪种类型,应按照自己的实际应用选择。

8.fetchColumn()方法
如果想获取指定记录里的一个字段结果,则可以使用PDOStatement::fetchColumn()。

<?php
$rs = $db->query(“Select COUNT(*) FROM foo”);
$col = $rs->fetchColumn();
echo $col;
?>
一般使用fetchColumn()方法进行count统计,对某些只需要单字段的记录可以很好地操作。

回调函数
PDO还规定在每一个fetch模式下,经过处理后的结果中使用一个回调函数。
<?php
function draw_message($subject,$email) { … }
$res = $db->query(“Select * FROM msg”);
$res->fetchAll(PDO::FETCH_FUNC,“draw_message”);
?>
10.直接查询的问题
直接使用Query查询行每次都会直接提交给数据库,如果查询较多,每次频频查询将导致效率降低。
另外,在安全问题上,没有过滤一些特殊字符容易产生SQL注入。

11.过滤字符
下面我们来看看如何使用PDO进行过滤引起SQL注入的方法,即过滤特殊字符。我们在PDO中使用quote()方法,使用例子如下:
$query = “Select * FROM users Where
login=“.$db->quote($_POST[‘login’]).”
AND
passwd=“.$db->quote($_POST[‘pass’]);

事务处理
PDO驱动程序支持所有的事务数据库,并且PDO提供更简便的方法,如下:
<?php
$db->beginTransaction();
if ($db->exec($qry) === FALSE) {
$db->rollback();
}
$db->commit();
?>

执行一个批处理事务
在下面的示例中,假设我们为一个新雇员创建一组条目,这个雇员有一个ID号,
即23。除了输入这个雇员的基本数据外,还需要记录雇员的薪水。分别完成两个更新很简单,
但通过将这两个更新包括在beginTransaction()和commit()调用中,就可以保证在更改完成之前,
其他人无法看到更改。如果发生了错误,catch块可以回滚事务开始以来发生的所有更改,
并打印出一条错误消息。代码内容如下:
<?php
try {
$dbh = new PDO(‘odbc:SAMPLE’, ‘db2inst1’, ‘ibmdb2’,
array(PDO_ATTR_PERSISTENT => true));
echo “Connected\n”;
$dbh->setAttribute(PDO_ATTR_ERRMODE, PDO_ERRMODE_EXCEPTION);
$dbh->beginTransaction();
$dbh->exec(“insert into staff (id, first, last) values (23, ‘Joe’, ‘Bloggs’)”);
$dbh->exec(“insert into salarychange (id, amount, changedate)
values (23, 50000, NOW())”);
$dbh->commit();
} catch (Exception $e) {
$dbh->rollBack();
echo “Failed: ” . $e->getMessage();
}
?>

并不是一定要在事务中做出更新,也可以通过复杂的查询来提取数据,还可以使用信息构建更多的更新和查询。
当事务在活动时,可以保证其他人在工作进行当中无法做出更改。
事实上,这不是100%的正确,但如果您之前没有听说过事务的话,这样介绍也未尝不可。
下面是一个扩展PDO&PDO语句的类,内容如下:
<?php
class Database extends PDO{
function __construct()   {
parent::__construct(‘mysql:dbname=test;host=localhost’, ‘root’, ”);
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS,array(‘DBStatement’array($this)));
}
}
class DBStatement extends PDOStatement{
public $dbh;
protected function __construct($dbh)   {
$this->dbh = $dbh;
$this->setFetchMode(PDO::FETCH_OBJ);
}
public function foundRows()   {
$rows = $this->dbh->prepare(‘Select found_rows() AS rows’,array(PDO::MYSQL_ATTR_ USE_BUFFERED_QUERY => TRUE));
$rows->execute();
$rowsCount = $rows->fetch(PDO::FETCH_OBJ)->rows;
$rows->closeCursor();
return $rowsCount;
}
}
?>

存储过程
在MySQL一章,我们已经了解了存储过程的创建,下面我们来看使用PDO调用的例子:
<?php
$stmt = $dbh->prepare(“CALL sp_set_string(?)”);
$stmt->bindParam(1, $str);
$str = ‘hospinfo’; //绑定参数
$stmt->execute();
?>
与先前讲过的绑定例子差不多,只是这里使用了“?”数据绑定方法,sp_set_string是存储过程名称。
带有输出参数的存储过程:
<?php
$stmt = $dbh->prepare(“CALL sp_get_string(?)”);
$stmt->bindParam(1, $ret,PDO:ARAM_STR, 4000);
if ($stmt->execute()) {
echo “返回值 $ret\n”;
}
下面是绑定列输出的脚本例子:
$stmt = $dbh->prepare(“Select extension, name from CREDITS”);
if ($stmt->execute()) {
$stmt->bindColumn(‘extension’, $extension);
$stmt->bindColumn(‘name’, $name);
while ($stmt->fetch(PDO::FETCH_BOUND)) {
echo “Extension: $extension\n”;
echo “Author: $name\n”;
}

Leave a Reply