揭露 PHP 應用程序中出現的五個常見數據庫問題 —— 包括數據庫模式設計數據庫訪問和使用數據庫的業務邏輯代碼 —— 以及它們的解決方案
如果只有一種 方式使用數據庫是正確的……
您可以用很多的方式創建數據庫設計數據庫訪問和基於數據庫的 PHP 業務邏輯代碼但最終一般以錯誤告終本文說明了數據庫設計和訪問數據庫的 PHP 代碼中出現的五個常見問題以及在遇到這些問題時如何修復它們
問題 直接使用 MySQL
一個常見問題是較老的 PHP 代碼直接使用 mysql_
函數來訪問數據庫清單 展示了如何直接訪問數據庫
清單 Access/getphp
<?php
function get_user_id( $name )
{
$db = mysql_connect( localhost root password );
mysql_select_db( users );
$res = mysql_query( SELECT id FROM users WHERE login=$name );
while( $row = mysql_fetch_array( $res ) ) { $id = $row[]; }
return $id;
}
var_dump( get_user_id( jack ) );
?>
注意使用了 mysql_connect
函數來訪問數據庫還要注意查詢其中使用字符串連接來向查詢添加 $name
參數
該技術有兩個很好的替代方案PEAR DB 模塊和 PHP Data Objects (PDO) 類兩者都從特定數據庫選擇提供抽象因此您的代碼無需太多調整就可以在 IBM® DB®MySQLPostgreSQL 或者您想要連接到的任何其他數據庫上運行
使用 PEAR DB 模塊和 PDO 抽象層的另一個價值在於您可以在 SQL 語句中使用 ?
操作符這樣做可使 SQL 更加易於維護且可使您的應用程序免受 SQL 注入攻擊
使用 PEAR DB 的替代代碼如下所示
清單 Access/get_goodphp
<?php
require_once(DBphp);
function get_user_id( $name )
{
$dsn = mysql://root:password@localhost/users;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$res = $db>query( SELECT id FROM users WHERE login=?
array( $name ) );
$id = null;
while( $res>fetchInto( $row ) ) { $id = $row[]; }
return $id;
}
var_dump( get_user_id( jack ) );
?>
注意所有直接用到 MySQL 的地方都消除了只有 $dsn
中的數據庫連接字符串除外此外我們通過 ?
操作符在 SQL 中使用 $name
變量然後查詢的數據通過 query()
方法末尾的 array
被發送進來
問題 不使用自動增量功能
與大多數現代數據庫一樣MySQL 能夠在每記錄的基礎上創建自動增量惟一標識符除此之外我們仍然會看到這樣的代碼即首先運行一個 SELECT
語句來找到最大的 id
然後將該 id
增 並找到一個新記錄清單 展示了一個示例壞模式
清單 Badidsql
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id MEDIUMINT
login TEXT
password TEXT
);
INSERT INTO users VALUES ( jack pass );
INSERT INTO users VALUES ( joan pass );
INSERT INTO users VALUES ( jane pass );
這裡的 id
字段被簡單地指定為整數所以盡管它應該是惟一的我們還是可以添加任何值如 CREATE
語句後面的幾個 INSERT
語句中所示清單 展示了將用戶添加到這種類型的模式的 PHP 代碼
清單 Add_userphp
<?php
require_once(DBphp);
function add_user( $name $pass )
{
$rows = array();
$dsn = mysql://root:password@localhost/bad_badid;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$res = $db>query( SELECT max(id) FROM users );
$id = null;
while( $res>fetchInto( $row ) ) { $id = $row[]; }
$id += ;
$sth = $db>prepare( INSERT INTO users VALUES(???) );
$db>execute( $sth array( $id $name $pass ) );
return $id;
}
$id = add_user( jerry pass );
var_dump( $id );
?>
add_userphp
中的代碼首先執行一個查詢以找到 id
的最大值然後文件以 id
值加 運行一個 INSERT
語句該代碼在負載很重的服務器上會在競態條件中失敗另外它也效率低下
那麼替代方案是什麼呢?使用 MySQL 中的自動增量特性來自動地為每個插入創建惟一的 ID更新後的模式如下所示
清單 Goodidphp
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id MEDIUMINT NOT NULL AUTO_INCREMENT
login TEXT NOT NULL
password TEXT NOT NULL
PRIMARY KEY( id )
);
INSERT INTO users VALUES ( null jack pass );
INSERT INTO users VALUES ( null joan pass );
INSERT INTO users VALUES ( null jane pass );
我們添加了 NOT NULL
標志來指示字段必須不能為空我們還添加了 AUTO_INCREMENT
標志來指示字段是自動增量的添加 PRIMARY KEY
標志來指示那個字段是一個 id
這些更改加快了速度清單 展示了更新後的 PHP 代碼即將用戶插入表中
清單 Add_user_goodphp
<?php
require_once(DBphp);
function add_user( $name $pass )
{
$dsn = mysql://root:password@localhost/good_genid;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$sth = $db>prepare( INSERT INTO users VALUES(null??) );
$db>execute( $sth array( $name $pass ) );
$res = $db>query( SELECT last_insert_id() );
$id = null;
while( $res>fetchInto( $row ) ) { $id = $row[]; }
return $id;
}
$id = add_user( jerry pass );
var_dump( $id );
?>
現在我不是獲得最大的 id
值而是直接使用 INSERT
語句來插入數據然後使用 SELECT
語句來檢索最後插入的記錄的 id
該代碼比最初的版本及其相關模式要簡單得多且效率更高
問題 使用多個數據庫
偶爾我們會看到一個應用程序中每個表都在一個單獨的數據庫中在非常大的數據庫中這樣做是合理的但是對於一般的應用程序則不需要這種級別的分割此外不能跨數據庫執行關系查詢這會影響使用關系數據庫的整體思想更不用說跨多個數據庫管理表會更困難了
那麼多個數據庫應該是什麼樣的呢?首先您需要一些數據清單 展示了分成 個文件的這樣的數據
清單 數據庫文件
Filessql:
CREATE TABLE files (
id MEDIUMINT
user_id MEDIUMINT
name TEXT
path TEXT
);
Load_filessql:
INSERT INTO files VALUES ( testjpg files/testjpg );
INSERT INTO files VALUES ( testjpg files/testjpg );
Userssql:
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id MEDIUMINT
login TEXT
password TEXT
);
Load_userssql:
INSERT INTO users VALUES ( jack pass );
INSERT INTO users VALUES ( jon pass );
在這些文件的多數據庫版本中您應該將 SQL 語句加載到一個數據庫中然後將 users
SQL 語句加載到另一個數據庫中用於在數據庫中查詢與某個特定用戶相關聯的文件的 PHP 代碼如下所示
清單 Getfilesphp
<?php
require_once(DBphp);
function get_user( $name )
{
$dsn = mysql://root:password@localhost/bad_multi;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$res = $db>query( SELECT id FROM users WHERE login=?
array( $name ) );
$uid = null;
while( $res>fetchInto( $row ) ) { $uid = $row[]; }
return $uid;
}
function get_files( $name )
{
$uid = get_user( $name );
$rows = array();
$dsn = mysql://root:password@localhost/bad_multi;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$res = $db>query( SELECT * FROM files WHERE user_id=?
array( $uid ) );
while( $res>fetchInto( $row ) ) { $rows[] = $row; }
return $rows;
}
$files = get_files( jack );
var_dump( $files );
?>
get_user
函數連接到包含用戶表的數據庫並檢索給定用戶的 IDget_files
函數連接到文件表並檢索與給定用戶相關聯的文件行
做所有這些事情的一個更好辦法是將數據加載到一個數據庫中然後執行查詢比如下面的查詢
清單 Getfiles_goodphp
<?php
require_once(DBphp);
function get_files( $name )
{
$rows = array();
$dsn = mysql://root:password@localhost/good_multi;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$res = $db>query(
SELECT files* FROM users files WHERE
userslogin=? AND usersid=filesuser_id
array( $name ) );
while( $res>fetchInto( $row ) ) { $rows[] = $row; }
return $rows;
}
$files = get_files( jack );
var_dump( $files );
?>
該代碼不僅更短而且也更容易理解和高效我們不是執行兩個查詢而是執行一個查詢
盡管該問題聽起來有些牽強但是在實踐中我們通常總結出所有的表應該在同一個數據庫中除非有非常迫不得已的理由
問題 不使用關系
關系數據庫不同於編程語言它們不具有數組類型相反它們使用表之間的關系來創建對象之間的一到多結構這與數組具有相同的效果我在應用程序中看到的一個問題是工程師試圖將數據庫當作編程語言來使用即通過使用具有逗號分隔的標識符的文本字符串來創建數組請看下面的模式
清單 Badsql
DROP TABLE IF EXISTS files;
CREATE TABLE files (
id MEDIUMINT
name TEXT
path TEXT
);
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id MEDIUMINT
login TEXT
password TEXT
files TEXT
);
INSERT INTO files VALUES ( testjpg media/testjpg );
INSERT INTO files VALUES ( testjpg media/testjpg );
INSERT INTO users VALUES ( jack pass );
系統中的一個用戶可以具有多個文件在編程語言中應該使用數組來表示與一個用戶相關聯的文件在本例中程序員選擇創建一個 files 字段其中包含一個由逗號分隔的文件 id
列表要得到一個特定用戶的所有文件的列表程序員必須首先從用戶表中讀取行然後解析文件的文本並為每個文件運行一個單獨的 SELECT
語句該代碼如下所示
清單 Getphp
<?php
require_once(DBphp);
function get_files( $name )
{
$dsn = mysql://root:password@localhost/bad_norel;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$res = $db>query( SELECT files FROM users WHERE login=?
array( $name ) );
$files = null;
while( $res>fetchInto( $row ) ) { $files = $row[]; }
$rows = array();
foreach( split( $files ) as $file )
{
$res = $db>query( SELECT * FROM files WHERE id=?
array( $file ) );
while( $res>fetchInto( $row ) ) { $rows[] = $row; }
}
return $rows;
}
$files = get_files( jack );
var_dump( $files );
?>
該技術很慢難以維護且沒有很好地利用數據庫惟一的解決方案是重新架構模式以將其轉換回到傳統的關系形式如下所示
清單 Goodsql
DROP TABLE IF EXISTS files;
CREATE TABLE files (
id MEDIUMINT
user_id MEDIUMINT
name TEXT
path TEXT
);
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id MEDIUMINT
login TEXT
password TEXT
);
INSERT INTO users VALUES ( jack pass );
INSERT INTO files VALUES ( testjpg media/testjpg );
INSERT INTO files VALUES ( testjpg media/testjpg );
這裡每個文件都通過 user_id
函數與文件表中的用戶相關這可能與任何將多個文件看成數組的人的思想相反當然數組不引用其包含的對象 —— 事實上反之亦然但是在關系數據庫中工作原理就是這樣的並且查詢也因此要快速且簡單得多清單 展示了相應的 PHP 代碼
清單 Get_goodphp
<?php
require_once(DBphp);
function get_files( $name )
{
$dsn = mysql://root:password@localhost/good_rel;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
$rows = array();
$res = $db>query(
SELECT files* FROM usersfiles WHERE userslogin=?
AND usersid=filesuser_id
array( $name ) );
while( $res>fetchInto( $row ) ) { $rows[] = $row; }
return $rows;
}
$files = get_files( jack );
var_dump( $files );
?>
這裡我們對數據庫進行一次查詢以獲得所有的行代碼不復雜並且它將數據庫作為其原有的用途使用
問題 n+ 模式
我真不知有多少次看到過這樣的大型應用程序其中的代碼首先檢索一些實體(比如說客戶)然後來回地一個一個地檢索它們以得到每個實體的詳細信息我們將其稱為 n+ 模式因為查詢要執行這麼多次 —— 一次查詢檢索所有實體的列表然後對於 n 個實體中的每一個執行一次查詢當 n= 時這還不成其為問題但是當 n= 或 n= 時呢?然後肯定會出現低效率問題清單 展示了這種模式的一個例子
清單 Schemasql
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
id MEDIUMINT NOT NULL AUTO_INCREMENT
name TEXT NOT NULL
PRIMARY KEY ( id )
);
DROP TABLE IF EXISTS books;
CREATE TABLE books (
id MEDIUMINT NOT NULL AUTO_INCREMENT
author_id MEDIUMINT NOT NULL
name TEXT NOT NULL
PRIMARY KEY ( id )
);
INSERT INTO authors VALUES ( null Jack Herrington );
INSERT INTO authors VALUES ( null Dave Thomas );
INSERT INTO books VALUES ( null Code Generation in Action );
INSERT INTO books VALUES ( null Podcasting Hacks );
INSERT INTO books VALUES ( null PHP Hacks );
INSERT INTO books VALUES ( null Pragmatic Programmer );
INSERT INTO books VALUES ( null Ruby on Rails );
INSERT INTO books VALUES ( null Programming Ruby );
該模式是可靠的其中沒有任何錯誤問題在於訪問數據庫以找到一個給定作者的所有書籍的代碼中如下所示
清單 Getphp
<?php
require_once(DBphp);
$dsn = mysql://root:password@localhost/good_books;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
function get_author_id( $name )
{
global $db;
$res = $db>query( SELECT id FROM authors WHERE name=?
array( $name ) );
$id = null;
while( $res>fetchInto( $row ) ) { $id = $row[]; }
return $id;
}
function get_books( $id )
{
global $db;
$res = $db>query( SELECT id FROM books WHERE author_id=?
array( $id ) );
$ids = array();
while( $res>fetchInto( $row ) ) { $ids []= $row[]; }
return $ids;
}
function get_book( $id )
{
global $db;
$res = $db>query( SELECT * FROM books WHERE id=? array( $id ) );
while( $res>fetchInto( $row ) ) { return $row; }
return null;
}
$author_id = get_author_id( Jack Herrington );
$books = get_books( $author_id );
foreach( $books as $book_id ) {
$book = get_book( $book_id );
var_dump( $book );
}
?>
如果您看看下面的代碼您可能會想嘿這才是真正的清楚明了 首先得到作者 id
然後得到書籍列表然後得到有關每本書的信息的確它很清楚明了但是其高效嗎?回答是否定的看看只是檢索 Jack Herrington 的書籍時要執行多少次查詢一次獲得 id
另一次獲得書籍列表然後每本書執行一次查詢三本書要執行五次查詢!
解決方案是用一個函數來執行大量的查詢如下所示
清單 Get_goodphp
<?php
require_once(DBphp);
$dsn = mysql://root:password@localhost/good_books;
$db =& DB::Connect( $dsn array() );
if (PEAR::isError($db)) { die($db>getMessage()); }
function get_books( $name )
{
global $db;
$res = $db>query(
SELECT books* FROM authorsbooks WHERE
booksauthor_id=authorsid AND authorsname=?
array( $name ) );
$rows = array();
while( $res>fetchInto( $row ) ) { $rows []= $row; }
return $rows;
}
$books = get_books( Jack Herrington );
var_dump( $books );
?>
現在檢索列表需要一個快速單個的查詢這意味著我將很可能必須具有幾個這些類型的具有不同參數的方法但是實在是沒有選擇如果您想要具有一個擴展的 PHP 應用程序那麼必須有效地使用數據庫這意味著更智能的查詢
本例的問題是它有點太清晰了通常來說這些類型的 n+ 或 n*n 問題要微妙得多並且它們只有在數據庫管理員在系統具有性能問題時在系統上運行查詢剖析器時才會出現
結束語
數據庫是強大的工具就跟所有強大的工具一樣如果您不知道如何正確地使用就會濫用它們識別和解決這些問題的訣竅是更好地理解底層技術長期以來我老聽到業務邏輯編寫人員抱怨他們不想要必須理解數據庫或 SQL 代碼他們把數據庫當成對象使用並疑惑性能為什麼如此之差
他們沒有認識到理解 SQL 對於將數據庫從一個困難的必需品轉換成強大的聯盟是多麼重要如果您每天使用數據庫但是不熟悉 SQL那麼請閱讀 The Art of SQL這本書寫得很好實踐性也很強可以指導您基本了解數據庫
From:http://tw.wingwit.com/Article/program/PHP/201311/20779.html