(唯一合适) PDO 教程

PDO是什么

首先思考, 为什么选择PDO

PDO 是一个数据访问抽象层(Database Access Abstraction Layer). 抽象是双重的: 一个是众所周知但不太重要的. 另一个是模糊的但是是最重要的.
众所周知 PDO 为不同的数据库提供了统一的接口. 虽然这个功能本身很庞大, 但是对于固定程序来说不是过于重要的事情, 基本所有的程序都是使用统一的后端数据库. 尽管有一些谣言, 但是通过改变单行 PDO 配置来切换后端数据库是不可能的-由于不同的 SQL 风格(为此, 需要使用像 DQL 这样的平均查询语言). 因此对于普通的 LAMP 开发者来说, 这一点是微不足道的, 并且对他而言, PDO只是熟悉的 mysql(i)_query() 函数的另一个更复杂版本. 但实际上它不是, 它有丰富的其他功能.
PDO 不仅抽象了数据库API, 还抽象了基本操作, 否则必须在每个应用程序中重复数百次, 使您的代码非常WET. 不同于 mysqlmysqli , 两个都不能直接使用低级裸 APIs(但仅作为某些更高级别抽象层的构建材料), PDO就是这样的抽象. 虽然仍是不完整的, 但是至少可用.
真正的PDO好处是:

  • 安全性 (可用的准备语句)
  • 可用性 (许多辅助函数可以自动执行日常操作)
  • 可重用性 (用于访问大量数据库的统一API, 从SQLITE到oracle)

请注意, 尽管 PDO 是原生数据库驱动程序中最好的, 但对于现代WEB应用程序来说, 请考虑将使用有查询构建器的 ORM 或者与其他更高抽象级别的库一起使用, 只是偶尔使用原生的PDO. 好的ORM比如 Doctrine, Eloquent, RedBeanYii::AR. Aura.SQL 是具有很多附加功能的使用PDO包装器的一个很好的例子.
无论哪种方式, 首先要了解基本工具是件好事. 那么, 让我们开始吧:

connection DSN

PDO有一个叫 DSN 的预想接方式. 它并不复杂-PDO需要你在三个不同的位置输入不同的配置, 而不是一个简单的选项列表.

  • database driver, host, db(schema) namecharset, 以及不常使用的 portunix_socket 设置 DSN
  • user_namepassword 设置构造方法
  • 其他所有的配置在options数组

其中 DSN 是以分号分隔的字符串, 由 param=value 键值对组成, 从驱动程序名称和冒号开始:

mysql:host=localhost;dbname=test;port=3306;charset=utf8mb4
driver^  ^colon           ^param=value pair   ^semicolon

注意, 遵循正确的格式是非常重要的- DSN中不能使用 空格, 引号, 和其他的符号, 只能使用参数, 值和定界符. 就像手册上展示的.

这里有一个例子:

$host = '127.0.0.1';
$db = 'test';
$pass = 'root';
$charset = 'utf8mb4';

$dsn = "mysql:host={$host};dbnamej={$db};charset={$charset}";
$options = [
    PDO::ATR_ERRMODE => PDO::ERRMODE_EXECPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];
try {
    $pdo = new \PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
}

设置了所有上述变量属性, 我们将在 $pdo 变量中得到一个正确的 PDO 实例.
使用旧mysql扩展用户重要通知

  1. 不同于 mysql_* 函数, 可以在代码的任意位置使用, PDO 实例被存储在一个变量中, 那就意味着只能在函数内部进行访问. 因此, 必须通过函数参数传递或使用更高级的技术, 比如IOC容器.
  2. 连接只用创建一次. 不要在函数, 类构造函数创建连接, 否则, 会创建多个连接, 最终导致数据库服务宕机. 因此必须创建唯一的 PDO 实例, 让整个脚本使用.(适用于FPM模式)
  3. 通过DSN设置字符集是非常重要的-这是唯一正确的方式因为它会告诉PDO哪个字符集会被使用. 因此, 忘记通过 Query 运行SET NAMES 或者通过 PDO::MYSQL_ATTR_INIT_COMMAND. 只有当PHP版本过低时(低于5.3.6), 才可以使用 SET NAMES 查询, 并且关闭仿真模式.

