schema-database - 查詢結果與 PDO~~FETCH_CLASS
我上週整理我幾年前開發的一個具備簡單 Active record 與 ORM 功能的 PHP 資料庫函數庫 schema-database 。我想要為查詢函數加上一個參數,讓編程人員選擇查詢結果的型態為陣列或個體。因為 schema-database 的底層是 PDO ,所以要調整 PDO 汲取方法(fetch)的選項。
在改寫動作中,我注意到 PDO 有一個選項叫 PDO::FETCH_CLASS 。它與 PDO::FETCH_OBJ 的差別在於 PDO::FETCH_OBJ 汲取出的資料型態是標準類別的個體(stdClass),也就是單純的屬性集合體。而 PDO::FETCH_CLASS 汲取出的資料型態則是指定的類別的個體;這個類別可由使用者自己定義。 PDO::FETCH_CLASS 的用途,很明顯可以搭配 ORM 類,讓 PDO 直接幫使用者配置 ORM 實體。
由於我的 schema-database 本身就有提供一個通用的 ORM 類別,叫 Database_Row。自然我就順手加上了這個選項,讓使用者可以取出具有 ORM 能力的資料記錄。但是 PDO::FETCH_CLASS 有一個在文件中沒有提到的奇特行為,讓我一開始寫的程式發生錯誤,困擾了我一個下午的時間。
在我說明這個奇特行為之前,先讓我們試著思考一下同樣的事,我們應該怎麼做?
首先,我們會從資料庫中讀出指定的記錄。接著我們會配置對應的 ORM 個體。最後將記錄的資料內容,填入 ORM 個體的屬性。或者直接把資料內容作為配置 ORM 個體時的建構參數。總之,一個通常的過程是: 一、調用建構方法配置個體,二、填入資料欄位(個體屬性)。 但是 PDO::FETCH_CLASS 奇特之處就在於它的過程是相反的。它是先填入個體屬性,再調用建構方法。很奇特吧。 在 C++/Java 等語言中,根本沒有先填屬性再調建構方法這種事,因為在你調用建構方法之前根本沒東西存在。但是 PHP 並不禁止這種行為。
下列內容就是用 PHP 語言表示上述所說的過程。假設 Book 是一個 ORM 類別。第一段是一般預期的配置過程。第二段則是 PDO::FETCH_CLASS 的配置過程。
<?php
class Book {
function __construct() {
$this->isbn = $this->title = $this->author = '';
}
}
$input_data = array(
'isbn' => '1234567890',
'title' => 'Title 1',
'author'=> 'rock'
);
// 1. expected way:
$b1 = new Book;
foreach ($input_data as $field => $v) {
$b1->$field = $v;
}
// 2. PDO_FETCH_CLASS's way:
foreach ($input_data as $field => $v) {
$b2->$field = $v;
}
$b2 = new Book;
echo "B1 title: {$b1->title}\n";
echo "B2 title: {$b2->title}\n"; // empty...
?>
眼尖的人,應該不用執行就可以看出 PDO::FETCH_CLASS 的奇特行為會帶來什麼困擾。 通常,建構方法會將個體的所有屬性指派為預設值或是空值。這表示 PDO::FETCH_CLASS 調用建構方法之後,它原先設定的欄位內容就全部重置了。於是接下來我們取出的值就不是我們預期的內容。
這個奇特行為曾經被舉報為 bug 與修正。但後來因為某些理由(但不知道是什麼),又被 PHP 維護團隊還原回去了,但另外增加一個 PDO::FETCH_PROPS_LATE 的調整選項。相關訊息可參考「Bug #49521 - PDO fetchObject sets values before calling constructor」。 PDO::FETCH_PROPS_LATE 選項可以延後設定屬性的行為,使 PDO::FETCH_CLASS 的配置過程改為先調建構方法再設屬性。
延用前一節的 PHP 程式碼,修改為實際 PDO::FETCH_CLASS 與 PDO::FETCH_PROPS_LATE 的用法。
<?php
class Book {
function __construct() {
$this->isbn = $this->title = $this->author = '';
}
function dump() {
echo "ISBN: {$this->isbn}\n";
echo "Title: {$this->title}\n";
echo "Author: {$this->author}\n";
}
}
$dsn = 'sqlite::memory:';
$db = new PDO($dsn);
$db->query("CREATE TABLE books (
isbn varchar(13) primary key,
title varchar(1024) not null,
author varchar(1024) not null
);");
$db->query("INSERT INTO books VALUES ('1234567890', 'Title1 ', 'rock');");
$stat = $db->query("SELECT * FROM books;");
//$stat->setFetchMode(PDO::FETCH_CLASS, 'Book');
// Unexcepted result. The constructor will reset all properties.
$stat->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'Book');
while ($book = $stat->fetch()) {
echo 'Class of book is ', get_class($book), "\n";
$book->dump();
}
?>
我一開始說這是我在修改 schema-database 函數庫時碰到的問題。那麼用我的 schema-database 配上 PDO::FETCH_CLASS 會如何? 因為我的 schema-database 的 Database_Row 的建構方法總是會初始資料欄位,若是按照 PDO 原先的奇特行為,那麼所有的資料欄位都會被重置,就沒用了。必須搭配 PDO::FETCH_PROPS_LATE 才會得到正確結果。所以我將 schema-database 設計為看到 PDO::FETCH_CLASS 的選項時,就會自己搭配 PDO::FETCH_PROPS_LATE,且將記錄配置為 Database_Row 個體。
前一節的 pdo_fetch_props_late_example.php ,改用 schema-database 的 Database_Query 實作的話,程式碼將會是下列內容:
<?php
require_once 'database_query.php';
$dsn = 'sqlite::memory:';
$db = new PDO($dsn);
$db->query("CREATE TABLE books (
isbn varchar(13) primary key,
title varchar(1024) not null,
author varchar(1024) not null
);");
$db->query("INSERT INTO books VALUES ('1234567890', 'Title1 ', 'rock');");
$query = new Database_Query(array('db' => $db));
$books = $query->from('books')->select(PDO::FETCH_CLASS);
// schema-database 看到 PDO::FETCH_CLASS 的選項時,總是搭配 PDO::FETCH_PROPS_LATE,
// 且將記錄配置為 Database_Row 個體。
foreach ($books as $book) {
echo 'Class of book is ', get_class($book), "\n"; // Database_Row
echo "ISBN: {$book->isbn}; Title: {$book->title}\n";
}
?>
我在「PHP - Schema-Database」的介紹文章最後,舉了一個 PowerQuery 例子。當時在該範例中,我要自己取出記錄後轉成 Database_Row 個體。現在不必這麼做了。 但實際操作時的效能議題依舊。如果你只需要從查詢結果中取值,我仍建議你取為標準個體(PDO::FETCH_OBJ)或陣列(PDO::FETCH_ASSOC)。
樂多舊回應