PostgreSQL電子商務
本文通過一個簡單的web 應用,示範了 PHP 和 PostgresSQL 在電子商務中的應用。
不久以前,如果要架構一個嚴肅的Web應用的話,意味著購買價格不菲的Cold Fusion 許可,以及一個商業的資料庫服務程式像Sybase 加上Sun 伺服器。幸運的是,這樣的日子一去不複返了。隨著日漸成熟的免費資料庫市場以及Apache 使用者的大量增長,一些替代產品已經具有相當,甚至超過了這些專有軟體的能力。
比較好的開放源碼軟體的一種是 PHP,一個很像Perl 的指令碼語言,以及PostgreSQL,一個很強大的物件導向的資料庫。如果把兩者結合起來的話,你可以設計從簡單的留言簿到一個巨大的基於Web 的財務軟體。PHP 提供大腦而Postgres 提供發達的肌肉。
下面介紹一個很基本的 PHP 購物車和庫存應用,充分利用 Postgres 的事務功能。源碼推淥柿峽梢源?PHPBuilder.com 下載。
首先要提到的是應用程式的結構,在我的PHP Web 應用程式中,我總是首先設定一個綜合庫,網站的每一個頁面都會用到它,取名叫common.php 存放在include 目錄。
這個庫會處理日常任務,例如資料庫連接,使用者鑒別,網站的頭部/尾部檔案等。把這些函數放在一個地方,我們的應用看上去很乾淨,容易維護。
表一:示範的庫代碼
common.php:
//串連 postgres 資料庫
$conn=pg_pconnect("user=tim dbname=db_example");
//看串連是否成功
if (!$conn) {
//如果失敗則報告出錯
echo pg_errormessage($conn);
exit;
}
//網站的標頭檔
function site_header ($title) {
return '
'.$title.'
';
}
// 頁面結尾的 HTML 程式碼
function site_footer () {
return '';
}
//一個簡單的查詢執行函數,用來減少代碼
function query($sql) {
global $conn;
return pg_exec($conn,$sql);
}
//讓每一個頁面自動啟動session或者儲存 session 狀態
session_start();
?>
因此,我們的第一個版本的庫已經可以用了,它串連資料庫,提供了簡單的 HTML
代碼。
我們網站上每一個頁面都包括:
require ($DOCUMENT_ROOT.'/include/common.php');
echo site_header('示範頁面');
/*
頁面邏輯處理
*/
echo site_footer();
?>
一般說來,在構建應用程式時,把邏輯和實際的表示(在我們這裡就是HTML)分開是很明智的。因此,我把邏輯放到函數裡面。但是PHP 使用函數調用的方法,缺點是沒有標準的出錯處理過程,如果函數內部有錯的話,呼叫函數的程式不能把把錯誤資訊傳遞給使用者。在其他的語言,例如Java 裡面,你可以使用try/catch語句來處理。
我的解決辦法是,每個函數總是返回 true 或者 false ,設定一個$feedback全域變數,這樣的話,結果就可以測試。現在有一個叫做PEAR (http://pear.php.net/) 的項目在做標準化錯誤處理以及資料庫存取的努力,
但是到目前為止,還不能穩定運行。
下面是一個使用我的 true/false 方法調用函數的例子:
$result=function_call_name();
if (!$result) {
//顯示錯誤
echo $feedback;
} else {
//沒有錯誤,繼續
}
?>
好了,現在讓我們開始想想購物車吧! 我們需要一些基本的資料結構儲存購物車的資料。例如,我們需要一個庫存資料庫列出物品名字,組件號碼,價格以及數量,同時,我們
還需要記錄顧客購買的物品......太複雜了,就寫這些吧。
表二、購物車資料結構
Cart.sql:
# 建立一個順序表用來產生顧客號碼。
# 每個id 之間用隨意的一個數字分開,以防別人猜測購物車號碼。
create sequence seq_customer_id increment 26 start 1;
create table customers (
customer_id int not null default 0 primary key,
name text,
address text,
credit_card text,
total_order MONEY DEFAULT '{CONTENT}.00'
);
create table cart_items (
cart_item serial,
customer_id int,
part_number int,
quantity int
);
create index idx_cart_customer on cart_items(customer_id);
create table item_inventory (
part_number serial,
name text,
price float,
inventory int
);
這個結構給我們一個基本的購物車,為了規範資料庫模式,我建立一個獨立的表,用於列出顧客的購物車裡的內容。這樣,讓顧客的購物車可以有多項物品,並且可以很容易
地和庫存資料庫連接。
現在我們需要考慮桓鱸諳呱痰甑母髦止δ芰恕R桓鱟罨鏡墓δ芫褪僑〉靡徊抗何鋶擔道鍰砑遊鍥罰緩蠼脊恕5比灰桓鍪導什僮韉腦諳呱痰輳剮枰芏喙δ埽皸?物品,調整數量等。這些就等你自己來完成了。
我從一個簡單的產生一個顧客的功能開始,所有這些其實就是在排隊的顧客中取得下一個顧客的資料,插入顧客表,把顧客號碼在PHP4 內建的session 管理中註冊。
表三、建立一個新顧客
function cart_new() {
global $conn, $customer_id, $feedback;
// 啟動一個事務
query("BEGIN WORK");
//查詢下一個顧客號碼
$res=query("SELECT nextval('seq_customer_id')");
//檢查錯誤
if (!$res || pg_numrows($res)<1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - Database didn't return next value ';
query("ROLLBACK");
return false;
} else {
$customer_id=pg_result($res,0,0);
// 登記到 session
session_register('customer_id');
// 插入新顧客
$res=query("INSERT INTO customers (customer_id)
VALUES ('$customer_id')");
//檢查錯誤
if (!$res || pg_cmdtuples($res)<1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - couldn't insert new customer row ';
query("ROLLBACK");
return false;
} else {
//commit this transaction
query("COMMIT");
return true;
}
}
}
?>
這段代碼比較長,雖然我不是很喜歡,但是它示範了怎樣正確開始和結束Postgres 的事務以及怎樣檢查查詢語句的錯誤。我要在所有的代碼用到同樣的錯誤監測程式,我想,你也應該如此。
需要計劃好如果查詢出錯的處理辦法,你是直接終止程式呢?還是重新執行查詢語句,抑或繼續執行,就當什麼也沒有發生?仔細考慮每種選擇的結果。例如,如果不能得到下一個顧客的customer_id ,那麼,建立新顧客的記錄也就泡湯,接下來就是不能更新她的地址,不能往購物車裡添加物品,對吧?
現在,我們看看添加物品的過程,這個步驟相對比較容易,在添加物品之前,要先檢查物品是否在資料庫中。這樣比較安全,因為物品號碼來自瀏覽器,可能被篡改。一旦知道物品存在,我們就能測試它是否已經在購物車裡,如果已經放入,那麼數量加一,而不是另外插入一行,否則,插入一條數量為一的記錄到購物車。
表四、添加物品到購物車
function cart_add_item($item_id,$quantity=1) {
global $customer_id, $feedback, $conn;
$res=query("SELECT * FROM item_inventory WHERE part_number='$item_id'");
if (!$res || pg_numrows($res)<1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error-item not found ';
return false;
} else {
// 檢查物品是否放入購物車,如果是,增加數量
// 開始事務
query("BEGIN WORK");
$res=query("SELECT * FROM cart_items ".
"WHERE part_number='$item_id' AND customer_id='$customer_id' FOR UPDATE");
if (!$res || pg_numrows($res)<1) {
//如果沒有該物品,新插入一條
$res=query("INSERT INTO cart_items ".
"(customer_id,part_number,quantity)".
"VALUES ($customer_id,$item_id,$quantity)");
if (!$res || pg_cmdtuples($res) < 1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error-couldn't insert into cart ';
//儘管沒有東西被改變,但是最好還是復原事務
query("ROLLBACK");
return false;
} else {
query("COMMIT");
return true;
}
} else {
//購物車中已經存在該物品
$res=query("UPDATE cart_items SET quantity = quantity + $quantity ".
"WHERE part_number='$item_id' AND
customer_id='$customer_id'");
if (!$res || pg_cmdtuples($res) < 1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error-couldn't increment quantity in cart ';
query("ROLLBACK");
return false;
} else {
// 提交改變,正式更新資料庫。
query("COMMIT");
return true;
}
}
}
}
?>
現在我們能建立新顧客,並且他們添加物品了。我們現在需要結賬,並減掉庫存。這一部分是最複雜的,充分利用了Postgres 的事務功能和先進鎖機制。
我們用Postgres 的 SELECT...FOR UPDATE 文法作為開始,這個語句能有效地對當前選擇的行加鎖使你能在一個事務裡更新並提交改變。
通過在一個事務裡使用這個語句,你可以保證資料的一致性。在其他的一些資料庫,例如MySQL ,就不能鎖定指定的資料行,而得到不正確的資料以及沒用的庫存統計。
這個語句也能利用子查詢,另外一個資料庫的標準特性。子查詢可以讓你很省事地把兩個查詢結合在一起,
鎖定行以後,我們需要按照購物車的物品減少對應的庫存量。為簡便起見,我們對庫存不夠不報告錯誤,並把庫存變為負數。你可以自己寫一個管理頁面,查看負數庫存的物品,並去訂購。
最後,我們更新顧客表中的信用卡,購買資訊,合計購買金額,撤掉這個顧客的session。
表五、結賬,減庫存
function cart_checkout($credit_card,$address,$name) {
global $conn, $customer_id, $feedback;
// 事務開始
query("BEGIN WORK");
// 鎖住庫存表的對應行,用一個簡單的子查詢來處理。
$sql="SELECT * FROM item_inventory ".
"WHERE part_number ".
"IN (SELECT part_number FROM cart_items ".
"WHERE customer_id='$customer_id') ".
"FOR UPDATE";
$res=query($sql);
if (!$res || pg_numrows($res)<1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - no items locked ';
query("END WORK");
return false;
} else {
// 庫存的某幾行已被鎖定,從購物車取得物品以及數量。
$sql="SELECT part_number,quantity ".
"FROM cart_items ".
"WHERE
customer_id='$customer_id' ".
"ORDER BY part_number DESC";
$res2=query($sql);
if (!$res2 || pg_numrows($res2)<1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - no items in cart ';
query("END WORK");
return false;
} else {
$rows=pg_numrows($res2);
// 更新庫存餘額
for ($i=0; $i < $rows; $i++) {
// 讀取購物車資料
$quantity=pg_result($res2,$i,'quantity');
$item_id=pg_result($res2,$i,'part_number');
$res3=query("UPDATE item_inventory".
"SET inventory =inventory-$quantity ".
"WHERE part_number='$item_id'");
if (!$res3 || pg_cmdtuples($res3) < 1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - updating inventory failed ';
query("ROLLBACK");
return false;
}
}
// 庫存更新結束,得到這個訂單的合計金額並更新顧客記錄
$res=query("SELECT sum(cart_items.quantity*item_inventory.price) ".
"FROM cart_items,item_inventory ".
"WHERE cart_items.customer_id='$customer_id' ".
"AND cart_items.part_number=item_inventory.part_number");
if (!$res || pg_numrows($res) < 1) {
//couldn't get order total
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - couldn't get order total ';
query("ROLLBACK");
return false;
} else {
// 更新顧客表
$total=pg_result($res,0,0);
$res=query("UPDATE customers ".
"SET address='$address',name='$name',".
"total_order='$total',credit_card='$credit_card'".
"WHERE customer_id='$customer_id'");
if (!$res || pg_cmdtuples($res) < 1) {
$feedback .= pg_errormessage($conn);
$feedback .= ' Error - updating customer information ';
query("ROLLBACK");
return false;
} else {
// 改變正式生效
query("COMMIT");
// 刪除 session
$customer_id=0;
session_destroy();
return true;
}
}
}
}
}
?>
理論上,這是一個很複雜的交易,每一步必須正確執行,否則,整個事務必須復原到正確的順序。
如果你沒有使用本例的交易處理,那麼萬一在更新過程中失敗的話,麻煩就大了。可能你只是更新了庫存的一部分,如果訪問者重新整理頁面的話,你怎麼知道那個庫存需要減少呢?
本文不想提供一個購物車的綜合解決方案(如果這樣的話,只要我有時間,完全可以寫一本書了),但是,本文示範了最基本的設計和啟動並執行方法,建議每一位Web 開發人員使用。更深入的討論可以訪問 PHPBuilder.com.
文中所有的代碼可以從http://www.phpbuilder.com/columns/linuxjournal200009.php3 下載。
譯者註:
作者 Tim Perdue (tim@perdue.net) 是 SourceForge.net 的建設者,以及 PHPBuilder.com 和
Geocrawler.com 的創立者。