更多关于连接的内容可以在 连接MySQL查看

运行查询 PDO::query()

使用 PDO 有两种方式运行查询. 如果查询中没有使用变量, 可以使用 PDO::query 方法. 它会运行查询并返回一个 PDOStatement 类的对象, 该类与 mysql_query 返回的资源大致相同, 特别时从中获取实际记录的操作:

$stmt = $pdo->query('SELECT name FROM users');
while ($row = $stmt->fetch()) {
    echo $row['name'] . "\n";
}

并且 query() 方法允许我们使用一个整洁的方法连接 SELECT 查询, 如下所示.

预处理, 防止SQL注入

放弃熟悉的 mysql_query()函数 并进入严格数据对象领域的主要原因是 PDO 已经准备好了开箱即用的预处理语句. 如果要在语句中使用变量, 预处理语句是唯一正确运行的方式. 它如此重要的原因在 The Hitchhiker's Guide to SQL Injection prevention.有详细的解释.
对于运行的查询, 如果至少使用一个变量, 你必须使用占位符替换它. 准备执行语句, 然后分别传入变量执行.
长话短说, 它不像感觉的那么困难. 在大多数例子中, 你只需要使用函数 prepareexecute .
首先, 需要修改查询, 在使用变量的位置添加占位符, 就像这样

$sql = "SELECT * FROM users WHERE email = '{$email}' AND status = '{$status}'";

改为

$sql = "SELECT * FROM users where email = ? and status = ?";

或者

$sql = "SELECT * FROM users where email = :email AND status = :status";

注意 PDO 支持位置(?)和命名(:email)占位符, 后者始终以冒号开始,并且只能使用字母, 数字和下划线. 还需要注意 占位符周围不能使用引号 .
一个查询使用了占位符, 就必须使用PDO::prepare()方法预处理. 这个方法返回一个和我们上边讨论的相同的 PDOStatement 对象, 但是没有绑定任何数据.
最后, 必须使用 PDOStatement 对象的 execute() 方法执行查询, 并且通过数组形式传递参数. 之后, 就可以从语句中得到结果数据(如果适用).

$stmt = $pdo->prepare("SELECT * FROM users WHERES email = ? AND status = ?");
$stmt->execute([$email, $status])
$user = $stmt->fetch();

// or
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email AND status = :status");
$stmt->execute(['email' => $email, 'status' => $status]);
$user = $stmt->fetch();

可以看到, 位置占位符, 你需要提供一个索引数组. 命名占位符, 需要提供一个关联数组, 并且键要匹配查询中的占位符. 同一个查询中不能混合位置占位符和命名占位符.
位置占位符可以让你写更简短的代码, 但是对参数顺序是敏感的(必须于查询中参数的顺序一致). 虽然命名占位符使代码更冗长, 但是允许随机参数绑定.
另外需要注意, 虽然存在普遍的误解, 但是数组键中 : 不是必须的.
执行后就可以使用支持的方法获取结果.

