【问题标题】:PHP, PDO and SQLSRV executes multiple times on one INSERT statementPHP、PDO 和 SQLSRV 在一个 INSERT 语句上执行多次
【发布时间】:2012-02-14 15:59:04
【问题描述】:

我在 MySQL 和 Apache 服务器上使用 PDO 和 PHP 已经有一段时间了。我最近的任务是将企业的旧 Web 应用程序转换为新设置。旧设置是标准 Linux Web 堆栈(Apache/PHP/MySQL/Filezilla),新设置将是带有 IIS/PHP(快速 cgi 安装)/SQL Server 2003/无 FTP 的 Windows 服务器 2003。

除了转换 MySQL 语句以更新带有文件访问信息的表之外,我几乎可以进行所有工作。使用带有 SQLSRV 驱动程序的 PDO 并在“文件下载”PHP 脚本中执行插入语句,将多条记录插入到 SQL 表中。

download.php 确实会向 SQL 服务器发出多个查询。一次检查一个表中的文件存在和变量,然后用访问信息更新另一个表。

请参阅下面的 download.php 代码

调试显示 $count 的打印/回显为 1。然而,检查 SQL 服务器记录,总是显示多于一个插入。有时它只是一个额外的行,总共两个,但有时它高达四个被插入的 EXTRA 语句。 $count 在任何情况下都显示为 1。

此特定 PHP 脚本在调用此插入语句之前根据 SQL 数据库验证信息。首先,验证文件访问的身份验证(成功),验证文件存在(成功),然后用下载信息更新访问表(错误),最后将 PDF 提供给用户(成功)。

当我手动向查询分析器发出 INSERT 语句时,它成功并按预期工作;它每次插入一行。该错误似乎与 execute() 的 SQLSRV 或 PDO 实现有关。

我已经在 stackoverflow、serverfault 和全能的 Google 上搜索过有关这方面的信息。用户需要在一个语句/执行中执行多个查询/插入时返回的唯一类型的结果。我的问题是相反的;我希望只执行一个插入语句,但是总是执行多个。

问题是:为什么会发生这种情况以及如何防止多次插入发生?

根据请求更新

访问此文件的代码是来自另一个网页的单个链接。该页面列出了允许用户访问的当前文件,并提供了指向 download.php 脚本的链接,用于 PDF 的验证、更新和实际服务。

查看页面有一个链接列表(在 for 循环中打印),排列如下:

<a href='download.php?f={$item['name']}&t={$type}' target='_blank'>{$item['name']}</a>

当用户点击该链接时,除了上面的 download.php 代码之外,下面的脚本会运行。它成功地提供了 PDF 文件。内容由 download.php 作为 PHP 标头/内联 PDF 发送:

代码见下文

查看服务器日志显示 两个 GET 请求到 download.php 文件:

2012-02-14 17:44:37 W3SVC1785071458 172.17.31.254 GET /download.php f=06304844-1A.pdf&t=av 4090 - 172.17.31.112 Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/535.7+(KHTML,+like+Gecko)+Chrome/16.0.912.77+Safari/535.7 200 0 0
2012-02-14 17:44:37 W3SVC1785071458 172.17.31.254 GET /download.php f=06304844-1A.pdf&t=av 4090 - 172.17.31.112 Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/535.7+(KHTML,+like+Gecko)+Chrome/16.0.912.77+Safari/535.7 200 0 0

我在 Firefox、Opera 和 IE (6-9b) 中测试过,结果是一样的。

更新两次

将整个 download.php 文件放在这里:

<?php
session_start();
require("cgi-bin/auth.php");

// Don't timeout when downloading large files
@ignore_user_abort();  
@set_time_limit(0);  

//error_reporting(E_ALL); 
//ini_set('display_errors',1);

