Total Pageviews

Monday 20 June 2016

在PHP中使用PDO访问和操作数据库

PDO(PHP Data Objects)扩展为PHP访问数据库定义了一个轻量级的、一致性的接口,它提供了一个数据访问抽象层,这样,无论使用什么数据库,都可以通过一致的函数执行查询和获取数据。PDO随PHP5.1发行,无法运行于之前的PHP版本。
PDO并不支持每种数据库特有的语法,但是在大多数情况下,仅仅只需要更改数据库连接地址,就可以方便的更换数据库。

1. 安装数据库驱动

在使用PDO操作数据库之前必须要检查自己是否安装了对应数据库的驱动:

目前,PDO支持的数据库有如下几种:
  • PDO_DBLIB ( FreeTDS / Microsoft SQL Server / Sybase )
  • PDO_FIREBIRD ( Firebird/Interbase 6 )
  • PDO_IBM ( IBM DB2 )
  • PDO_INFORMIX ( IBM Informix Dynamic Server )
  • PDO_MYSQL ( MySQL 3.x/4.x/5.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 )
  • PDO_4D ( 4D )
操作系统中并不保证安装了这些驱动,可以通过如下代码查看系统中安装了那些驱动:
print_r(PDO::getAvailableDrivers());

2. 连接数据库

在PDO中,不同的数据库连接方式基本是统一,只是有稍微的差别,他们都遵循如下的格式:
$dbh = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
所以连接MySQL的代码如下。
try {
  # MySQL with PDO_MYSQL
  $dbh = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
} catch(PDOException $e) {
  echo $e->getMessage();
}
值得注意的是,尽量将所有的PDO操作都放到try/catch代码块里面,这样就可以捕获操作中的异常了。
下面是另外3中常见数据库的连接,MS SQL和Sybase的连接和MySQL基本一致,而SQLite比较特殊,连接方式稍有不同。
# MS SQL Server and Sybase with PDO_DBLIB
$dbh = new PDO("mssql:host=$host;dbname=$dbname", $user, $pass);
$dbh = new PDO("sybase:host=$host;dbname=$dbname", $user, $pass);
# SQLite Database
$dbh = new PDO("sqlite:my/database/path/database.db");
数据库连接成功后,会返回一个Database Handle,也就是代码中的$dbh。以后对数据库的增删查改都将通过这个Handle进行。要想关闭数据库的连接,只需要将这个Handle赋值为null就行。
# close the connection
$dbh = null;
另外,在连接的时候,通常将ATTR_ERRMODE设置为ERRMODE_EXCEPTION。这样可以操作的时候出了任何错误,都会抛出异常,你可以catch到进行处理。
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

3. 插入和修改

使用PDO插入和修改数据,通常需要分成三步:prepare、bind、execute。
经过prepare阶段,会返回PDOStatement对象,后续可以通过这个PDOStatement对象操作数据库。下面是一个小例子,其中$stmt就是一个PDOStatement对象。
# stmt means "Statement Handle"
$stmt = $dbh->prepare("INSERT INTO folks ( first_name ) values ( 'Cathy' )");
$stmt->execute()
在对数据库插入和修改的过程中,会有新的数据存入数据库。通常这些操作都有受到数据库注入攻击的风险。 使用这种prepare->bind->execute的方法可以有效避免这种攻击。
在prepare阶段,可以在SQL语句中使用数据占位符,然后在bind阶段将数据变量绑定到这些占位符上,最后execute将数据存入数据库。正是通过这种间接的方式存储数据,bind操作避免了SQL注入。
prepare的时候可以使用两种类别的占位符:命名占位符和非命名占位符。

3.1 非命名占位符

非命名展位符使用?号在SQL中占据将要传入数据的位置。在bind阶段使用从1开始的数据代表这些占位符,下面的代码是一个例子。
# unnamed placeholders
$stmt = $dbh->prepare("INSERT INTO folks (name, addr, city) values (?, ?, ?)");

# assign variables to each place holder, indexed 1-3
$stmt->bindParam(1, $name);
$stmt->bindParam(2, $addr);
$stmt->bindParam(3, $city);

# insert one row
$name = "Daniel"
$addr = "1 Wicked Way";
$city = "Arlington Heights";
$stmt->execute();

