首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP ODBC 連接 MSSQL 分頁類別:從傳統寫法到現代 PDO 最佳實踐

     編輯:PHP 綜合知識

前言

早期 PHP 專案常使用 ODBC 連接 Microsoft SQL Server,並自行撰寫分頁類別。隨著 PHP 版本演進,PDO (PHP Data Objects) 已成為資料庫存取的標準介面,提供更好的安全性、可攜性與效能。本文先解析傳統 ODBC 分頁類別的運作原理,再展示現代 PDO 重構版本,並比較兩者優劣。

傳統 ODBC 分頁類別分析

<?php
// 傳統寫法(教學用,不建議生產環境使用)
class LegacyPages {
    public $cn, $d, $result, $dsn, $user, $pass;
    public $total, $pages, $onepage, $page, $fre, $nxt, $i, $nums;

    public function getConnect($dsn, $user, $pass) {
        $this->cn = @odbc_connect($dsn, $user, $pass);
        if (!$this->cn) die("資料庫連線失敗");
    }

    public function getDo($sql) {
        $this->d = @odbc_do($this->cn, $sql);
        if (!$this->d) die("查詢錯誤");
        return $this->d;
    }

    public function getTotal($sql) {
        $dT = $this->getDo($sql);
        $this->total = odbc_result($dT, 'total');
        return $this->total;
    }

    public function getList($sql, $onepage, $page) {
        $this->onepage = $onepage;
        $this->page = $page ?: 1;
        $this->dList = $this->getDo($sql);
        $this->pages = ceil($this->total / $this->onepage);
        $this->fre = $this->page - 1;
        $this->nxt = $this->page + 1;
        $this->nums = ($this->page - 1) * $this->onepage;
        // 注意:ODBC 無 LIMIT,需在應用層跳過記錄
        return $this->dList;
    }

    public function getFanye() {
        $str = '';
        if ($this->page != 1) {
            $str .= "首頁 ";
            $str .= "fre}'>上頁 ";
        }
        if ($this->page < $this->pages) {
            $str .= "nxt}'>下頁 ";
            $str .= "pages}'>尾頁 ";
        }
        $str .= "共 {$this->pages} 頁,目前第 {$this->page} 頁";
        return $str;
    }
}

// 使用方式
$pg = new LegacyPages();
$pg->getConnect('mssql_dsn', 'sa', 'password');
$pg->getTotal("SELECT COUNT(*) AS total FROM students");
$pg->getList("SELECT name FROM students ORDER BY id", 10, $_GET['page'] ?? 1);

// 手動跳過記錄(ODBC 無 OFFSET)
for ($i = 0; $i < $pg->nums; $i++) odbc_fetch_row($pg->dList);

