PDO 与 MySQLi:PHP 数据库 API 之战

秦涵涤
2023-12-01

介绍

mysql_使用该扩展的日子已经一去不复返了,因为它的方法自 PHP 5.5起已被弃用,自PHP 7 起已被删除。唉,互联网仍然受到大量旧教程的困扰,初学者只需复制/粘贴并在具有旧 PHP 版本的共享托管平台上使用,从而继续其遗产。

如果您在 PHP 中使用 MySQL 或 MariaDB,那么您可以选择 MySQLi 或 PDO。前者只是一个简单的改进版本,具有过程和 OOP 支持并添加了准备好的语句,而后者是一个抽象层,允许您对其支持的所有12 个数据库驱动程序使用统一的 API 。尽管应该提到的是,无论如何,MySQL 无疑是 PHP 世界中最流行的数据库。

从理论上讲,人们可能会认为讨论应该结束了。我们不需要为存在的每种类型的数据库提供特定于供应商的 API,因为只使用一个就简单多了。虽然这肯定有很多道理,但问题是它PDO_MYSQL根本不具备 MySQLi 所具备的所有最新和最先进的功能。老实说,我不明白为什么会这样,因为它会完全消除使用供应商特定 API 的任何理由。也就是说,我想大多数人不需要这些额外的功能,但肯定有一些人需要。

对于任何感兴趣的人,这里是PDOMySQLi的完整文章。

PDO 优势

MySQLi 有很多方面可以追赶 PDO。它需要添加以下功能:

  1. 有用的获取模式
  2. 允许将变量和值直接传递给执行
  3. 自动检测变量类型的能力(实际发生的情况是,所有内容在发送到服务器时都被视为字符串,但被转换为正确的类型。这在 100% 的时间内适用于原生准备好的语句,但不适用于某些边缘情况,例如具有仿真模式的 LIKE。)
  4. 提供一个选项以使用准备好的语句自动缓冲结果
  5. 命名参数(虽然在 PDO 中关闭仿真模式时无用,因为您只能使用相同的名称一次)

如您所见,如果 MySQLi 想要保持相关性,它应该从 PDO 学习很多东西。

如果是这样,那么应该没有任何区别。人们总是谈论你必须如何学习一个全新的扩展,但实际上它们在大多数情况下已经几乎相同。对于本文,我将使用本机准备好的语句,但模拟的语句也完全可以接受。这是关于两者之间差异的简短说明。

MySQLi 优势

PDO 也缺少一些功能,尽管对大多数用户来说不太重要,例如:

  1. 异步查询
  2. 能够获取有关受影响行的更多信息,例如更新具有相同值的行(可以在 PDO 中作为构造函数设置完成,以后无法更改)
  3. 正确的数据库关闭方法
  4. 一次多个查询(尽管如果在 PDO 中打开了仿真模式,则可以)
  5. 使用持久连接进行自动清理

那么我应该使用哪个?

我的观点是应该默认使用 PDO,尤其是初学者,因为它的多功能性、一般可预测性和有用的获取模式。然而,对于想要最新的、特定于 MySQL 的功能的高级用户来说,MySQLi 将是一个更好的选择。

具有讽刺意味的是,更有经验的 PHP 开发人员倾向于在 100% 的情况下认为 PDO 是唯一可接受的选项,而初学者则倾向于使用 MySQLi。从两端来看,这绝对是疯狂的。当然,大多数开发人员并不真正需要MySQLi 提供的额外高级功能,但如前所述,它肯定对某些人非常有用。

尤其令人好奇的是,新手害怕尝试“新”事物并切换到 PDO,而许多高级用户则将“易于从数据库驱动程序切换”的论点背诵为 PDO 的优势。任何相信您可以轻松地在 PDO 中的数据库之间无缝切换的神话的人显然从未尝试过这样做。每个驱动程序都不同,从 Microsoft SQL Server 到 MySQL 的切换肯定不会自动进行。首先让我们澄清一件事,语法非常相似——几乎相同,我将在示例中展示它。PDO 也不是 MySQLi 之上的抽象层,而是 PDO_MYSQL 之上的抽象层。

如果 PDO 最终能跟上所有最新的或独特的 MySQL 功能,那么我就会明白为什么 MySQLi 应该消失了;我什至会鼓励它,如果它最终是这样的话。尽管 PDO 确实有几个特定于驱动程序的功能,但它并不具备全部功能,也没有跟上最新的功能。这正是我认为 MySQLi 和 PDO 不一定是竞争对手的原因,而是目前两个重点完全不同的强大库。然而,显然 PDO 应该得到更广泛的应用。但如前所述,差异几乎可以忽略不计。正如前面多次提到的,MySQLi 的生存依赖于它赶上 PDO,同时 PDO 主要坚持在它支持的大多数 DB 驱动程序中使用的特性。

