揭露 PHP 应用程序中出现的五个常见数据库问题 —— 包括数据库模式设计、数据库访问和使用数据库的业务逻辑代码 —— 以及它们的解决方案。 如果只有一种 方式使用数据库是正确的…… 您可以用很多的方式创建数据库设计、数据库访问和基于数据库的 PHP 业务逻辑代码,但最终一般以错误告终。本文说明了数据库设计和访问数据库的 PHP 代码中出现的五个常见问题,以及在遇到这些问题时如何修复它们。 问题 1:直接使用 MySQL 一个常见问题是较老的 PHP 代码直接使用 mysql_ 函数来访问数据库。清单 1 展示了如何直接访问数据库。 清单 1. Access/get.php <?php $res = mysql_query( "SELECT id FROM users WHERE login=".$name."" ); return $id; var_dump( get_user_id( jack ) ); 注意使用了 mysql_connect 函数来访问数据库。还要注意查询,其中使用字符串连接来向查询添加 $name 参数。 该技术有两个很好的替代方案:PEAR DB 模块和 PHP Data Objects (PDO) 类。两者都从特定数据库选择提供抽象。因此,您的代码无需太多调整就可以在 IBM? DB2?、MySQL、PostgreSQL 或者您想要连接到的任何其他数据库上运行。 使用 PEAR DB 模块和 PDO 抽象层的另一个价值在于您可以在 SQL 语句中使用 ? 操作符。这样做可使 SQL 更加易于维护,且可使您的应用程序免受 SQL 注入攻击。 使用 PEAR DB 的替代代码如下所示。 清单 2. Access/get_good.php <?php function get_user_id( $name ) $res = $db->query( SELECT id FROM users WHERE login=?,array( $name ) ); return $id; var_dump( get_user_id( jack ) ); 注意,所有直接用到 MySQL 的地方都消除了,只有 $dsn 中的数据库连接字符串除外。此外,我们通过 ? 操作符在 SQL 中使用 $name 变量。然后,查询的数据通过 query() 方法末尾的 array 被发送进来。 问题 2:不使用自动增量功能 与大多数现代数据库一样,MySQL 能够在每记录的基础上创建自动增量惟一标识符。除此之外,我们仍然会看到这样的代码,即首先运行一个 SELECT 语句来找到最大的 id,然后将该 id 增 1,并找到一个新记录。清单 3 展示了一个示例坏模式。 清单 3. Badid.sql DROP TABLE IF EXISTS users; INSERT INTO users VALUES ( 1, jack, pass ); 这里的 id 字段被简单地指定为整数。所以,尽管它应该是惟一的,我们还是可以添加任何值,如 CREATE 语句后面的几个 INSERT 语句中所示。清单 4 展示了将用户添加到这种类型的模式的 PHP 代码。 清单 4. Add_user.php <?php function add_user( $name, $pass ) $dsn = mysql://root:password@localhost/bad_badid; $res = $db->query( "SELECT max(id) FROM users" ); $id += 1; $sth = $db->prepare( "INSERT INTO users VALUES(?,?,?)" ); return $id; $id = add_user( jerry, pass ); var_dump( $id ); add_user.php 中的代码首先执行一个查询以找到 id 的最大值。然后文件以 id 值加 1 运行一个 INSERT 语句。该代码在负载很重的服务器上会在竞态条件中失败。另外,它也效率低下。 那么替代方案是什么呢?使用 MySQL 中的自动增量特性来自动地为每个插入创建惟一的 ID。更新后的模式如下所示。 清单 5. Goodid.php DROP TABLE IF EXISTS users; INSERT INTO users VALUES (null, jack, pass); We added the NOT NULL flag to indicate that the field must not be empty. We also added the AUTO_INCREMENT flag to indicate that the field is auto-incrementing, and the PRIMARY KEY flag to indicate that the field is an id. These changes speed things up. Listing 6 shows the updated PHP code that inserts the user into the table. Listing 6. Add_user_good.php <?php function add_user( $name, $pass ) $sth = $db->prepare( "INSERT INTO users VALUES(null,?,?)" ); $res = $db->query( "SELECT last_insert_id()" ); return $id; $id = add_user( jerry, pass ); var_dump( $id ); Now instead of getting the largest id value, I directly use the INSERT statement to insert the data, and then use the SELECT statement to retrieve the id of the last inserted record. The code is much simpler and more efficient than the original version and its associated patterns. Issue 3: Using multiple databases Occasionally, we will see an application where each table is in a separate database. This is reasonable in very large databases, but for general applications this level of partitioning is not required. Additionally, relational queries cannot be performed across databases, which takes away from the whole idea of using a relational database, not to mention that it would be more difficult to manage tables across multiple databases. So, what should multiple databases look like? First, you need some data. Listing 7 shows such data split into 4 files. Listing 7. Database file Files.sql: Load_files.sql: Users.sql: Load_users.sql: In the multi-database version of these files, you should load the SQL statements into one database and then load the users SQL statements into another database. The PHP code for querying the database for files associated with a specific user is shown below. Listing 8. Getfiles.php <?php function get_user( $name ) $res = $db->query( "SELECT id FROM users WHERE login=?",array( $name ) ); return $uid; function get_files( $name ) $rows = array(); $dsn = mysql://root:password@localhost/bad_multi2; $res = $db->query( "SELECT * FROM files WHERE user_id=?",array( $uid ) ); $files = get_files( jack ); var_dump( $files ); The get_user function connects to the database containing the users table and retrieves the ID of a given user. The get_files function connects to the files table and retrieves the file rows associated with a given user. A better way to do all of these things is to load the data into a database and then execute a query, such as the query below. Listing 9. Getfiles_good.php <?php function get_files( $name ) $dsn = mysql://root:password@localhost/good_multi;
function get_user_id( $name )
{
$db = mysql_connect( localhost, root, password );
mysql_select_db( users );
while( $row = mysql_fetch_array( $res ) ) { $id = $row[0]; }
}
?>
require_once("DB.php");
{
$dsn = mysql://root:password@localhost/users;
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0]; }
}
?>
CREATE TABLE users (
id MEDIUMINT,
login TEXT,
password TEXT
);
INSERT INTO users VALUES ( 2, joan, pass );
INSERT INTO users VALUES ( 1, jane, pass );
require_once("DB.php");
{
$rows = array();
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0]; }
$db->execute( $sth, array( $id, $name, $pass ) );
}
?>
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, joan, pass);
INSERT INTO users VALUES (null, jane, pass);
require_once("DB.php");
{
$dsn = mysql://root:password@localhost/good_genid;
$db =& DB::Connect( $dsn, array () );
if (PEAR::isError($db)) { die($db->getMessage()); }
$db->execute( $sth, array( $name, $pass ) );
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[ 0]; }
}
?>
CREATE TABLE files (
id MEDIUMINT,
user_id MEDIUMINT,
name TEXT,
path TEXT
);
INSERT INTO files VALUES ( 1, 1, test1.jpg, files/test1.jpg );
INSERT INTO files VALUES ( 2, 1, test2.jpg, files/test2. jpg );
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id MEDIUMINT,
login TEXT,
password TEXT
);
INSERT INTO users VALUES (1, jack, pass);
INSERT INTO users VALUES (2, jon, pass);
require_once("DB.php");
{
$dsn = mysql://root:password@localhost/bad_multi1;
$db =& DB::Connect( $dsn, array() ) ;
if (PEAR::isError($db)) { die($db->getMessage()); }
$uid = null;
while( $res->fetchInto( $ row ) ) { $uid = $row[0]; }
}
{
$uid = get_user( $name );
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db )) { die($db->getMessage()); }
while( $res->fetchInto( $row ) ) { $rows[] = $row; }
return $rows;
}
?>
require_once("DB.php");
{
$rows = array();