踩到两只“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 ...
- quartz源码解析(一)
本文的起因源于一次quartz的异常,在win2003正常运行的程序放在linux环境就抛出异常了,虽然找出异常没花我多长时间,不过由此加深了对quzrtz的了解:古人说,三折肱,为良医,说明经验对于 ...
- Ubuntu12.04 使用中遇到的问题
这个随笔回记录使用Ubuntu遇到的一些问题 不定期进行整理和分类 1.Question: ubuntu 无法检测包或者源码包 Description:Ubuntu软件中心打开时报错 无法检 ...
- hashCode()和equals()的用法
使用hashCode()和equals() hashCode()和equals()定义在Object类中,这个类是所有java类的基类,所以所有的java类都继承这两个方法. hashCode()方法 ...
- 关于 jquery cookie的用法
东钿微信公众平台新版上线 需要一个引导用户操作步骤.设置一个cookie师傅偶第一次访问此页面 .如果是则跳出用户引导,如果不是,正常显示. 一开始在百度了一段jquery cookie插件,也没仔细 ...
- Python 3.2: 使用pymysql连接Mysql
在python 3.2 中连接MYSQL的方式有很多种,例如使用mysqldb,pymysql.本文主要介绍使用Pymysql连接MYSQL的步骤 1 安装pymysql · ...
- EasyUI 下拉列表联动
//绑定部门.人员下拉菜单项 function BindDdl() { var $ddlbm = $("#ddlBm");//部门下拉列表 var $ddlry = $(" ...
- [IoC容器Unity] :Unity预览
1.引言 高内聚,低耦合成为一个OO架构设计的一个参考标准.高内聚是一个模块或者一个类中成员跟这个模块或者类的关系尽量高,低耦合是不同模块或者不同类之间关系尽量简单. 拿咱国家举例来说,假如你是中国人 ...
- JAVA 正则表达式 (超详细)
(PS:这篇文章为转载,我不喜欢转载的但我觉得这篇文章实在是超赞了,就转了过来,这篇可以说是学习JAVA正则表达的必读篇.作者是个正真有功力的人,阅读愉快) 在Sun的Java JDK 1.40版本中 ...
- Amazon DynamoDB 概览
1. 什么是Amazon DynamoDB DynamoDB 是一种快速.全面受管的 NoSQL 数据库服务,它能让用户以简单并且经济有效地方式存储和检索任何数据量,同时服务于任何程度的请求流量.所有 ...