更多的例子可以查看(respective article)[https://phpdelusions.net/pdo_...].

参数绑定

将数据传入 execute() (如上所示)方法中应被视为默认的最方便的方式. 如果使用这个方法, 所有参数都将会绑定为字符串(如果使用NULL值, 将会使用SQL NULL发送给查询), 大多数时候都没有问题.
但是, 有时候最好明确设置类型. 可能情况如下:

  • 开启仿真模式的 LIMIT 子句(或者其他不能接受字符串操作数的 SQL 子句)
  • 可能受到错误操作数类型影响具有特殊查询计划的复杂特殊查询
  • 特有的列类型, 像 bigint boolean 必须绑定精确的操作数(为了将 bigint 绑定为 PDO::PARAM_INT 需要基于 mysqlnd)

这种情况下, 必须使用显式绑定, 可以从 bindvalue()bindParam() 两个函数中选择一个. 前者是推荐使用的, 它不像 bindParam()具有一定的副作用.

查询可以绑定的部分

了解哪些查询部分可以使用参数绑定哪些部分不能使用是非常重要的. 事实上, 这个列表是非常短的: 只有字符串和数字字面量可以被绑定. 只要你的数据在查询中能被表示为数字或者带引号的字符串, 就可以被绑定. 其他所有情况你不能使用 PDO 预处理语句: 既不是标识符也不是逗号分隔列表, 或者是引用的文字字符串的一部分, 或者其他任意查询部分都不能使用预准备语句绑定
最常见的用例解决方案可以在[本章的响应部分查看]()

预处理, 多次执行

有时候你可以使用预处理多次执行准备好的查询, 比一次又一次执行相同的查询快一点, 因为它只解析查询一次. 如果可以执行另一个PHP实例中的预处理语句, 这个功能就是非常有用的, 但是事实并非如此. 只会在同一个实例中重复相同的查询, 这在常规的PHP脚本中很少使用到, 并限制了此功能用于重复插入和更新.

$data = [
    1 => 1000,
    2 => 200,
    3 => 200,
];
$stmt = $pdo->prepare('UPDATE users SET bonus = bonus + ? where id = ?');
foreach ($data as $id => $bonus) {
    $stmt->execute([$bonus, $id]);
}

注意这个功能有点被高估了. 不仅需要讨论, 而且性能提升也不是很大 - 查询解析有时候是
很快的. 而且只有在关闭仿真模式的时候才能带来性能提升.

运行SELECT INSERT UPDATE DELETE语句

这些查询没有什么特别之处, 对PDO来说他们都是一样的. 运行哪个查询并不重要.
如上所示, 需要准备带有占位符的预处理查询, 传入变量并执行. DELETESELECT 的处理过程是基本相同的. 仅有的不同点是( DML查询不会返回任何数据), 你可以使用链式方法, 调用 execute()prepare().

$sql = "UPDATE users SET name = ? where id = ?";
$pdo->prepare($sql)->execute([$name, $id]);

然而, 你像获得影响行数, 代码将和无聊的三行代码相同:

$stmt = $pdo->prepare("DELETE FROM goods where category = ?");
$stmt->execute([$cat]);
$deleted = $stmt->rowCount();

更多的例子可以在respective article.找到.

从statement获取数据 foreach

我们已经见过这个函数了, 现在让我们仔细看看. 它从数据库获取单行数据, 在结果集中移动内部指针, 因此, 对函数的后续调用将逐个返回所有行. 这个方法和 mysql_fetch_array() 大致相同但在工作模式稍微有点不同: 代替很多不同函数( mysql_fetch_assoc() mysql_fetch_row), 这个只有一个方法, 但是它的行为可以通过一个参数改变. 在 PDO 中有很多的获取模式, 稍后我们详细讨论, 这里有一些简单的实例:

  • PDO::FETCH_NUM 返回索引数组
  • PDO::FETCH_ASSOC 返回关联数组
  • PDO::FETCH_BOTH 以上两者都包含
  • PDO::FETCH_OBJ 返回对象
  • PDO::FETCH_LAZY 允许三个(索引数组, 关联数组, 对象)方法没有内存开销.

从上面可以看出, 这个必须在两种情况下使用:

  1. 当只需要一行时, 只获取一行

    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    将以关联数组的方式从语句中获取一行

  2. 当我们需要在使用之前处理返回数据. 在这种情况下, 必须通过while循环运行, 如上所示.

另一种有用的模式是 PDO::FETCH_CLASS 可以创建一个特定类的对象

$news = $pdo->query("select * from news")->fetchAll(PDO::FETCH_CLASS, 'News');

将生成一个News类对象的数组, 并且通过返回值设置类属性. 注意这个模式下:

  • 属性会在构造方法之前设置
  • 所有未定义的属性都会调用 __set魔术方法
  • 如果没有 __set方法, 将会创建新属性
  • 私有属性也会被设置, 这有点意外但是非常方便

相关推荐