代码差异

如前所述,PDO 和 MySQLi 都非常相似,但语法略有不同。MySQLi 遵循老式的 PHP 蛇大小写约定,而 PDO 使用 camelCase。此外,MySQLi 的方法用作对象属性,而 PDO 使用传统的函数语法。

我永远无法理解为什么 PDO 和 MySQLi 都强迫您使用两种单独的方法来使用准备好的语句,从而使事情变得复杂。幸运的是,PDO 消除了使用专用绑定函数的需要——尽管我不确定为什么对execute(). 未准备好的 MySQLi 和 PDO 确实还不错,只是不幸的准备好的语句的实现导致它们看起来很冗长。例如,在特定于供应商的PostgreSQL API中,您可以这样做。这是一个示例,说明如何使用 MySQLi 和 PDO 执行“未准备”查询以获取关联数组,以供参考。

$arr = $mysqli->query("SELECT * FROM myTable")->fetch_all(MYSQLI_ASSOC);
复制
$arr = $pdo->query("SELECT * FROM myTable")->fetchAll(PDO::FETCH_ASSOC);
复制

实际上,最好的方法是使用包装器、查询构建器或 ORM。我有一个你可能喜欢的相当基本的MySQLi 包装器。虽然 PDO 是朝着正确方向迈出的一步,因为您可以将值直接绑定到执行中,但它仍然不理想。在我创建的类中,您可以链接所有调用,同时将要绑定的值作为参数参数传递。看看你能做什么。

$arr = $mysqli->query("SELECT * FROM myTable WHERE id > ?", [12])->fetchAll('assoc');
复制

您现在以更简洁的方式将整个关联数组存储在变量中。奇怪的是为什么 PDO 和 MySQLi 都不这样做。

对于本教程的其余部分,我们将使用准备好的语句,因为没有充分的理由不将它们用于 SQL 注入保护,除非您使用的是 async 之类的功能,该功能目前不支持它们。否则,您将需要正确手动设置查询格式。

创建新的数据库连接

PDO

$dsn = "mysql:host=localhost;dbname=myDatabase;charset=utf8mb4";
$options = [
  PDO::ATTR_EMULATE_PREPARES   => false, // turn off emulation mode for "real" prepared statements
  PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, //turn on errors in the form of exceptions
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, //make the default fetch be an associative array
];
try {
  $pdo = new PDO($dsn, "username", "password", $options);
} catch (Exception $e) {
  error_log($e->getMessage());
  exit('Something weird happened'); //something a user can understand
}
复制

MySQLi

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
  $mysqli = new mysqli("localhost", "username", "password", "databaseName");
  $mysqli->set_charset("utf8mb4");
} catch(Exception $e) {
  error_log($e->getMessage());
  exit('Error connecting to database'); //Should be a message a typical user could understand
}
复制

插入、更新、删除

PDO

$stmt = $pdo->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
$stmt->execute([$_POST['name'], 29]);
$stmt = null;
复制

MySQLi

$stmt = $mysqli->prepare("UPDATE myTable SET name = ? WHERE id = ?");
$stmt->bind_param("si", $_POST['name'], $_SESSION['id']);
$stmt->execute();
$stmt->close();
复制

应该注意的是,使用 PDO,您可以链接prepare()and execute(),尽管您不会获得受影响的行,所以我看不出这很有用。

获取受影响的行数

PDO

$stmt->rowCount();
复制

MySQLi

$stmt->affected_rows;
复制

获取插入的最新主键

请注意,这两者如何使用连接变量,而不是$stmt.

PDO

$pdo->lastInsertId();
复制

MySQLi

$mysqli->insert_id;
复制

获取匹配的行

PDO

在 PDO 中,实现此目的的唯一方法是将其设置为连接选项以更改 的行为rowCount(),不幸的是。这意味着rowCount()将为您的整个数据库连接返回匹配的行或更改的行,但不能同时返回两者。

$options = [
  PDO::MYSQL_ATTR_FOUND_ROWS => true
];
复制

MySQLi

$mysqli->info;
复制

这将输出一整串信息,如下所示:

Rows matched: 1 Changed: 0 Warnings: 0

我不知道他们为什么认为这是一个明智的实现,因为它在数组中会方便得多。幸运的是,你可以做到这一点。

preg_match_all('/(\S[^:]+): (\d+)/', $mysqli->info, $matches); 
$infoArr = array_combine ($matches[1], $matches[2]);
var_export($infoArr);
复制

现在您可以很容易地访问这些值。请注意,该值是一个字符串,因此您可以将所有值转换为整数,这样===可以工作或严格检查==.

['Rows matched' => '1', 'Changed' => '0', 'Warnings' => '0']
复制

抓取

获取关联数组

PDO