# insert another row with different values
$name = "Steve"
$addr = "5 Circle Drive";
$city = "Schaumburg";
$stmt->execute();
如果将数据按照顺序存入数组中,那么有一种更简单的写法。
# the data we want to insert
$data = array('Cathy', '9 Dark and Twisty Road', 'Cardiff');
$stmt = $dbh->("INSERT INTO folks (name, addr, city) values (?, ?, ?);
$stmt->execute($data);

3.2 命名占位符

命名占位符使用以:开头的字符串占据数据插入的位置。
# named placeholders
$stmt = $dbh->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
绑定数据变量的时候采用如下方式:
# the first argument is the named placeholder name - notice named
# placeholders always start with a colon.
$stmt->bindParam(':name', $name);
同样,如果将数据存储到HASH数组里面,可以跳过bind步骤,写法更为简单。
# the data we want to insert
$data = array( 'name' => 'Cathy', 'addr' => '9 Dark and Twisty', 'city' => 'Cardiff' );

# the shortcut!
$stmt = $dbh->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$stmt->execute($data);
注意:数组的key不用带:号,但是名字要与占位符匹配。
命名占位符还有另外一个强大的功能:可以将对象直接插入数据库。如果对象的属性和占位符名字匹配,可以采用如下方式。
# a simple object
class person {
    public $name;
    public $addr;
    public $city;

    function __construct($n,$a,$c) {
        $this->name = $n;
        $this->addr = $a;
        $this->city = $c;
    }
    # etc ...
}

$cathy = new person('Cathy','9 Dark and Twisty','Cardiff');

# here's the fun part:
$stmt = $dbh->("INSERT INTO folks (name, addr, city) value (:name, :addr, :city)");
$stmt->execute((array)$cathy);

4. 查询数据

要想取出数据库中的数据,需要在执行查询语句之后,在PDOstatement对象上执行fetch()函数。取出的数据可以被整理成各种格式:HASH数组、对象、数组等等。PDO定义了如下几种格式:
  • PDO::FETCH_ASSOC
  • PDO::FETCH_BOTH (default)
  • PDO::FETCH_BOUND
  • PDO::FETCH_CLASS
  • PDO::FETCH_INTO
  • PDO::FETCH_LAZY
  • PDO::FETCH_NUM
  • PDO::FETCH_OBJ
使用得最多的几种是:PDO::FETCH_ASSOC,PDO::FETCH_OBJ。
可以通过setFetchMode()指定数据返回的格式。
$stmt->setFetchMode(PDO::FETCH_ASSOC);
也可以在fetch()中直接指定格式。
一个使用PDO::FETCH_ASSOC的例子:
# using the shortcut ->query() method here since there are no variable
# values in the select statement.

$stmt = $dbh->query('SELECT name, addr, city from folks');

# setting the fetch mode
$stmt->setFetchMode(PDO::FETCH_ASSOC);

while($row = $stmt->fetch()) {
    echo $row['name'] . "\n";
    echo $row['addr'] . "\n";
    echo $row['city'] . "\n";
}
fetch()函数每次只能取出一条数据,所以需要循环读取。如果想要一次取出所有的数据,可以使用fetchAll()函数。
如果使用FETCH_OBJ模式,将返回一个匿名对象。对象的属性名对应数据库的列名。
# creating the statement
$stmt = $dbh->query('SELECT name, addr, city from folks');

# setting the fetch mode
$stmt->setFetchMode(PDO::FETCH_OBJ);

# showing the results
while($row = $stmt->fetch()) {
    echo $row->name . "\n";
    echo $row->addr . "\n";
    echo $row->city . "\n";
}

5. 其它一些有用的操作

获得最后一条插入数据的ID。
$dbh->lastInsertId();
这个函数属于数据库的Handler,而不是PDOStatement。
$dbh->exec('DELETE FROM folks WHERE 1');
$dbh->exec("SET time_zone = '-8:00'");
exec()函数可以用来执行那些不会返回数据的SQL语句。
$rows_affected = $stmt->rowCount();
rowCount()用来返回受到影响的列数目,但是对于SELECT语句却不适用。
下面的语句,可以获得SELECT选出的行数。
$stmt = $dbh->query("SELECT COUNT(*) as count FROM folks");
$rows_count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];

参考资料

  1. Why you Should be using PHP’s PDO for Database Access
  2. PHP Data Objects – PHP Manual