【PHP8.3】PDO将提供数据库特定的功能
PDO是一个通用的数据库驱动程序。
// MySQL
$pdo = new PDO('mysql:host=localhost;dbname=test');
// PostgreSQL
$pdo = new PDO('pgsql:host=localhost;dbname=test');
// SQLite
$pdo = new PDO('sqlite:/path/to/sqlite_db.db');
// 以後は同じように使える
$data = $pdo->query('SELECT * FROM table');
不管连接的数据库是什么类型,只需更改数据源名称(DSN),就可以完全相同地编写代码。这真是非常方便啊。
逆又可说,由于它是通用驱动程序,所以使用各个数据库的特定功能不太方便。这就需要特意使用特定模块,很麻烦。
因此,有人提出了一个RFC,计划在PDO中创建子类来解决这个问题。
// MySQL固有機能が使える
$pdo = new PdoMySql('mysql:host=localhost;dbname=test');
$pdo->getWarningCount();
// PostgreSQL固有機能が使える
$pdo = new PdoPgsql('pgsql:host=localhost;dbname=test');
$pdo->escapeIdentifier($name);
// SQLite固有機能が使える
$pdo = new PdoSqlite('sqlite:/path/to/sqlite_db.db');
$pdo->createCollation($collection);
可以继续使用new PDO(),在这种情况下,与以前一样没有任何变化。这仅仅是为了能够生成子类以使用特定于数据库的功能。
已经被接受并可以在PHP8.3及更高版本中使用。
以下是关于RFC(Request for Comments)和PDO驱动程序特定子类的介绍。
PHP 请求响应(RFC):PDO驱动器特定的子类
简介
PDO是一个通用的数据库类。
支持的数据库中可能还有一些具有特定功能的。
例如,如果连接到SQLite,就可以使用PDO::sqliteCreateFunction()方法。
然而,根据连接的数据库的不同,方法的可用性或不可用性是非常不自然的。
作为PDO的子类,为每个数据库准备专门的方法会使代码更易读。
建议
这个RFC包含两个建议。
在PDO中添加可以使用特定驱动程序功能的子类。
添加工厂方法PDO::connect()。
添加新的PDO子类
在所有PDO扩展模块中,将各自的子类添加到PHP中。例如,sqliteCreateFunction()不应该在PDO::sqliteCreateFunction()中,而应该在PdoSqlite::createFunction()中。
class PdoDblib extends PDO
{}
class PdoFirebird extends PDO
{}
class PdoMySql extends PDO
{
public function getWarningCount(): int {}
}
class PdoOci extends PDO
{}
class PdoOdbc extends PDO
{}
class PdoPgsql extends PDO
{
/**
* @var int
* @cname PDO_PGSQL_ATTR_DISABLE_PREPARES
*/
public const ATTR_DISABLE_PREPARES = UNKNOWN;
/**
* @var int
* @cname PGSQL_TRANSACTION_IDLE
*/
public const TRANSACTION_IDLE = UNKNOWN;
/**
* @var int
* @cname PGSQL_TRANSACTION_ACTIVE
*/
public const TRANSACTION_ACTIVE = UNKNOWN;
/**
* @var int
* @cname PGSQL_TRANSACTION_INTRANS
*/
public const TRANSACTION_INTRANS = UNKNOWN;
/**
* @var int
* @cname PGSQL_TRANSACTION_INERROR
*/
public const TRANSACTION_INERROR = UNKNOWN;
/**
* @var int
* @cname PGSQL_TRANSACTION_UNKNOWN
*/
public const TRANSACTION_UNKNOWN = UNKNOWN;
public function escapeIdentifier(string $input): string {}
public function copyFromArray(string $tableName, array $rows, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
public function copyFromFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
public function copyToArray(string $tableName, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): array|false {}
public function copyToFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = "\\\\N", ?string $fields = null): bool {}
public function lobCreate(): string|false {}
// Opens an existing large object stream. Must be called inside a transaction.
/** @return resource|false */
public function lobOpen(string $oid, string $mode = "rb"){}
public function lobUnlink(string $oid): bool {}
public function getNotify(int $fetchMode = PDO::FETCH_USE_DEFAULT, int $timeoutMilliseconds = 0): array|false {}
public function getPid(): int {}
}
class PdoSqlite extends PDO
{
/**
* @var int
* @cname SQLITE_DETERMINISTIC
*/
public const DETERMINISTIC = UNKNOWN;
/**
* @var int
* @cname SQLITE_ATTR_OPEN_FLAGS
*/
public const ATTR_OPEN_FLAGS = UNKNOWN;
/**
* @var int
* @cname SQLITE_OPEN_READONLY
*/
public const OPEN_READONLY = UNKNOWN;
/**
* @var int
* @cname SQLITE_OPEN_READWRITE
*/
public const OPEN_READWRITE = UNKNOWN;
/**
* @var int
* @cname SQLITE_OPEN_CREATE
*/
public const OPEN_CREATE = UNKNOWN;
/**
* @var int
* @cname SQLITE_ATTR_READONLY_STATEMENT
*/
public const ATTR_READONLY_STATEMENT = UNKNOWN;
/**
* @var int
* @cname
*/
public const ATTR_EXTENDED_RESULT_CODES = UNKNOWN;
// 集計関数をユーザ定義
public function createAggregate(
string $name,
callable $step,
callable $finalize,
int $numArgs = -1
): bool {}
// 照合関数をユーザ定義
public function createCollation(string $name, callable $callback): bool {}
public function createFunction(
string $function_name,
callable $callback,
int $num_args = -1,
int $flags = 0
): bool {}
// コンパイル方法によってSQLITE_OMIT_LOAD_EXTENSIONが定義されているか否かが変わる
#ifndef SQLITE_OMIT_LOAD_EXTENSION
public function loadExtension(string $name): bool {}
#endif
public function openBlob(
string $table,
string $column,
int $rowid,
?string $dbname = "main", //null,
int $flags = PdoSqlite::OPEN_READONLY
): mixed /* resource|false */ {}
}
在中国的母语中,可能也有DB固有的其他功能,但在本RFC中将不予考虑。
通过PDO静态工厂方法添加创建它们的方式
在PDO类中新增一个名为静态工厂方法PDO::connect()。
该方法会准确检查连接数据库类型的DSN,并在目标存在时返回相应的子类。
class PDO
{
// connectのPHP擬似コード
public static function connect(string $dsn [, string $username [, string $password [, array $options ]]]) {
// SQLiteに接続しようとしていたらPdoSqliteを返す
if (connecting to SQLite DB) {
return new PdoSqlite(...);
}
/* 中略 */
// いずれでもなければPDOを返す
return new PDO(...);
}
}
PDO::connect() 会返回合适的子类,以便连接到特定的数据库。
或者,您也可以直接生成子类。但是,如果连接的数据库不适用于该类,则会引发异常。
$db = new PdoSqlite($dsn, $username, $password, $options);
不兼容的向后变更 (Bù de
没有不具备后向兼容性的更改。
对于直接扩展PDO并添加数据库特定功能的人来说,可能有些不便。
建议的 PHP 版本(们)
PHP8.3可以进行更高效的编程和开发。
PHP功能未受影响。
除了PDO之外,PHP的其他功能不受影响。
经常被问到的问题
常见问题。
如果有人执行 ‘new PDO(…)’,他们会得到 ‘PdoPgsql’ 返回吗?
问:在写上 new PDO(…) 时,有可能返回 PdoPgsql 吗?
A: 没有。 .)
未来的范围
该项目是关于未来展望的,并且不包含在此RFC中。
何时在PDO上弃用旧函数。
未来应该删除现有的PDO::sqliteCreateFunction等驱动特定的方法,但优先级不高。
通过删除不同驱动程序可能存在或不存在的方法,可以整理PDO的代码。
虽然对用户来说没有太多好处,但可能能减轻维护的复杂性。
援引标识符
在讨论中,有人指出目前的PDO没有对标识符进行转义的功能。
至少在Postgres中有转义标识符的功能,所以向PDO类添加转义方法是值得的。
这个功能需要由对各种数据库驱动程序都熟悉的人来完成。
SQLite常量
SQLite3拡展中存在三个当前未定义的常量。
・SQLITE_DIRECTONLY – 仅SQLite直接访问
・SQLITE_INNOCUOUS – SQLite无风险
・SQLITE_SUBTYPE – SQLite子类型
这个RFC不在讨论范围内。
PdoSqlite 聚合、排序规则和函数
PdoSqlite的代码是从SQLite3扩展中复制过来的。
在SQLite中,存在一个表示数据字符编码的标志,但在SQLite3扩展中,SQLITE_UTF8被硬编码了。
我认为可以将SQLITE_UTF16、SQLITE_UTF16BE、SQLITE_UTF16LE、SQLITE_UTF16_ALIGNED以及SQLITE_UTF8等标志公开且可以进行指定,但本RFC不涉及此内容。
提议的投票选择
投票的时间为2023年7月3日至2023年7月17日,投票通过的赞成票必须达到三分之二方可生效。
此RFC被全体成员以23票赞成、0票反对的结果批准通过。
补丁和测试
https://github.com/php/php-src/pull/8707 的以下内容进行本地化翻译(仅提供一种选项):
参考资料
曾经存在过一个添加SQLite openBlob功能的RFC,但遭到了拒绝。
在那个时候,有一种感觉认为子类的方法更有可能被接受。
对此的想法或感触
为什么以前没有这个存在呢?这似乎非常方便。
由于我主要使用MySQL,所以并没有太多MySQL特有的功能,但对于希望使用Postgres或SQLite的特定功能的人来说,这将是个好消息。
此外,如果未来出现支持Cassandra或Redis等NoSQL的驱动程序,那可能会更加有趣。
就算数据库的移植很困难,但实际上数据库的移植并不经常发生,所以无论如何也无所谓。