为什么不应该使用mysql_*函数的技术原因是什么?(例如mysql_query()、mysql_connect()或mysql_real_ascape_string())?

为什么我要使用其他东西,即使它们在我的网站上工作?

如果它们在我的网站上不起作用,为什么我会出现这样的错误

警告:mysql_connect():没有这样的文件或目录


当前回答

说到技术原因,只有几个,非常具体,很少使用。很可能你一生中都不会使用它们。也许我太无知了,但我从来没有机会使用它们

非阻塞异步查询返回多个结果集的存储过程加密(SSL)压缩

如果您需要,这些无疑是从mysql扩展转向更时尚、更现代的技术原因。

尽管如此,也有一些非技术性的问题,这会使您的体验更加困难

在现代PHP版本中进一步使用这些函数将引起不推荐使用的级别通知。它们可以简单地关闭。在不久的将来,它们可能会从默认的PHP构建中删除。也没什么大不了的,因为mydsql-ext将被迁移到PECL中,每个宿主都会很乐意用它来编译PHP,因为他们不想失去那些网站工作了几十年的客户。Stackoverflow社区的强烈抵抗。每次你提到这些诚实的功能时,你都会被告知它们是严格的禁忌。作为一个普通的PHP用户,您使用这些函数的想法很可能是错误的。就因为有这么多的教程和手册教你错误的方法。不是函数本身-我必须强调它-而是它们的使用方式。

后一个问题是一个问题。但是,在我看来,提议的解决方案也没有更好。在我看来,所有这些PHP用户都将立即学习如何正确处理SQL查询,这是一个过于理想化的梦想。他们很可能只是机械地将mysql_*更改为mysqli_*,保持方法不变。特别是因为mysqli使准备好的语句使用起来非常痛苦和麻烦。更不用说,本机准备的语句不足以防止SQL注入,mysqli和PDO都没有提供解决方案。

因此,我宁愿反对错误的做法,以正确的方式教育人们,而不是反对这种诚实的延伸。

此外,还有一些错误或不重要的原因,如

不支持存储过程(我们使用的是mysql_query(“CALL my_proc”);年龄)不支持交易(同上)不支持多个语句(谁需要它们?)不在积极发展中(那么,它对你有什么实际影响吗?)缺少OO接口(创建一个OO接口需要几个小时)不支持准备语句或参数化查询

最后一点很有趣。虽然mysql-ext不支持本机准备的语句,但出于安全考虑,它们不是必需的。我们可以使用手动处理的占位符轻松地伪造准备好的语句(就像PDO一样):

function paraQuery()
{
    $args  = func_get_args();
    $query = array_shift($args);
    $query = str_replace("%s","'%s'",$query); 

    foreach ($args as $key => $val)
    {
        $args[$key] = mysql_real_escape_string($val);
    }

    $query  = vsprintf($query, $args);
    $result = mysql_query($query);
    if (!$result)
    {
        throw new Exception(mysql_error()." [$query]");
    }
    return $result;
}

$query  = "SELECT * FROM table where a=%s AND b LIKE %s LIMIT %d";
$result = paraQuery($query, $a, "%$b%", $limit);

瞧,一切都是参数化和安全的。

但好吧,如果你不喜欢手册中的红色框,那么会出现一个选择问题:mysqli还是PDO?

答案如下:

如果您了解使用数据库抽象层并寻找API来创建抽象层的必要性,那么mysqli是一个非常好的选择,因为它确实支持许多mysql特有的特性。如果像绝大多数PHP用户一样,您在应用程序代码中正确使用原始API调用(这基本上是错误的做法),那么PDO是唯一的选择,因为这个扩展假装不仅仅是API,而是一个半DAL,仍然不完整,但提供了许多重要功能,其中两个功能使PDO与mysqli有着重要区别:与mysqli不同,PDO可以通过值绑定占位符,这使得动态构建的查询变得可行,而无需几个屏幕的代码。与mysqli不同,PDO总是可以在一个简单的普通数组中返回查询结果,而mysqli只能在mysqlnd安装上执行。