$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id <= ?");
$stmt->execute([5]);
$arr = $stmt->fetchAll(PDO::FETCH_ASSOC);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?");
$stmt->bind_param("s", $_POST['name']);
$stmt->execute();
$arr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

获取单行

PDO

$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE name = ?");
$stmt->execute([$_POST['name']]);
$arr = $stmt->fetch(PDO::FETCH_ASSOC);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?");
$stmt->bind_param("s", $_POST['name']);
$stmt->execute();
$arr = $stmt->get_result()->fetch_assoc();
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

获取单个值(标量)

PDO

$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE name = ?");
$stmt->execute([$_POST['name']]);
$arr = $stmt->fetch(PDO::FETCH_COLUMN);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?");
$stmt->bind_param("s", $_POST['name']);
$stmt->execute();
$arr = $stmt->get_result()->fetch_row()[0];
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

获取对象数组

PDO

class myClass {}
$stmt = $pdo->prepare("SELECT name, age, weight FROM myTable WHERE name = ?");
$stmt->execute(['Joe']);
$arr = $stmt->fetchAll(PDO::FETCH_CLASS, 'myClass');
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

class myClass {}
$arr = [];
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE id = ?");
$stmt->bind_param("s", $_SESSION['id']);
$stmt->execute();
$result = $stmt->get_result();
while($row = $result->fetch_object('myClass')) {
  $arr[] = $row;
}
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

如您所见,PDO 真的在这里大放异彩。MySQLi 没有类似$mysqli_result->fetch_all(MYSQLI_OBJ). PDO 甚至更进一步,并且有一个很棒的方法来处理它在类构造函数之后被调用的恼人的默认行为,通过使用fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'myClass'). 可以在 MySQLi 中复制这种行为,但它依赖于省略构造函数并依赖魔法__set(),或者仅在构造函数不等于默认值时将其设置在构造函数中。

SQL LIKE语句

PDO

$search = "%{$_POST['search']}%";
$stmt = $pdo->prepare("SELECT id, name, age FROM myTable WHERE name LIKE ?");
$stmt->execute([$search]);
$arr = $stmt->fetchAll();
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$search = "%{$_POST['search']}%";
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name LIKE ?"); 
$stmt->bind_param("s", $search);
$stmt->execute();
$arr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

获取模式

这是迄今为止我最喜欢的关于 PDO 的功能。PDO 中的获取模式非常有用,令人震惊的是 MySQLi 还没有添加它们。

获取键/值对

PDO

