• 踩到两只“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!

  • 相关阅读:
    iOS:分组的表格视图UITableView,可以折叠和展开
    iOS:带主标题、副标题、图像类型的表格视图UITableView
    iOS:多个单元格的删除(方法二):
    iOS:多个单元格的删除(方法一)
    iOS:UITableViewCell自定义单元格
    iOS:删除、插入、移动单元格
    iOS:UITableView表格视图控件
    iOS:UIImageView图像视图控件
    iOS:UIScrollView控件和UIPageControl控件的详解
    淘宝卖家工具推荐
  • 原文地址:https://www.cnblogs.com/lazycat-cz/p/4378830.html
Copyright © 2020-2023  润新知