function getfile() {
    require('cgi-bin/connect_db_pdf.php');
    //Verify information 
    if (!isset($_GET) || !isset($_GET['f']) || !isset($_GET['t'])) {
        echo "Nothing to do!";
        exit(0);

    }

    //Update variables
    $vuname = strtolower(trim($_SESSION['uname']));
    $file = trim($_GET['f']); //Filename we're looking for
    $type = trim($_GET['t']);//Filetype

    if (!preg_match('/^[a-zA-Z0-9_\-\.]{1,60}$/', $file) || !preg_match('/^av|ds|cr|dp$/', $type)) {
        echo "Non conforming values";
        exit(0);
    }

    try {

        $sQuery = "SELECT * FROM pdf_info WHERE PDF_name=:file AND PDF_type=:type";

        $statm = $conn->prepare($sQuery);
        $statm->execute(array(':file'=>$file,':type'=>$type));
        $result = $statm->fetch();
        $count = $statm->rowCount();
        $sQuery = null;
        $statm = null;

        if ($count == 1 ){ //File was found in the database so let them download it. Update the time as well

            $sQuery = "INSERT INTO access (PDF_name,PDF_type, PDF_time, PDF_access) VALUES (:file, :type, GetDate(), :vuname)";

            $statm = $conn->prepare($sQuery);
            $statm->execute(array( ':vuname'=>$vuname, ':file'=>$file, ':type'=>$type));
            $count = $statm->rowCount();
            $sQuery = null;
            $statm = null;

            $sQuery = "UPDATE pdf_info SET last_view=GetDate(),viewed_uname=:vuname WHERE PDF_name=:file AND PDF_type=:type";

            $statm = $conn->prepare($sQuery);
            $statm->execute(array( ':vuname'=>$vuname, ':file'=>$file, ':type'=>$type));
            $sQuery = null;
            $statm = null;

            //$result is from FIRST SELECT query outside this 'if' scope.
            $file_loc = $result['floc'];
            $file_name = $result['filename'];

            $fileh = fopen($file_loc,'rb');//Send content to browser as inline PDF
            header("Content-Type: application/pdf"); 
            header("Pragma: no-cache");  
            header("Cache-Control: must-revalidate, post-check=0, pre-check=0");  
            header("Content-Length: " . filesize($file_loc));  
            header("Accept-Ranges: bytes");
            header("Content-Disposition: inline; filename={$file_name}");  

            while (!feof($fileh)) { 
                echo(@fgets($fileh, 8192)); 
            } 

            fclose ($fileh); 
            exit(0);

            } else { //We did not find a file in the database. Redirect the user to the view page.
                header("Location: view.php");
            }

            }   catch(PDOException $err) {//PDO SQL error. 
            //echo $err;
            header('Location: error.php');
            exit(0);
         }
}

getfile();

?>

【问题讨论】:

  • 请发布更多代码。如果你得到多个插入,很可能你的脚本被多次调用,要么是因为错误的逻辑(例如,当帖子尚未提交时),要么是因为有错误的重写规则。
  • 在这个脚本运行时观察你的服务器的访问日志,看看你是否真的点击了几次。
  • 如果你只做一个简单的测试文件,其中只有与 INSERT 相关的代码,会发生什么?
  • @Michael 我在重新启动 IIS 后检查了服务器日志。该脚本实际上调用了 download.php 两次。如果$_GET 变量未设置或不是预期值,则脚本退出。然后逻辑进入文件信息的检索和下载。为什么这会导致下载脚本或我的函数多次执行?
  • @PenguinCoder 请贴出插入代码之前出现的代码,检查$_GET并退出的部分。

标签: php sql-server pdo


【解决方案1】:

您评论说下载脚本被调用了两次,如果没有找到正确的值,则应该退出。情况可能并非如此,因为您滥用 PDOStatement 的 rowCount() 来测试找到的文件。此方法旨在为 DELETE、UPDATE 或 INSERT 语句返回受影响的行数,但不是 SELECT

如果关联 PDOStatement 执行的最后一条 SQL 语句是 SELECT 语句,则某些数据库可能会返回该语句返回的行数。 但是,并非所有数据库都可以保证此行为,并且不应依赖于可移植应用程序。 http://php.net/manual/en/pdostatement.rowcount.php

他们继续:

对于大多数数据库,PDOStatement::rowCount() 不会返回受 SELECT 语句影响的行数。相反,使用 PDO::query() 发出 SELECT COUNT(*) 语句,其谓词与预期的 SELECT 语句相同,然后使用 PDOStatement::fetchColumn() 检索将返回的行数。

如果您两次调用 download.php 脚本 -- 1 次使用不正确的文件值(恰好通过了您的正则表达式测试)和 1 次使用正确的值,即使您只是下载,也很可能插入两次一次。

【讨论】:

  • 我将$result 更改为$result = $statm-&gt;fetchAll();,将$count 更改为$count = count($result);,并为每个命名绑定参数指定了一个唯一名称。这样,访问表的 INSERT 只发生一次。但是,pdf_info 的更新永远不会发生,并且浏览器会挂起并且永远不会传递 PDF 文件。很可能是其他一些逻辑中的错误,并且与此问题无关。
  • 我所做的更改给出了预期的结果,其中带有注释掉的 while 循环用于 PDF 下载。这样,如果发生任何错误,我就可以打印出来。删除围绕while 循环的评论,INSERT 再次发生不止一次。我的 PHP/标头逻辑中似乎有一个错误,与这个问题无关。
【解决方案2】:

如果您使用 Apache 的 mod_rewrite,页面实际上会加载两次。例如,在您的索引页面上使用以下代码:

<?php

  session_start();

  if (!isset($_SESSION['id']))
  {
    $_SESSION['id'] = 1;
  }

  else
  {
    $_SESSION['id'] += 1;
  }

  echo $_SESSION['id'];

?>

如果不使用重定向,每次刷新时输出都会增加 1。现在添加一个带有以下内容的 .htaccess 文件:

<IfModule mod_rewrite.c>
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php
</IfModule>

每次刷新都会使页面增加 2。我使用单个 INSERT 查询尝试了相同的操作,并且发生了同样的情况:使用 .htaccess,插入了两行;没有,插入一行。

所以,如果您使用的是 mod_rewrite 并且它会影响 download.php,那么我相信这就是问题所在。

【讨论】:

  • 感谢回复,但此服务器是 IIS 服务器,没有任何 URL 重写支持。
  • 啊,你说你使用使用Apache。很抱歉没有正确阅读。
  • 尽管如此,您是否尝试过复制我上面的$_SESSION 增量代码示例(作为一个独立文件以保持简单)?可能是您的服务器设置的其他原因导致页面加载两次。如果它每次增加两个或更多,那么您就知道问题不在于您的 PHP 代码。
  • 使用增量变量测试您的 PHP 代码,每次我点击该页面时,该值都会增加 1。在 150 次点击中,变量会增加适当的值 1。所以在这个测试之后,会指向那个 download.php 脚本被错误的 PHP 代码逻辑执行了两次?
【解决方案3】:

提示:从所有 PHP 文件的末尾删除 ?&gt;。有时您最终会在其后得到一个不可见的空格或换行符,并且当输出像 PDF 这样的二进制格式时,它会成为 PDF 的一部分并使其损坏。 (因此浏览器/PDF 阅读器挂起。)

【讨论】:

  • 我猜,但您的双重获取问题可能与此事件顺序有关:1) 您单击该链接,这将打开一个新的浏览器窗口,该窗口开始从您的脚本加载。 2) 浏览器获取标题并理解应使用 PDF 阅读器读取文件。 3) 浏览器关闭连接,打开一个 PDF 阅读器,输入 url。 4) PDF阅读器运行脚本并再次下载文件。
  • 在我修复脚本中的另一个错误后,PDF 文件不再挂起。但是,为了测试您的答案的有效性,我从所有文件中删除了 ?&gt;。这会导致一些 PHP 文件未正确加载(内部 500 个),并且与原始问题一样,双文件命中。
  • 关于?&gt;,这很奇怪,只要您只是删除文件中的最后一个就可以了。在此处阅读更多信息:php.net/manual/en/…
猜你喜欢
  • 2012-06-09
  • 2012-02-16
  • 1970-01-01
  • 2012-07-06
  • 2018-05-14
  • 2023-04-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-03
相关资源
最近更新 更多