$stmt = $pdo->prepare("SELECT event_name, location FROM events WHERE id < ?");
$stmt->execute([25]);
$arr = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$arr = [];
$id = 25;
$stmt = $con->prepare("SELECT event_name, location FROM events WHERE id < ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
while($row = $result->fetch_row()) {
  $arr[$row[0]] = $row[1];
}
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

输出:

['Cool Event' => 'Seattle', 'Fun Event' => 'Dallas', 'Boring Event' => 'Chicago']
复制

获取组列

PDO

$stmt = $pdo->prepare("SELECT hair_color, name FROM myTable WHERE id < ?");
$stmt->execute([10]);
$arr = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_COLUMN);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$arr = [];
$id = 10;
$stmt = $con->prepare("SELECT hair_color, name FROM myTable WHERE id < ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
while($row = $result->fetch_row()) {
  $arr[$row[0]][] = $row[1];
}
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

输出:

[
  'blonde' => ['Patrick', 'Olivia'],
  'brunette' => ['Kyle', 'Ricky'],
  'red' => ['Jordan', 'Eric']
]
复制

获取键/值对数组

PDO

$stmt = $pdo->prepare("SELECT id, max_bench, max_squat FROM myTable WHERE weight < ?");
$stmt->execute([200]);
$arr = $stmt->fetchAll(PDO::FETCH_UNIQUE);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$arr = [];
$weight = 200;
$stmt = $con->prepare("SELECT id, max_bench, max_squat FROM myTable WHERE weight < ?");
$stmt->bind_param("i", $weight);
$stmt->execute();
$result = $stmt->get_result();
$firstColName = $result->fetch_field_direct(0)->name;
while($row = $stmtResult->fetch_assoc()) {
  $firstColVal = $row[$firstColName];
  unset($row[$firstColName]);
  $arr[$firstColVal] = $row;
}
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

输出:

[
  17 => ['max_bench' => 230, 'max_squat' => 175],
  84 => ['max_bench' => 195, 'max_squat' => 235],
  136 => ['max_bench' => 135, 'max_squat' => 285]
]
复制

获取组

PDO

$stmt = $pdo->prepare("SELECT hair_color, name, age FROM myTable WHERE id < ?");
$stmt->execute([12]);
$arr = $stmt->fetchAll(PDO::FETCH_GROUP);
if(!$arr) exit('No rows');
var_export($arr);
$stmt = null;
复制

MySQLi

$arr = [];
$id = 12;
$stmt = $con->prepare("SELECT hair_color, name, age FROM myTable WHERE id < ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result();
$firstColName = $result->fetch_field_direct(0)->name;
while($row = $stmtResult->fetch_assoc()) {
  $firstColVal = $row[$firstColName];
  unset($row[$firstColName]);
  $arr[$firstColVal][] = $row;
}
if(!$arr) exit('No rows');
var_export($arr);
$stmt->close();
复制

输出:

[
  'blonde' => [
    ['name' => 'Patrick', 'age' => 22],
    ['name' => 'Olivia', 'age' => 18]
  ],
  'brunette'  => [
    ['name' => 'Kyle', 'age'=> 25],
    ['name' => 'Ricky', 'age' => 34]
  ],
   'red'  => [
    ['name' => 'Jordan', 'age' => 17],
    ['name' => 'Eric', 'age' => 52]
  ]
]
复制

数组中的位置

PDO

$inArr = [1, 3, 5];
$clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id IN ($clause)");
$stmt->execute($inArr);
$resArr = $stmt->fetchAll();
if(!$resArr) exit('No rows');
var_export($resArr);
$stmt = null;
复制

MySQLi

$inArr = [12, 23, 44];
$clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks
$types = str_repeat('i', count($inArr)); //create 3 ints for bind_param
$stmt = $mysqli->prepare("SELECT id, name FROM myTable WHERE id IN ($clause)");
$stmt->bind_param($types, ...$inArr);
$stmt->execute();
$resArr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
if(!$resArr) exit('No rows');
var_export($resArr);
$stmt->close();
复制

与其他占位符在数组中的位置

PDO

$inArr = [1, 3, 5];
$clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks
$stmt = $pdo->prepare("SELECT * FROM myTable WHERE id IN ($clause) AND id < ?");
$fullArr = array_merge($inArr, [5]); //merge WHERE IN array with other value(s)
$stmt->execute($fullArr);
$resArr = $stmt->fetchAll();
if(!$resArr) exit('No rows');
var_export($resArr);
$stmt = null;
复制

MySQLi

$inArr = [12, 23, 44];
$clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks
$types = str_repeat('i', count($inArr)); //create 3 ints for bind_param
$types .= 'i'; //add 1 more int type
$fullArr = array_merge($inArr, [26]); //merge WHERE IN array with other value(s)
$stmt = $mysqli->prepare("SELECT id, name FROM myTable WHERE id IN ($clause) AND age > ?");
$stmt->bind_param($types, ...$fullArr); //4 placeholders to bind
$stmt->execute();
$resArr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
if(!$resArr) exit('No rows');
var_export($resArr);
$stmt->close();
复制

交易

PDO

try {
  $pdo->beginTransaction();
  $stmt1 = $pdo->prepare("INSERT INTO myTable (name, state) VALUES (?, ?)");
  $stmt2 = $pdo->prepare("UPDATE myTable SET age = ? WHERE id = ?");
  if(!$stmt1->execute(['Rick', 'NY'])) throw new Exception('Stmt 1 Failed');
  else if(!$stmt2->execute([27, 139])) throw new Exception('Stmt 2 Failed');
  $stmt1 = null;
  $stmt2 = null;
  $pdo->commit();
} catch(Exception $e) {
  $pdo->rollback();
  throw $e;
}
复制

您可能想知道为什么我只检查execute()PDO 的真实性。这是因为它可以返回 false,同时默默地失败。这里有更详细的解释

MySQLi

try {
  $mysqli->autocommit(FALSE); //turn on transactions
  $stmt1 = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)");
  $stmt2 = $mysqli->prepare("UPDATE myTable SET name = ? WHERE id = ?");
  $stmt1->bind_param("si", $_POST['name'], $_POST['age']);
  $stmt2->bind_param("si", $_POST['name'], $_SESSION['id']);
  $stmt1->execute();
  $stmt2->execute();
  $stmt1->close();
  $stmt2->close();
  $mysqli->autocommit(TRUE); //turn off transactions + commit queued queries
} catch(Exception $e) {
  $mysqli->rollback(); //remove all queries from queue if error (undo)
  throw $e;
}
复制

MySQLi 也有一个陷阱,但解决方案是使用全局处理程序将错误转换为异常。在这里阅读更多。

命名参数

这是仅 PDO 的功能,但仅在关闭仿真模式时才有用。否则,您只能使用同一个变量一次。还应该注意的是,前导冒号不是必需的,但这在任何地方都没有记录。无论如何,它最终可能会发生,但我想你永远不会知道。您也不能将?占位符与命名的占位符混合使用;它是一个或另一个。

$stmt = $pdo->prepare("UPDATE myTable SET name = :name WHERE id = :id");
$stmt->execute([':name' => 'David', ':id' => 3]);
$stmt = null;
复制

 类似资料: