wraugh/pdoqt

PDO查询工具

0.1.0 2018-04-17 02:28 UTC

This package is not auto-updated.

Last update: 2024-09-24 22:48:03 UTC


README

Pdoqt 是一个围绕 PDO 的小型包装器,使得运行简单的查询变得简单,并可能运行复杂的查询。它提供两种类型的方法:一些执行查询,一些帮助构建查询。它不会阻止您构建或运行无效的 SQL;相反,它的设计是为了鼓励在 PHP 中编写查询的一致和清晰风格。

Pdoqt 以文学风格编写。感谢docco,相同的文本和代码可以在源代码、README 或 html 中阅读。

namespace Pdoqt;

class Pdoqt {
    use \Defphp\Ex;

    private $conn;

    public function __construct (\PDO $connection) {
        $this->conn = $connection;
    }

    public function getConn (): \PDO {
        return $this->conn;
    }

首先,我们需要创建一个围绕查询方法的错误检查包装器。根据我的经验,通常期望数据库语句成功执行。与它们的错误真的是一个异常,适合 try/catch 处理。

在这个过程中,我们可以将参数化语句和无参数语句组合在同一个方法中。

    public function q (string $query, array $params = null): \PDOStatement {
        if (isset($params)) {
            $stmt = $this->conn->prepare($query);
            $stmt->execute($params);
        } else {
            $stmt = $this->conn->query($query);
        }

        if ($stmt === false) {
            $msg = "Error executing query $query";
            if (isset($params)) {
                $msg .= " w/ params " . json_encode($params);
            }
            $msg .= ": " . implode(" ", $this->conn->errorInfo());
            static::ex('DbQueryError', $msg, self::ERR_QUERY);
        }

        return $stmt;
    }

这样的简单包装器已经使得代码易于阅读,例如

try {
    $db->q("CREATE TABLE foo (id INT NOT NULL, name TEXT NOT NULL)");
    $db->q("INSERT INTO foo (id, name) VALUES (?, ?)", [1, "Foo One"]);
    $foo = $db->q("SELECT * FROM foo WHERE id = :id", [":id" => 1])->fetch();
    echo json_encode($foo) . "\n";
} catch (\Exception $e) {
    error_log($e);
    exit($e->getCode());
}

它还有一个非常通用的优点:您可以使用它来运行数据库服务器处理的任何语句。

尽管如此,我们通常想要运行选择查询。让我们编写一个只做这件事的方法

    public function fetchAll (
        string $query,
        array $params = null,
        string $indexBy = null
    ): array {
        $stmt = $this->q($query, $params);

        $rows = [];
        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            if (isset($indexBy)) {
                $this->indexRowByCol($rows, $row, $indexBy);
            } else {
                $rows[] = $row;
            }
        }

        return $rows;
    }

注意,fetchAllq接受相同的参数。这使得代码库更容易阅读(重复的模式更容易识别)。嗯,几乎是这样。fetchAll有一个可选的第三个参数,允许您通过一列索引结果集,例如

$q = "INSERT INTO foo (name) VALUES ('Herp', 'Derp') RETURNING name";
$rows = $db->fetchAll($q, null, "name");
$derp = $rows["Derp"];

这仅在索引列没有空或重复值时才有效;通常您会为此使用 id 列。

    private function indexRowByCol (array &$index, array $row, string $col) {
        if (!isset($row[$col])) {
            static::ex('DbIdx,DbIdxMissingCol',
                "No value at column '$col' for row " . json_encode($row),
                self::ERR_IDX_MISSING_COL);
        }
        if (isset($index[$row[$col]])) {
            static::ex('DbIdx,DbIdxDuplicateCol',
                "column $col isn't unique; found duplicate value " .
                    json_encode($row[$col]) . " in row " . json_encode($row),
                self::ERR_IDX_DUPLICATE_COL);
        }
        $index[$row[$col]] = $row;
    }

为每一行查找索引是不高效的,但同样,将整个结果集提取到数组中也是不高效的。fetchAll仅适用于小型结果集。当预期大量数据时,可以直接调用 POD 的fetch来逐行处理,例如

 $stmt = $db->q("SELECT * FROM foobar");
 while ($row = $stmt->fetch()) {
     // do something with $row
 }

