踩到两只“bug”

  近期在修复ex和头儿的代码时,碰到两个特别点的bug,其实也不能称之为bug,非常简单的用法,稍不严谨点可能就出错了。

  第一个是in_array,大家都知道功能是检查一个值是否在数组中,第三个参数传入true是严格模式检查,比较的时候要求类型一致,问题就在这个严格,比如现在有这么个判断:

     

   可以猜猜是否有输出,结果让人大跌眼镜,竟然打印了。这个非严格模式很有点模糊,当然知道这里不会检查类型,比如官网或者手册上会举若干例子,最典型的就是,数组中有数字字符串,然后判断等值的整型数是否在数组中时,结果为真,或者只是大小写不同的字符串也行。这也还好说,比如这里的在检查时,会将字符串"cz"转为整型再与0比较,结果还是真就是true了。是不是php字符串转为整型为0才导致这种结果呢?貌似不是。比如下面:

       

  单个c字符在用它时仍没有转化为单个ASCII码值,仍然最后转化成了0,所以下面的也打印了

                 

  因此非严格模式的in_array所做的检查,比如对于数字和字符串之间大概就是,先强转为数值型,然后进行数值型之间的比较。转换成数值时采用类似intval的方法,以字符串第一个出现的数字开始往后找到数字字符串的最大长度,转为等值数字,如果字符串第一个是字母,转为数值则为0。所以如果检查的数组中不小心有了元素0会是个定时炸弹,任何第一个不为数字的字符串过来都是真,说不定哪天挂(偶就花了几小时走ex的逻辑找漏洞,而且不止一处-_-#)。并且在这个函数中能在非严格模式下转化为0的类型太多了,如null、false、''、""、array()等等,php手册官网的注释部分也有老外写了几个测试,可以看看。

   第二个bug是关于PDO驱动的lastInsertId方法。问题在我要执行一个事物,插入一张表,更新两张表,插入时成功则写入缓存,在客户端上执行这个操作时第一次总是失败,第二次到第n次又是成功,让人纳闷。我们知道在插入数据表中一行数据时,理论上lastInsertId()应该返回上次插入的id号,但是不是总成功呢?不得不说包括我的头儿也有点想当然。先看看PDO驱动的lastInsertId()的解释:, 原型 public string PDO::lastInsertId ([ string $name = NULL ] )。

  "Returns the ID of the last inserted row, or the last value from a sequence object, depending on the underlying driver. For example, PDO_PGSQL requires you to specify the name of a sequence object for the name parameter." 大意是返回上次插入行的ID,或者是一个序列对象的最后的值(不一定是ID号),这取决于底层驱动。比如对于PGSQL这种数据库,需要指定一个序列对象的属性名称,这个名称由传入的$name参数决定。它还有个注意事项:“This method may not return a meaningful or consistent result across different PDO drivers, because the underlying database may not even support the notion of auto-increment fields or sequences.” 大意是,对于不同的驱动这个方法可能不会返回一个有意义的或连续的结果,因为底层驱动可能甚至都不支持一个自增的字段或者序列。

  比如说我在机子上随便建一张表test1

      

  然后来个测试脚本

<?php
    $dsn = 'mysql:dbname=test;host=localhost';
    $user = 'root';
    $pass = '1234';

    $pdo = new PDO($dsn, $user, $pass) or die('connect failed');
    $sql = 'insert into test1(num) values(?)';
    $statement = $pdo->prepare($sql);  // 准备语句
    $ret = $statement->execute(array(6)); // 执行查询
    $lastId = $pdo->lastInsertId();  // 获取上次插入ID

    echo 'statement=><pre>'; var_dump($statement);
    echo 'ret=><pre>'; var_dump($ret);
    echo 'lastId=><pre>'; var_dump($lastId);

  看看效果,数据库中是有记录的

      

  如果这时以lastInsertId()作为返回结果就是有问题的(当时我还以为是PDO的bug...>3<),也许眼尖或者有过类似经历的人可能已经看到,上面创建这张表时,我没有定义主键。不妨试试有主键的

      

  果然是主键导致的问题,在查查头儿建的这几张表,没有主键-_-#,而底层的读写数据库代码是共用一套的,所以是那里行,这里就是不行。

  有没有发现,用命令行操作数据库时,它总是返回受影响的行数,下面是对没有主键的表插入一行

    

  所以我的第一次插入数据是成功的,表中也有,但最终结果失败(返回的是插入和更新三个操作返回结果的并),第一次就写入了混村,而第二次、三次读的是缓存,没有插入操作,所以是成功的。因此我想修改底层代码吧,让它返回受影响的函数,但新的问题又出现了,有一种情况是返回受影行数为0但是执行仍然是成功的,导致我的事物结果还是挂掉。看看下图的过程

    

  插入一条数据,找到id号,再更新它,我做的是原本原样的更新,也就是相当于没有更新,可以清楚看到他的受影响函数是0,但我确实执行了更新操作,没有任何问题。用php测试打印受影响行数确实也是0。什么时候会出现这种情况,比如客户端上有个更新用户信息按钮,里面有一些名称、出生年月日,更新时间等信息,用户无意点进来,信息啥都没改,点了个保存,而且连续、快速的点击两次,两次间隔不超过1秒,所以在更新表时,那个更新信息的时间字段(timestamp类型)实际是一样的,因为受影响行数为0,所以最后结果报错,本来是成功执行了,却又弹出了个不友好的提示框“您的信息保存失败xxx”,挺不雅。测试人员连这样诡异的错误都能抓到,每次他们兴冲冲往这边跑时我就知道没有好事-_-#!

   问题当然要解决,偶这套框架是基于PDO的查询,都是prepare返回一个PDOStament对象,然后execute传入数组参数,执行sql语句。在上面打印的结果不知你看到没,无论怎样,只要执行是成功的,PDOStament这个对象总是完整的,且execute执行的结果总是真的,从这里入手,下面的代码精简写的,极不严格,权作消遣

<?php
    /**
     * database class 
     */
    class MyPDO
    {
        private $pdo = null;  // PDO Class Object

        private $config = array('dsn'=>'', 'user'=>'root', 'password'=>''); // config info to connect db

        private $i = '`'; // field quote

        public function __construct($dbname, $host = '127.0.0.1', $user, $password = '', $i = '`')
        {
            $this->config['dsn'] = "mysql:dbname={$dbname};host={$host}";
            $this->config['user'] = $user;
            $this->config['password'] = $password;
            $this->i = $i;

            $this->connect(); // connect db
        }

        /**
         * connect db
         */
        private function connect()
        {
            if(!$this->pdo)
            {
                try
                {
                    $this->pdo = new PDO($this->config['dsn'], $this->config['user'], $this->config['password']);  
                    $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // set the error mode, if there's a error, throw an exception
                }
                catch(Exception $e)
                {
                    echo 'ERROR: '.$e->getMessage();
                    exit;
                }
                
            }
        }

        /**
         * insert data into table
         *
         * @param string $table the table name
         * @param array $data parameters to be inserted into,should be 'column'=>value 
         * @param bool $isReturn a choice to return result
         * @return int
         */
        public function insert($table, $data = array(), $isReturn = false)
        {
            if(!$table || !$data) return 0;

            $sql = $this->insert_sql($table, $data); 
            $ret = $this->query($sql, array_values($data), $isReturn);
            return $ret ? ($isReturn ? $ret : $this->pdo->lastInsertId()) : null;
        }

        /**
         * update  table
         *
         * @param string $table the table name
         * @param array $data parameters to be updated,should be 'column'=>value 
         * @param bool $isReturn a choice to return the result
         * @return int
         */
        public function update($table, $data = array(), $where = array(), $isReturn = false)
        {
            if(!$table || !$data) return 0;
            $i = $this->i;
            $columns = rtrim(implode("{$i} = ?, {$i}", array_keys($data)), $i);
            $sql = "UPDATE {$table} SET {$i}{$columns}{$i} = ? WHERE ";

            list($where, $params) = $this->where($where);
            
            // append where clause and execute query
            if($stmt = $this->query($sql . $where, $params, $isReturn))
            {
                return $isReturn ? $stmt : $stmt->rowCount();
            }

        }

        /**
         * generate an insert sql
         */
        private function insert_sql($table, $data = array())
        {
            $i = $this->i; 
            $columns = implode("$i, $i", array_keys($data));
            $items = rtrim(str_repeat('?, ', count($data)), ', ');
            return "INSERT INTO {$i}{$table}{$i} ($i" . $columns . "$i) VALUES(" . $items . ")";
        }

        /**
         * entrance for executing all request sql 
         */
        private function query($sql, $params, $isReturn = false)
        {
            if(!$this->pdo) $this->connect();

            $stmt = $this->pdo->prepare($sql);

            if($ret = $stmt->execute($params))
            {
                throw new Exception('execute sql error!');
            }

            return !$isReturn ? $stmt : $ret;
        }

        /**
         * generate where part in a whole sql
         */
        private function where($where = array())
        {
            if(!$where) return array();
            $i = $this->i;
            $cols = $vals = array();

            foreach($where as $key=>$val)
            {
                if(!empty($val))
                {
                    $cols[] = "{$i}$key{$i} = ?";
                    $vals[] = $val;
                }
                
            } 

            return array(implode(' AND ', $cols), $vals);
        }

    }

  大致流程是,insert方法,传入表名,插入的参数,和第三个参数isReturn,isReturn为真,且查询结果为真,则返回一个查询结果ret(实际是PDOStatement对象执行execute后所得),在该参数为假且执行结果为真时,则返回lastInsertId(),否则返回null。因此对于没有主键的表,只要传入第三个参数为true就应该不会出现上边的情况,有主键的表不需要传入这个参数直接调用。

  insert时先调insert_sql生成插入语句,再调用query执行这条语句,看看query方法,pdo属性成员执行prepare方法会返回一个$stmt变量(PDOStatement对象),最后如果isReturn为false则返回$stmt,如果为真返回execute执行后结果,所以只要语句正确,没有其他问题,这里返回结果总是为true,不管上次插入ID还是影响行数哪个为0。

  在执行update方法更新表时,需要调where方法生成where子句,然后执行query,如果isReturn传入false,query方法会返回PDOStatement对象变量$stmt,update方法返回rowCount(),即受影响行数;如果isReturn传入true,query方法返回$stmt执行execute后的结果,只要语句对应该没什么问题,update也兼顾了返回受影响函数这个量,基本就解决了问题。 :-)

   Happy April Fool's Day!

 

posted @ 2015-04-01 01:11  lazycat-cz  阅读(1038)  评论(4编辑  收藏  举报