// 顯示當頁資料
$count = 0;
while (odbc_fetch_row($pg->dList) && $count < $pg->onepage) {
    echo odbc_result($pg->dList, 'name') . "
"; $count++; } echo $pg->getFanye(); ?>

傳統寫法的缺點

  • SQL 注入風險:直接拼接 SQL 字串,未使用參數綁定。
  • 效能低落:ODBC 無原生 LIMIT/OFFSET,需在 PHP 迴圈中 odbc_fetch_row() 跳過大量記錄(深分頁極慢)。
  • 資源洩漏:未明確釋放 odbc_free_result()、odbc_close()。
  • 錯誤處理簡陋:使用 @ 抑制錯誤配合 die(),難以整合日誌系統。
  • 緊耦合:HTML 輸出混雜在類別中,難以複用於 API/JSON 輸出。

現代 PDO 分頁類別(推薦)

<?php
/**
 * 現代化分頁類:支援 PDO、參數綁定、多種資料庫、分離關注點
 */
class Paginator
{
    private PDO $pdo;
    private string $baseUrl;
    private int $perPage;
    private int $currentPage;
    private int $totalItems = 0;
    private int $totalPages = 0;

    public function __construct(PDO $pdo, string $baseUrl = '', int $perPage = 15)
    {
        $this->pdo = $pdo;
        $this->baseUrl = $baseUrl ?: rtrim($_SERVER['PHP_SELF'], '/');
        $this->perPage = max(1, $perPage);
        $this->currentPage = max(1, (int)($_GET['page'] ?? 1));
    }

    /**
     * 設定查詢並取得分頁資料
     * @param string $sql 基礎查詢(不含 LIMIT/OFFSET),可含 ? 或 :name 佔位符
     * @param array $params 參數陣列
     * @param string $countSql 可選:自訂計數查詢(複雜 JOIN 時需指定)
     * @return array 當頁資料陣列
     */
    public function paginate(string $sql, array $params = [], ?string $countSql = null): array
    {
        // 1. 計算總筆數
        if ($countSql === null) {
            // 簡單移除 ORDER BY、SELECT 欄位,包裝 COUNT(*)
            $countSql = preg_replace('/^\s*SELECT\s+.*?\s+FROM\s+/is', 'SELECT COUNT(*) FROM ', $sql);
            $countSql = preg_replace('/\s+ORDER\s+BY\s+.*$/is', '', $countSql);
        }
        $stmt = $this->pdo->prepare($countSql);
        $stmt->execute($params);
        $this->totalItems = (int)$stmt->fetchColumn();
        $this->totalPages = (int)ceil($this->totalItems / $this->perPage);

        // 修正當前頁
        if ($this->currentPage > $this->totalPages) $this->currentPage = max(1, $this->totalPages);

        // 2. 取得當頁資料(支援 MySQL/PostgreSQL/SQLite LIMIT,SQL Server OFFSET/FETCH)
        $offset = ($this->currentPage - 1) * $this->perPage;
        $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);

        if ($driver === 'sqlsrv' || $driver === 'dblib' || $driver === 'mssql') {
            // SQL Server 2012+ OFFSET/FETCH
            $sql .= " ORDER BY (SELECT 0) OFFSET ? ROWS FETCH NEXT ? ROWS ONLY";
            $params[] = $offset;
            $params[] = $this->perPage;
        } else {
            // MySQL, PostgreSQL, SQLite
            $sql .= " LIMIT ? OFFSET ?";
            $params[] = $this->perPage;
            $params[] = $offset;
        }

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * 取得分頁導航 HTML(Bootstrap 5 風格)
     */
    public function links(array $extraParams = []): string
    {
        if ($this->totalPages <= 1) return '';

        $query = array_merge($_GET, $extraParams);
        unset($query['page']);

        $html = '';
        return $html;
    }

    /**
     * 取得分頁資訊陣列(供 API/JSON 使用)
     */
    public function toArray(): array
    {
        return [
            'current_page' => $this->currentPage,
            'per_page' => $this->perPage,
            'total_items' => $this->totalItems,
            'total_pages' => $this->totalPages,
            'has_more' => $this->currentPage < $this->totalPages,
        ];
    }

    private function url(array $query): string
    {
        return $this->baseUrl . '?' . http_build_query($query);
    }

    // Getter
    public function totalItems(): int { return $this->totalItems; }
    public function totalPages(): int { return $this->totalPages; }
    public function currentPage(): int { return $this->currentPage; }
    public function perPage(): int { return $this->perPage; }
}
?>

使用範例

1. 連接 MSSQL (sqlsrv 驅動)

<?php
$dsn = 'sqlsrv:Server=localhost;Database=mydb';
$pdo = new PDO($dsn, 'sa', 'password', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false, // 使用真實預備語句
]);
?>

2. 控制器邏輯

<?php
require 'Paginator.php';

$paginator = new Paginator($pdo, '/students.php', 10);

// 參數化查詢,防止 SQL 注入
$keyword = $_GET['q'] ?? '';
$params = [];
$sql = "SELECT id, name, email, created_at FROM students WHERE 1=1";
if ($keyword) {
    $sql .= " AND (name LIKE ? OR email LIKE ?)";
    $params[] = "%$keyword%";
    $params[] = "%$keyword%";
}
$sql .= " ORDER BY created_at DESC";

$students = $paginator->paginate($sql, $params);
$paginationHtml = $paginator->links(['q' => $keyword]);
$paginationData = $paginator->toArray(); // 給 AJAX 用
?>

3. 視圖輸出

<table class="table table-striped">
    <thead><tr><th>ID</th><th>姓名</th><th>Email</th><th>建立時間</th></tr></thead>
    <tbody>
    <?php foreach ($students as $s): ?>
        <tr>
            <td><?= $s['id'] ?></td>
            <td><?= htmlspecialchars($s['name']) ?></td>
            <td><?= htmlspecialchars($s['email']) ?></td>
            <td><?= $s['created_at'] ?></td>
        </tr>
    <?php endforeach; ?>
    </tbody>
</table>
<?= $paginationHtml ?>

效能優化建議

優化點說明
索引覆蓋確保 WHERE/ORDER BY 欄位有索引,避免全表掃描
避免 SELECT *只查詢需要的欄位,減少網路傳輸與記憶體
深分頁優化大資料集(>10萬筆)深分頁時,改用「游標分頁」:WHERE id > ? ORDER BY id LIMIT ?
快取總筆數總筆數變動不頻繁時,Redis 快取 COUNT(*) 結果
連線池生產環境使用 PHP-FPM + PDO 連線重用,或 Swoole/RoadRunner 長連線

結語

從 ODBC 手寫分頁邁向 PDO 參數化分頁,不僅是語法更新,更是安全性、可維護性、效能的全面升級。現代分頁類別應具備:驅動無關性(自動處理 LIMIT/OFFSET/FETCH 差異)、關注點分離(資料取得與 HTML 輸出分離)、防注入(全程參數綁定)、可測試性(依賴注入 PDO)。遺留專案建議逐步重構,新專務必採用 PDO + 參數綁定作為基準。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved