为什么我不应该在PHP中使用mysql_*函数?
为什么我不应该在PHP中使用mysql_*函数?
想要完善本文吗?提供详细的解答,包括引用和解释为什么您的答案是正确的。缺乏足够细节的答案可能会被编辑或删除。
为什么不能使用mysql_*
函数?(例如mysql_query()
,mysql_connect()
或mysql_real_escape_string()
)的技术原因是什么?
即使它们在我的网站上起作用,为什么我还应该使用其他的东西?
如果它们在我的网站上不起作用,为什么会出现错误,例如
警告:mysql_connect():没有这样的文件或目录
PHP提供了三种不同的API连接MySQL。它们分别是mysql
(自PHP 7以后已经删除),mysqli
和PDO
扩展。
mysql_*
函数曾经很受欢迎,但现在不再鼓励使用。文档团队正在讨论数据库安全情况,教育用户移开常用的ext/mysql扩展之一是这个过程的一部分(参见php.internals: deprecating ext/mysql)。
后来的PHP开发人员已经决定在用户连接到MySQL时生成E_DEPRECATED
错误,无论是通过mysql_connect()
,mysql_pconnect()
还是隐式链接功能(内置到ext/mysql
中)。
ext/mysql
自PHP 5.5以后官方废弃,并且已经自PHP 7中删除。
看到红框了吗?
当您访问任何mysql_*
函数手册页面时,您会看到一个红框,解释为什么不应再使用它。
为什么
移开ext/mysql
不仅是为了安全,还在于可以访问MySQL数据库所有功能。
ext/mysql
是为MySQL 3.23构建的,此后只有很少的添加,同时保持与旧版本的兼容性,使代码变得有些难以维护。不能由ext/mysql
支持的缺失功能包括:(来自PHP手册).
不使用mysql_*
函数的原因:
不支持预处理语句特别重要,因为它们提供了一种更明确、更少错误的方法来转义和引用外部数据,而不是通过单独的函数调用手动转义。
请参阅SQL扩展的比较。
抑制弃用警告
在代码转换为MySQLi
/PDO
时,可以通过在php.ini中设置error_reporting
来排除E_DEPRECATED
的错误来抑制它们:
error_reporting = E_ALL ^ E_DEPRECATED
请注意,这也会隐藏其他弃用警告,但这可能不是关于MySQL的东西。(来自PHP手册)
文章Dejan Marjanovic的“PDO vs. MySQLi:应该使用哪一个?”可以帮助你做出选择。
而更好的方法是使用PDO
,我现在正在编写一个简单的PDO
教程。
一个简单和短的PDO教程
Q. 我心中的第一个问题是:什么是`PDO`?
A. “PDO - PHP数据对象 - 是一个数据库访问层,提供了访问多个数据库的统一方法。”
连接到MySQL
使用mysql_*
函数或我们可以说它是老的方式(在PHP 5.5及以上版本中已弃用)
$link = mysql_connect('localhost', 'user', 'pass'); mysql_select_db('testdb', $link); mysql_set_charset('UTF-8', $link);
使用PDO
:你所需要做的就是创建一个新的PDO
对象。构造函数接受用于指定数据库源的参数,PDO
的构造函数通常需要四个参数,分别是DSN
(数据源名称)和可选的username
、password(s)
。
在这里我认为你已经熟悉了所有数据(username、password等),除了DSN
;这在PDO
中是新的。一个DSN
基本上是一串选项的字符串,它告诉PDO
要使用哪一个驱动程序和连接细节。更多参考,请查看PDO MySQL DSN。
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');
注意:你也可以使用charset=UTF-8
,但有时会导致错误,因此最好使用utf8
。
如果有连接错误,它将抛出一个PDOException
对象,可以捕获它以进一步处理Exception
。
好文章:连接和连接管理¶
您还可以将多个驱动程序选项作为数组传递给第四个参数。我建议您将参数传递到将PDO
置于异常模式的参数。因为一些PDO
驱动程序不支持本机预准备语句,所以PDO
执行准备的仿真。它还让您手动启用此仿真。要使用本机服务器端准备的语句,您应该将其显式设置为false
。
另一种方法是关闭默认情况下在MySQL
驱动程序中启用的准备仿真,但为了安全地使用PDO
,应关闭准备仿真。
稍后我将解释为什么应该关闭准备仿真。要查找原因,请查看此帖子。
仅当您使用不建议使用的旧版本MySQL
时才可使用它。
以下是示例,说明如何进行操作:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8', 'username', 'password', array(PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
在PDO构建之后,我们可以设置属性吗?
是的,我们也可以使用setAttribute
方法在PDO构建之后设置某些属性:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8', 'username', 'password'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
错误处理
在PDO
中,错误处理比mysql_*
要容易得多。
在使用mysql_*
时的一个常见做法是:
//Connected to MySQL $result = mysql_query("SELECT * FROM table", $link) or die(mysql_error($link));
使用OR die()
不是处理错误的好方法,因为我们不能在die
中处理问题。它会突然终止脚本,然后将错误消息回显到屏幕上,通常不希望将其显示给最终用户,让可恶的黑客发现您的架构。作为替代方法,可以将mysql_*
函数的返回值与mysql_error()一起使用来处理错误。
PDO
提供了更好的解决方案:异常。我们使用PDO
进行的任何操作都应该包含在try
-catch
块中。可以通过设置错误模式属性来将PDO
强制设置为三种错误模式之一。下面是三种错误处理模式:
PDO::ERRMODE_SILENT
。只是设置错误代码,并且与mysql_*
的行为非常相似,您必须检查每个结果,然后查看$db->errorInfo();
以获取错误详细信息。PDO::ERRMODE_WARNING
引发E_WARNING
。(运行时警告(非致命错误)。脚本的执行不会停止。)PDO::ERRMODE_EXCEPTION
:抛出异常。它表示由PDO引发的错误。您不应该从自己的代码中抛出PDOException
。有关PHP中异常的更多信息,请参见异常。当没有捕获时,它的行为非常类似于or die(mysql_error());
。但是,与or die()
不同,如果选择这样做,可以优雅地捕获和处理PDOException
。
阅读材料:
例如:
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); $stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING ); $stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
你可以用try
-catch
包装它,如下:
try { //Connect as appropriate as above $db->query('hi'); //Invalid query! } catch (PDOException $ex) { echo "An Error occured!"; //User friendly message/message you want to show to user some_logging_function($ex->getMessage()); }
你现在不必处理try
-catch
。在任何适当的时候都可以捕捉它,但我强烈建议您使用try
-catch
。而且在调用PDO
的函数之外捕捉可能更合理:
function data_fun($db) { $stmt = $db->query("SELECT * FROM table"); return $stmt->fetchAll(PDO::FETCH_ASSOC); } //Then later try { data_fun($db); } catch(PDOException $ex) { //Here you can handle error and show message/perform action you want. }
还可以使用or die()
进行处理,也可以像mysql_*
那样说,但这将变得十分复杂。您可以通过关闭display_errors
并仅阅读错误日志来在生产中隐藏危险的错误消息。
现在,阅读了上面所有的内容,你可能会想:当我只想开始学习简单的SELECT
、INSERT
、UPDATE
或DELETE
语句时,这是什么鬼?别担心,我们来看看:
选择数据
在mysql_*
中所做的操作是:
现在,在
PDO
中,你可以这样做:query('SELECT * FROM table'); while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { echo $row['field1']; }
或者
query('SELECT * FROM table'); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); //Use $results
注意:如果你使用像下面这样的方法(
query()
),这个方法将返回一个PDOStatement
对象。因此,如果想获取结果,就像上面一样使用它。query('SELECT * FROM table') as $row) { echo $row['field1']; }
在PDO Data中,它通过语句句柄的
->fetch()
方法获得。在调用fetch()之前,最好告诉PDO你想要如何获取数据。在下面的部分中,我将解释这一点。获取模式
注意在
fetch()
和fetchAll()
代码中使用了PDO::FETCH_ASSOC
。这告诉PDO
将行作为具有字段名称的键的关联数组返回。还有许多其他的抓取模式,我将逐一解释。首先,我解释一下如何选择获取模式:
$stmt->fetch(PDO::FETCH_ASSOC)
在上述代码中,我一直使用
fetch()
。你也可以使用:
PDOStatement::fetchAll()
- 返回包含所有结果集行的数组PDOStatement::fetchColumn()
- 从结果集的下一行返回单个列PDOStatement::fetchObject()
- 获取下一行并将其作为对象返回。PDOStatement::setFetchMode()
- 为此语句设置默认的获取模式
现在我来讲解抓取模式:
PDO::FETCH_ASSOC
:返回一个由列名索引的数组,这些列名是您的结果集中返回的PDO::FETCH_BOTH
(默认):返回一个由列名和以0为基础的列数索引的数组,这些列名是您的结果集中返回的
还有更多的选择!在PDOStatement
Fetch documentation中了解所有内容。
获取行数:
不要使用mysql_num_rows
来获取返回行数,您可以获取一个PDOStatement
并执行rowCount()
,例如:
query('SELECT * FROM table'); $row_count = $stmt->rowCount(); echo $row_count.' rows selected';
获取最后插入的ID
exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')"); $insertId = $db->lastInsertId();
插入和更新或删除语句
我们在mysql_*
函数中所做的是:
而在pdo中,这同样可以使用以下方式实现:
exec("UPDATE table SET field='value'"); echo $affected_rows;
在上面的查询中,
PDO::exec
执行一个SQL语句并返回受影响的行数。插入和删除将在以后讨论。
当您需要在查询中使用变量时,请不要像上面那样尝试,因此预处理语句或参数化语句是必须的。
预处理语句
Q.什么是预处理语句,我为什么需要它们?
A.预处理语句是一种预编译的SQL语句,可以通过仅向服务器发送数据来多次执行它们。使用预处理语句的典型工作流程如下所示(引用自维基百科三个点):
准备:应用程序创建语句模板并将其发送到数据库管理系统(DBMS)。某些值未指定,称为参数,占位符或绑定变量(下面标有
?
):
INSERT INTO PRODUCT (name, price) VALUES (?, ?)
DBMS对语句模板进行解析、编译和执行查询优化,并将结果存储在不执行的情况下。
- 执行:在稍后的时间,应用程序为参数提供(或绑定)值,DBMS执行语句(可能返回结果)。应用程序可以多次使用不同的值执行该语句。在这个例子中,它可能会为第一个参数提供“面包”,第二个参数提供
1.00
。您可以在SQL中包含占位符来使用预处理语句。基本上有三个没有占位符的(不要尝试使用变量,它在上面),一个使用未命名占位符,一个使用命名占位符。
问题: 现在,命名占位符是什么以及如何使用它们?
答案: 命名占位符。使用描述性名称并在冒号前面,而不是问号。我们不关心名称占位符中值的位置/顺序:$stmt->bindParam(':bla', $bla);
bindParam(parameter,variable,data_type,length,driver_options)
您也可以使用执行数组绑定:
prepare("SELECT * FROM table WHERE id=:id AND name=:name"); $stmt->execute(array(':name' => $name, ':id' => $id)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
命名占位符的另一个良好特性是,假设属性与命名字段匹配,它们具有将对象直接插入到数据库的功能。例如:
class person { public $name; public $add; function __construct($a,$b) { $this->name = $a; $this->add = $b; } } $demo = new person('john','29 bla district'); $stmt = $db->prepare("INSERT INTO table (name, add) value (:name, :add)"); $stmt->execute((array)$demo);
问题: 现在,未命名的占位符是什么以及如何使用它们?
答案: 让我们举一个例子:prepare("INSERT INTO folks (name, add) values (?, ?)"); $stmt->bindValue(1, $name, PDO::PARAM_STR); $stmt->bindValue(2, $add, PDO::PARAM_STR); $stmt->execute();和
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)"); $stmt->execute(array('john', '29 bla district'));在上面的示例中,您可以看到那些问号而不是像命名占位符中那样的名称。现在,在第一个示例中,我们将变量分配给各个占位符(
$stmt->bindValue(1, $name, PDO::PARAM_STR);
)。然后,我们向这些占位符分配值并执行语句。在第二个示例中,第一个数组元素进入第一个问号,第二个进入第二个问号。注意: 在未命名的占位符中,我们必须注意将要传递给
PDOStatement::execute()
方法的数组中元素的正确顺序。
SELECT
,INSERT
,UPDATE
,DELETE
预准备查询
SELECT
:$stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name"); $stmt->execute(array(':name' => $name, ':id' => $id)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
INSERT
:$stmt = $db->prepare("INSERT INTO table(field1,field2) VALUES(:field1,:field2)"); $stmt->execute(array(':field1' => $field1, ':field2' => $field2)); $affected_rows = $stmt->rowCount();
DELETE
:$stmt = $db->prepare("DELETE FROM table WHERE id=:id"); $stmt->bindValue(':id', $id, PDO::PARAM_STR); $stmt->execute(); $affected_rows = $stmt->rowCount();
UPDATE
:$stmt = $db->prepare("UPDATE table SET name=? WHERE id=?"); $stmt->execute(array($name, $id)); $affected_rows = $stmt->rowCount();
注意:
但是
PDO
和/或MySQLi
并不完全安全。请查看Are PDO prepared statements sufficient to prevent SQL injection?的答案,by ircmaxell。此外,我引用了他答案中的一部分:$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $pdo->query('SET NAMES GBK'); $stmt = $pdo->prepare("SELECT * FROM test WHERE name = ? LIMIT 1"); $stmt->execute(array(chr(0xbf) . chr(0x27) . " OR 1=1 /*"));
MySQL扩展:
- 不在积极开发中
- 自PHP 5.5(于2013年6月发布)起 官方已经宣布弃用。
- 自PHP 7.0(于2015年12月发布)起已完全删除。
- 这意味着自2018年12月31日起,它在任何受支持的PHP版本中都不存在。如果你正在使用支持它的版本,你正在使用一个不会修复安全问题的版本。
- 缺乏面向对象的接口
- 不支持:
- 非阻塞的异步查询
- 预处理语句或参数化查询
- 存储过程
- 多个语句
- 事务
- “新”密码认证方法(在MySQL 5.6中默认开启;在5.7中必须使用)
- MySQL 5.1或之后的任何新功能
由于已被弃用,使用它会使你的代码不够未来化。
缺乏预处理语句的支持尤其重要,因为它们提供了一种比手动使用单独的函数调用对外部数据进行转义和引用更清晰、更不容易出错的方法。
参见SQL扩展的比较。