【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的驱动程序,那可能会更加有趣。

就算数据库的移植很困难,但实际上数据库的移植并不经常发生,所以无论如何也无所谓。

广告
将在 10 秒后关闭
bannerAds