因此,如果你是一个普通的PHP用户,并且想在使用本机准备的语句时省去一大堆麻烦,那么PDO是唯一的选择。然而,PDO也不是银弹,也有其困难。因此,我为PDO标签wiki中的所有常见陷阱和复杂案例编写了解决方案

然而,每个谈论扩展的人总是忽略了关于Myqli和PDO的两个重要事实:

事先准备好的声明不是灵丹妙药。有一些动态标识符不能使用准备好的语句绑定。存在具有未知数量参数的动态查询,这使得查询构建成为一项困难的任务。mysqli_*和PDO函数都不应该出现在应用程序代码中。在它们和应用程序代码之间应该有一个抽象层,它将在内部完成绑定、循环、错误处理等所有肮脏的工作,使应用程序代码变得干干净净。特别是对于动态查询构建这样的复杂情况。

因此,仅仅切换到PDO或mysqli是不够的。必须使用ORM、查询生成器或任何数据库抽象类,而不是在代码中调用原始API函数。相反,如果在应用程序代码和mysqlAPI之间有一个抽象层,那么使用哪个引擎实际上并不重要。您可以使用mysql-ext,直到它被弃用,然后轻松地将抽象类重写到另一个引擎,使所有应用程序代码保持完整。

下面是基于我的safemysql类的一些示例,以说明这样的抽象类应该是什么样子的:

$city_ids = array(1,2,3);
$cities   = $db->getCol("SELECT name FROM cities WHERE is IN(?a)", $city_ids);

将这一行与PDO所需的代码量进行比较。然后,将您需要的大量代码与原始Myqli准备的语句进行比较。请注意,错误处理、分析和查询日志已经内置并正在运行。

$insert = array('name' => 'John', 'surname' => "O'Hara");
$db->query("INSERT INTO users SET ?u", $insert);

将其与通常的PDO插入进行比较,当每个字段名重复六到十次时,在所有这些众多的命名占位符、绑定和查询定义中。

另一个例子:

$data = $db->getAll("SELECT * FROM goods ORDER BY ?n", $_GET['order']);

您很难找到PDO处理此类实际案例的示例。而且这太啰嗦了,很可能是不安全的。

所以,再一次,你应该关注的不仅仅是原始驱动程序,而是抽象类,它不仅适用于初学者手册中的愚蠢示例,也适用于解决任何现实问题。

其他回答

PHP提供了三种不同的API来连接MySQL。这些是mysql(自PHP7起删除)、mysqli和PDO扩展。

mysql_*函数曾经非常流行,但现在已经不鼓励使用了。文档团队正在讨论数据库安全情况,教育用户远离常用的ext/mysql扩展也是其中的一部分(请检查php.internals:弃用ext/mysql)。

后来的PHP开发团队决定在用户连接到MySQL时生成E_DEPRECATED错误,无论是通过MySQL_connect()、MySQL_pconnect()还是ext/MySQL内置的隐式连接功能。

ext/mysql从PHP 5.5开始被正式弃用,从PHP 7开始被删除。

看到红盒子了吗?

当您进入任何mysql_*函数手册页面时,都会看到一个红色框,说明它不应该再使用。

Why


离开ext/mysql不仅仅是为了安全性,还为了能够访问mysql数据库的所有特性。

ext/mysql是为MySQL3.23构建的,从那时起只添加了很少的内容,但大部分都保持了与这个旧版本的兼容性,这使得代码有点难以维护。ext/mysql不支持的缺失功能包括:(来自PHP手册)。

存储过程(无法处理多个结果集)编制的报表加密(SSL)压缩完整字符集支持

不使用mysql_*函数的原因:

未积极开发自PHP 7起删除缺少OO接口不支持非阻塞异步查询不支持准备好的语句或参数化查询不支持存储过程不支持多个语句不支持事务不支持MySQL 5.1中的所有功能

以上引用了昆汀的回答

缺少对准备好的语句的支持尤为重要,因为与使用单独的函数调用手动转义外部数据相比,它们提供了一种更清晰、更不易出错的转义和引用外部数据的方法。

请参阅SQL扩展的比较。


取消弃用警告

当代码转换为MySQLi/PDO时,通过在php.ini中设置error_reporting以排除E_DEPRECATED,可以抑制E_DEPRECATE错误:

error_reporting = E_ALL ^ E_DEPRECATED

注意,这也会隐藏其他弃用警告,但是,这些警告可能针对MySQL以外的其他内容。(摘自PHP手册)

文章PDO与MySQLi:你应该使用哪一个?Dejan Marjanovic将帮助您选择。

更好的方法是PDO,我现在正在编写一个简单的PDO教程。


简单简短的PDO教程


问:我脑海中的第一个问题是:什么是“PDO”?

答:“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(数据源名称)和可选的用户名和密码。

在这里,我认为您对除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中异常的更多信息,请参阅异常。它的行为非常像或死(mysql_error());,当它没有被抓住时。但与or die()不同,如果您选择这样做,PDOException可以被捕获并优雅地处理。

读起来不错:

错误和错误处理¶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.
}

此外,您可以处理by或die(),或者我们可以说像mysql_*,但它会非常多样化。通过关闭display_errors并读取错误日志,可以在生产中隐藏危险的错误消息。

现在,在阅读了以上所有内容之后,您可能会想:当我只想开始学习简单的SELECT、INSERT、UPDATE或DELETE语句时,这到底是什么?别担心,我们来了:


选择数据

因此,您在mysql_*中所做的是:

<?php
$result = mysql_query('SELECT * from table') or die(mysql_error());

$num_rows = mysql_num_rows($result);

while($row = mysql_fetch_assoc($result)) {
    echo $row['field1'];
}

现在在PDO中,您可以这样做:

<?php
$stmt = $db->query('SELECT * FROM table');

while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    echo $row['field1'];
}

Or

<?php
$stmt = $db->query('SELECT * FROM table');
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

//Use $results

注意:如果使用下面的方法(query()),此方法将返回PDOStatement对象。因此,如果您想获取结果,请像上面那样使用它。

<?php
foreach($db->query('SELECT * FROM table') as $row) {
    echo $row['field1'];
}

在PDOData中,它是通过->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文档中的所有内容。。

获取行计数:

您可以获取PDOStatement并执行rowCount(),而不是使用mysql_num_rows来获取返回的行数,例如:

<?php
$stmt = $db->query('SELECT * FROM table');
$row_count = $stmt->rowCount();
echo $row_count.' rows selected';

获取上次插入的ID

<?php
$result = $db->exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')");
$insertId = $db->lastInsertId();

插入和更新或删除语句

我们在mysql_*函数中所做的是:

<?php
$results = mysql_query("UPDATE table SET field='value'") or die(mysql_error());
echo mysql_affected_rows($result);

在pdo中,同样的事情可以通过以下方式完成:

<?php
$affected_rows = $db->exec("UPDATE table SET field='value'");
echo $affected_rows;

在上述查询中,PDO::exec执行SQL语句并返回受影响的行数。

稍后将介绍插入和删除。

上述方法仅在查询中不使用变量时有用。但当您需要在查询中使用变量时,千万不要像上面那样尝试,因为准备好的语句或参数化语句就是这样。


编制的报表

问:什么是准备好的声明,为什么我需要它们?答:准备好的语句是预编译的SQL语句,只需将数据发送到服务器即可多次执行。

使用准备好的语句的典型工作流程如下(引自维基百科三点):

准备:语句模板由应用程序创建并发送到数据库管理系统(DBMS)。某些值未指定,称为参数、占位符或绑定变量(以下标记为?):插入产品(名称、价格)值(?,?)DBMS对语句模板进行分析、编译和执行查询优化,并在不执行结果的情况下存储结果。执行:稍后,应用程序为参数提供(或绑定)值,DBMS执行语句(可能返回结果)。应用程序可以使用不同的值执行任意次数的语句。在本例中,它可能为第一个参数提供“面包”,为第二个参数提供1.00。

您可以通过在SQL中包含占位符来使用准备好的语句。基本上有三个没有占位符(不要尝试使用上面的变量its),一个没有命名占位符,一个有命名占位符。