当您期望获取单行(例如,通过 id 查询)时,您可以使用fetchOne方法,这仅仅是fetchAll加上合理性检查

    public function fetchOne (string $query, array $params = null): array {
        $rows = $this->fetchAll($query, $params);
        if (count($rows) != 1) {
            $msg = "Expected exactly one row but got " . count($rows) .
                " from query $query";
            if (isset($params)) {
                $msg .= " w/ params " . json_encode($params);
            }
            if (count($rows)) {
                static::ex('DbUnexpectedRows,DbTooManyRows',
                    $msg,
                    self::ERR_TOO_MANY_ROWS);
            } else {
                static::ex('DbUnexpectedRows,DbNoRows,NotFound',
                    $msg,
                    self::ERR_NO_ROWS);
            }
        }

        return reset($rows);
    }

对于插入、更新和删除查询,获取受影响行数通常很有用。

    public function nAffected (string $query, array $params = null): int {
        return $this->q($query, $params)->rowCount();
    }

调用类似以下内容

$n = $db->nAffected("DELETE FROM foo WHERE id = ?", [1]);

与例如

$n = $db->q("DELETE FROM foo WHERE id = ?", [1])->rowCount();

相比,节省的打字不多,但可能更容易阅读。它遵循与fetchAllfetchOne相同的模式。

围绕查询执行这些包装器并不能帮助构建查询,但它们确实鼓励与数据库交互的一致风格。因为我们希望直接编写 SQL,所以无法避免连接字符串。我们能做的就是提供方便的工具来做这件事,并依赖于编码风格/编写模式来保持事情整洁和清晰。

这样的工具之一是用于转义列名的辅助工具。这始终需要这样做;我们最好有统一的方式来做。

    /**
     * Escape column names for use in queries.
     * For example:
     *
     *      $cols = ['a', 'b', 'c'];
     *      $q = "INSERT INTO t (" . $this->cs($cols) . ") VALUES ...";
     *
     * If an element's key is not numeric, its key is used as the column name.
     * That is, the following gives the same result as the above example:
     *
     *      $cols = ['a', 'b' => 'foo', 'c'];
     *      $q = "INSERT INTO t (" . $this->cs($cols) . ") VALUES ...";
     */
    public function cs (array $cols): string {
        $cs = [];
        foreach ($cols as $k => $v) {
            $col = is_numeric($k) ? $v : $k;
            $cs[] = '"' . strtr($col, ['"' => '""']) . '"';
        }

        return implode(', ', $cs);
    }

下一个工具是帮助我们编写 INSERT 语句。通常,当您开始将记录插入数据库时,您会以键值数组的形式拥有它们。此方法将这些数组转换为 VALUES 文本

    /**
     * Generate parameters and their placeholders for use in VALUES commands.
     * For example,
     *
     *      $foobars = [
     *          ['foo' => 1, 'bar' => 2],
     *          ['foo' => 1, 'bar' => 3],
     *      ];
     *      $cols = ['foo', 'bar'];
     *      list ($vals, $params) = $this->vals($foobars, $cols);
     *      $q = "INSERT INTO foobars (" . $this->cs($cols) . ") VALUES " . $vals .
     *          " RETURNING id";
     *      $this->fetchAll($q, $params);
     *
     * Constant values can be set by using a key => value syntax for $cols.
     * That is, the following gives the same result as the above example:
     *
     *      $foobars = [
     *          ['bar' => 2],
     *          ['bar' => 3],
     *      ];
     *      $cols = ['foo' => 1, 'bar'];
     *      list ($vals, $params) = $this->vals($foobars, $cols);
     *      $q = "INSERT INTO foobars (" . $this->cs($cols) . ") VALUES " . $vals .
     *          " RETURNING id";
     *      $this->fetchAll($q, $params);
     */
    public function vals (array $rows, array $cols): array {
        $vals = [];
        $params = [];

        foreach ($rows as $row) {
            $vs = [];
            foreach ($cols as $k => $c) {
                if (is_numeric($k)) {
                    $vs[] = '?';
                    $params[] = isset($row[$c]) ? $row[$c] : null;
                } else {
                    $vs[] = $c;
                }
            }
            $vals[] = '(' . implode(', ', $vs) . ')';
        }

        return [implode(",\n", $vals), $params];
    }

我们现在需要的就这些。您可以根据遇到的重复模式添加更多工具以及风格示例。

    const ERR_QUERY             = 1969001;
    const ERR_IDX_MISSING_COL   = 1969002;
    const ERR_IDX_DUPLICATE_COL = 1969003;
    const ERR_TOO_MANY_ROWS     = 1969004;
    const ERR_NO_ROWS           = 1969005;
}

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */