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