问:现在,什么是命名占位符,我如何使用它们?A.命名占位符。使用冒号前面的描述性名称,而不是问号。我们不关心名称占位符中的位置/价值顺序:

 $stmt->bindParam(':bla', $bla);

bindParam(参数、变量、data_type、长度、驱动程序选项)

也可以使用执行数组进行绑定:

<?php
$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);

OOP朋友的另一个很好的特性是,命名占位符可以直接将对象插入数据库,前提是财产与命名字段匹配。例如:

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);

问:现在,什么是未命名占位符,我如何使用它们?A.让我们举个例子:

<?php
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $add, PDO::PARAM_STR);
$stmt->execute();

and

$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->execute(array('john', '29 bla district'));

在上面,你可以看到这些?而不是像名字占位符那样的名字。现在在第一个示例中,我们将变量分配给各种占位符($stmt->bindValue(1,$name,PDO::PARAM_STR);)。然后,我们将值分配给这些占位符并执行该语句。在第二个示例中,第一个数组元素转到第一个?第二个到第二个?。

注意:在未命名占位符中,我们必须注意传递给PDOState::execute()方法的数组中元素的正确顺序。


SELECT、INSERT、UPDATE、DELETE准备好的查询

选择:$stmt=$db->prepare(“SELECT*FROM table WHERE id=:id AND name=:name”);$stmt->execute(数组(“:name”=>$name,“:id”=>$id));$rows=$stmt->fetchAll(PDO::FETCH_ASSOC);插入:$stmt=$db->prepare(“INSERT INTO table(field1,field2)VALUES(:field1,:field2)”);$stmt->execute(数组(':field1'=>$field1,':field2'=>$field2));$affected_rows=$stmt->rowCount();删除:$stmt=$db->prepare(“DELETE FROM table WHERE id=:id”);$stmt->bindValue(“:id”,$id,PDO::PARAM_STR);$stmt->execute();$affected_rows=$stmt->rowCount();更新:$stmt=$db->prepare(“UPDATE table SET name=?WHERE id=?”);$stmt->execute(数组($name,$id));$affected_rows=$stmt->rowCount();


注:

然而,PDO和/或MySQLi并不完全安全。检查答案PDO准备的语句是否足以防止SQL注入?由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_函数:

已过时-不再维护不允许您轻松移动到另一个数据库后端不支持准备好的语句,因此鼓励程序员使用串联来构建查询,从而导致SQL注入漏洞

因为(除其他原因外)要确保输入数据得到净化要困难得多。如果您使用参数化查询,就像使用PDO或mysqli一样,您可以完全避免风险。

例如,有人可以使用“enhzflep);drop table users”作为用户名。旧的函数将允许每个查询执行多个语句,所以像这种讨厌的bug可以删除整个表。

如果要使用mysqli的PDO,用户名将以“enhzflep);drop table user”结尾。

参见bobby-tables.com。

MySQL扩展是三个扩展中最古老的,也是开发人员用来与MySQL通信的原始方式。由于PHP和MySQL的更新版本都有改进,因此现在不推荐使用此扩展,而支持其他两种替代方案。

MySQLi是用于处理MySQL数据库的“改进”扩展。它利用了MySQL服务器较新版本中可用的功能,向开发人员公开了面向功能和面向对象的界面,并做了一些其他漂亮的事情。PDO提供了一个API,它整合了以前分布在主要数据库访问扩展(即MySQL、PostgreSQL、SQLite、MSSQL等)中的大部分功能。该接口为程序员提供高级对象,以处理数据库连接、查询和结果集,低级驱动程序与数据库服务器进行通信和资源处理。PDO正在进行大量的讨论和工作,它被认为是使用现代专业代码处理数据库的适当方法。

原因有很多,但可能最重要的一点是,这些函数鼓励不安全的编程实践,因为它们不支持准备好的语句。准备好的语句有助于防止SQL注入攻击。

当使用mysql_*函数时,您必须记住通过mysql_real_aescape_string()运行用户提供的参数。如果你只在一个地方忘记了,或者你碰巧只漏掉了部分输入,你的数据库可能会受到攻击。

在PDO或mysqli中使用已准备好的语句将使其更难产生此类编程错误。