揭露 PHP 应用程序中出现的五个常见数据库问题 —— 包括数据库模式设计数据库访问和使用数据库的业务逻辑代码 —— 以及它们的解决方案
如果只有一种 方式使用数据库是正确的……
您可以用很多的方式创建数据库设计数据库访问和基于数据库的 PHP 业务逻辑代码但最终一般以错误告终本文说明了数据库设计和访问数据库的 PHP 代码中出现的五个常见问题以及在遇到这些问题时如何修复它们
问题 直接使用 MySQL
一个常见问题是较老的 PHP 代码直接使用 mysql_
函数来访问数据库清单 展示了如何直接访问数据库
清单 Access/getphp
<?phpfunction 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
<?phprequire_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
<?phprequire_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
<?phprequire_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
<?phprequire_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
<?phprequire_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
<?phprequire_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
<?phprequire_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
<?phprequire_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
<?phprequire_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这本书写得很好实践性也很强可以指导您基本了解数据库