踩到两只“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!
踩到两只“bug”的更多相关文章
- powershell中的两只爬虫
--------------------序-------------------- (PowerShell中的)两只爬虫,两只爬虫,跑地快,爬网页不赖~~~ 一只基于com版的ie,一只基于.net中 ...
- 微信小程序的两个BUG?
微信小程序的两个BUG,也许可能是我搞错了 1.wx.uploadFile 用循环上传图片的时候,电脑.苹果手机上都会正常,安卓机上面则会出现the same task is working的问题 2 ...
- VS 2013 Preview 自定义 SharePoint 2013 列表 之 两个Bug
SharePoint 2013 已RTM了,对于程序员来说又要了解新功能了,同时 VS 2013 也将要 RTM了,两者同时应用定会有不新功能,我们先从 自定义 列表开始. SharePoint 20 ...
- 洛谷P1518 两只塔姆沃斯牛 The Tamworth Two
P1518 两只塔姆沃斯牛 The Tamworth Two 109通过 184提交 题目提供者该用户不存在 标签USACO 难度普及+/提高 提交 讨论 题解 最新讨论 求数据 题目背景 题目描 ...
- 【USACO 2.4.1】两只塔姆沃斯牛
[题目描述] 两只牛逃跑到了森林里.农夫John开始用他的专家技术追捕这两头牛.你的任务是模拟他们的行为(牛和John). 追击在10x10的平面网格内进行.一个格子可以是: 一个障碍物, 两头牛(它 ...
- 发现了学校教务处官网的两个BUG
许久没有写博客了,感觉自己技术还差的好多-_-好像没啥好写的,之前学完了某易的WEB安全基础视频教程,自认对WEB安全入了门,忍不住就想拿学校教务处官网来练练手 教务处登录界面如图所示(为保护隐私,部 ...
- QuantLib 金融计算——修复 BatesProcess 中的两个 Bug
QuantLib 金融计算--修复 BatesProcess 中的两个 Bug 我发现了 BatesProcess 中的两个 Bug: 基类 HestonProcess::factors 的返回值取决 ...
- luogu P1518 两只塔姆沃斯牛 The Tamworth Two
luogu P1518 两只塔姆沃斯牛 The Tamworth Two 题目描述 两只牛逃跑到了森林里.农夫John开始用他的专家技术追捕这两头牛.你的任务是模拟他们的行为(牛和John). 追击在 ...
- 青蛙的约会 扩展欧几里得 方程ax+by=c的整数解 一个跑道长为周长为L米,两只青蛙初始位置为x,y;(x!=y,同时逆时针运动,每一次运动分别为m,n米;问第几次运动后相遇,即在同一位置。
/** 题目:青蛙的约会 链接:https://vjudge.net/contest/154246#problem/R 题意:一个跑道长为周长为L米,两只青蛙初始位置为x,y:(x!=y,同时逆时针运 ...
随机推荐
- [iOS UI进阶 - 2.2] 彩票Demo v1.2 UICollectionView基本
A.需要掌握的 设计.实现设置界面 cell的封装 UICollectionView的使用 自定义UICollectionView 抽取控制器父类 "帮助"功能 code sour ...
- arcgis下载
你懂的~ t.cn/RA4cc3k 密码ygdr 包含10.2全部,含有(亲测)字样表示测试过OK的,SP是从esri网站下载的几乎全部patch和sp,包括desktop.engine和sever: ...
- 用Emacs 写python了
之前都是用python 自带的IDLE 写 python 的,现在换了Emacs,感觉真是不错,爽. 截图留念: 用了sr-speedbar ,顿时有了IDE 的感觉,是不是很爽. 版权声明:本文为博 ...
- iOS开发-数据持久化
iOS中四种最常用的将数据持久存储在iOS文件系统的机制 前三种机制的相同点都是需要找到沙盒里面的Documents的目录路径,附加自己相应的文件名字符串来生成需要的完整路径,再往里面创建.读取.写入 ...
- 读取AD模拟分量
//EEPROM数据保存---------------------- #include <EEPROM.h> #define EEPROM_write(address, p) {int i ...
- BZOJ 2733: [HNOI2012]永无乡 启发式合并treap
2733: [HNOI2012]永无乡 Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://www.lydsy.com/JudgeOnline/pr ...
- OSG中的示例程序简介(转)
OSG中的示例程序简介 1.example_osganimate一)演示了路径动画的使用 (AnimationPath.AnimationPathCallback),路径动画回调可以作用在Camera ...
- C++中使用union的几点思考(转)
C++中使用union的几点思考 大卫注:这段时间整理旧资料,看到一些文章,虽然讲的都是些小问题,不大可能用到,但也算是一个知识点,特整理出来与大家共享.与此相关的那篇文章的作者的有些理解是错误的,我 ...
- <Android>关于EditText中setInputType和setSingleLine的冲突
近期自己开发了一个带有删除button的EditText,一方面须要设置为SingleLine,还有一方面又须要设置输入类型,起先在xml文件里设置了android:inputType类型,在自己定义 ...
- Android学习笔记(20)————利用ListView制作带竖线的多彩表格
http://blog.csdn.net/conowen/article/details/7421805 /********************************************** ...