Yii2框架中一些折磨人的坑


Posted in PHP onDecember 15, 2019

说点闲话

距离上次写博客,已经有一年了。在动手写之前,总是带着深深的罪恶感。被它折磨许久,终于,还是,动手了。

值得庆祝的一件事:最近开始健身了。每天动感单车45分钟,游泳45分钟,真的是(生)爽(不)到(如)爆(死)。

好了,扯淡完毕,步入正题。

ActiveRecord被莫名写入?

准备知识

ActiveRecord的基本用法。如果不理解,可参考这里。

代码现场

/**
 * @property integer $id
 * @property string $name
 * @property string $detail
 * @property double $price
 * @property integer $area
 **/
class OcRoom extends ActivieRecord
{
 ...
}

$room = OcRoom::find()  //先取出一个对象。
 ->select(['id'])  //只取出'id'列
 ->where(['id'=>20])
 ->one();
$room->save();    //保存,会发现此行的其它字段都被写成默认值了。

总结问题

这个例子的问题在于:

  1. 我从数据库中取出了一行,也就是代码中的$room,但是只取出了id字段,而其他字段自然就是默认值。
  2. 当我$room->save()的时候,那些是默认值的字段也被保存到数据库里去了。what!?
  3. 也就是说,当你想节约资源,不取出所有字段的时候,一定要注意不能保存,否则,很多数据会被莫名修改为默认值。

解决方法

然而,我们有什么解决办法呢?提供几种思路:

  1. 自己时刻注意,避免未完全取出的ActiveRecord的保存。
  2. 修改或继承ActiveRecord, 使得,当此对象由find()新建,且字段没有完全取出,调用save()方法,抛出异常。
  3. 修改或继承ActiveRecord,使得,当此对象由find()新建,且字段没有完全取出,调用save()方法时,只保存取出过的字段,其他字段被忽略。

你的Transaction生效了吗?

代码现场

/**
 * @property integer $id
 * @property string $name
 **/
class OcRoom extends ActiveRecord
{
 public function rules()
 {
  return [['name','string','min'=>2,'max'=>10]];
 }
 ...
}
class OcHouse extends ActiveRecord
{
 public function rules()
 {
  return [['name','string','max'=>10]];
 }
 ...
}

$a = new OcRoom();
$a->name = '';    //name为空字符串,不满足rules()条件。

$b = new OcHouse();
$b->name = '我的房间';   //name合法,可以保存。

$transaction = Yii::$app->db->beginTransaction();
try{
 $a->save();    //name字段不合法,无法验证通过,在validate()阶段已经返回false,不会进行数据库存储的步骤,所以也不会抛出异常。
 $b->save();    //name字段合法,可以正常保存。

 $transaction->commit(); //提交后,发现$a保存失败,而$b保存成功。
}
catch (Exception $e) 
{
 Yii::error($e->getTraceAsString(),__METHOD__);
 $transaction->rollBack();
}

问题总结

这段代码的问题在于:

  1. 大家知道$transaction的存在意义是保证整段数据库存储代码要么全成功,要么全失败。
  2. 显然,在这个例子中,transaction并没有达到我们想要的效果:$a因为validate()都没过,所以$transation->commit()的时候并不会报错。

解决方法

在$transation块内,所有的save()都要判断下返回值,如果为false,则直接抛出异常。

'Y-m-d'不被识别?

代码现场

OcRenterBill extends ActiveRecord
{
 public function rules()
 {
  return [
   ['start_time','date','format'=>'Y-m-d'],
  ];
 }
}

$a = new OcRenterBill();
$a = '2015-09-12';
$a->save();     //会报错,说格式不对

问题总结

如果一开始,Yii框架就报错,这个还不算坑。坑的是我在Mac上开发时,这个可以完全正常的工作,而发布到线上环境(Ubuntu)后,就弹出“属性start_time格式无效”的错误。而参考官方文档,发现这种格式是允许的官方文档。

啊啊啊。各种试错,最后发现如果改成php:Y-m-d,世界就清净了。所以,如果你遇到这种问题,感激我吧。

内存泄露

代码现场

public static function actionTest() {
  $total = 10;
  var_dump('开始内存'.memory_get_usage());
  while($total){
   $ret=User::findOne(['id'=>910002]);
   var_dump('end内存'.memory_get_usage());
   unset($ret);
   $total--;
  }
 }

上面代码的内存一直在增长, 按照原本想法来看, 变量被释放了,内存就算增长也不会一直增长。因为每循环一次内存都会被释放。

分析问题 上面这段代码涉及到了数据库的操作,而我们知道,数据库的很多地方都能引起内存泄漏。 所以先屏蔽数据库相关操作, 我手写了一个原生的数据库查询操作, 发现内存正常,没有问题。

$dsn = "mysql:dbname=test;host=localhost";
$db_user = 'root';
$db_pass = 'admin';
//查询
$sql = "select * from buyer";
$res = $pdo->query($sql);
foreach($res as $row) {
 echo $row['username'].'<br/>';
}

这时候答案呼之欲出--- 是yii2框架搞了鬼

定位问题 既然知道了是yii2 框架的问题那就可以进一步缩小问题。

public static function actionTest() {
  $total = 10;
  var_dump('开始内存'.memory_get_usage());
  while($total){
   $ret= new User();
   var_dump('end内存'.memory_get_usage());
   unset($ret);
   $total--;
  }
 }

内存还是一直增长。 这时候我测试了一个其他的yii2类 发觉内存不增长了。 这就可以联想到是在new 对象的时候yii2内部自己执行了什么操作,然后导致内存泄漏。 什么方法是new 的时候就执行的呢。。。 对的 构造方法 __construct 。 然后 我一步一步的从model 查到object 发觉都没有能引起泄漏的地方。

这个时候我们不妨换个思路, 既然是yii2框架下出现的泄漏, 那肯定就是yii2独有的功能, 那什么功能是yii2独有的,又是在new 对象的时候就会执行的呢?

行为(Behavior) 发觉我的模型类里面果然有用了行为

public function behaviors()
 {
  return [
   TimestampBehavior::class,
  ];
 }

最普通不过的代码。 我们知道 行为最后调用的地方是 yii\base\Component->attachBehaviors 最后定位到

private function attachBehaviorInternal($name, $behavior)
 {
  if (!($behavior instanceof Behavior)) {
   $behavior = Yii::createObject($behavior);
  }
  if (is_int($name)) {
   $behavior->attach($this);
   $this->_behaviors[] = $behavior;
  } else {
   if (isset($this->_behaviors[$name])) {
    $this->_behaviors[$name]->detach();
   }
   $behavior->attach($this);
   $this->_behaviors[$name] = $behavior;
  }
 
  return $behavior;
 }

我们观察这段代码,发觉他把自己传进去了$behavior->attach($this); 最后调用的是 yii\base\Behavior->attach

public function attach($owner)
 {
  $this->owner = $owner;
  foreach ($this->events() as $event => $handler) {
   $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
  }
 }

问题总结

这个时候答案已经呼之欲出, Yii2为了实现行为这一功能, 把自身this传进去,以便能注册事件、触发事件、解除事件。 这就导致了一个循环引用的问题。 所以导致对象refcount一直不为0 一直回收不了。

接下来就好办了。将查询换成原始的连接试试。果然,内存上升的非常慢了,可以说这才是正常现象。现在的内存也就是50m左右,cpu也稳定在7%左右。

代码优化后,再跑脚本,1分钟左右吧,脚本就跑完了。重点是不会再报出内存错误了。所以,以后考虑问题还是要深入。敢于质疑。以后如果遇到这种内存错误,一定要先检查自己的代码是不是有内存泄漏的地方。不要想着先设置php的内存。这样只会治标不治本。

总结

1、从开发速度方面,借助于gii脚手架,可以快速生成代码,也就是说搭建一个可以增删改查的系统可能一行代码都不用写,而且集成了jquery和bootstrap,特效和样式基本也不需要写了,这对于设计和审美能力普遍较差的后端程序员来说简直是一大福利。不过在前后端完全的分离的趋势下,Yii2前后端的耦合的还是有些重了。

2、从代码的可读性方面,Yii不会为了刻板地遵照某种设计模式而对代码进行过度的设计。基本上类在IDE里不借助第三方组件是可以跳转阅读源码的。这点上Yii要比Laravel略胜一筹。

3、从开源生态圈方面,Yii因为人少,稍微偏门一点的资料就很少,需要强大的谷歌能力和阅读英文文档的能力。

不可否认,Yii是一个优秀的开发框架,值得PHP开发者上手学习,踩坑的过程也是一种成长与积累。最后祝愿PHP小伙伴们都健健康康,事业有成。

PHP 相关文章推荐
中国站长站 For Dede4.0 采集规则
May 27 PHP
生成php程序的php代码
Apr 07 PHP
基于PHP的cURL快速入门教程 (小偷采集程序)
Jun 02 PHP
解析Ubuntu下crontab命令的用法
Jun 24 PHP
php汉字转拼音的示例
Feb 27 PHP
PHP SPL标准库之数据结构栈(SplStack)介绍
May 12 PHP
PHP图像处理类库及演示分享
May 17 PHP
php提取身份证号码中的生日日期以及验证是否为成年人的函数
Sep 29 PHP
ThinkPHP3.2.2实现持久登录(记住我)功能的方法
May 16 PHP
php reset() 函数指针指向数组中的第一个元素并输出实例代码
Nov 21 PHP
PHP使用PDO实现mysql防注入功能详解
Dec 20 PHP
PHP笛卡尔积实现原理及代码实例
Dec 09 PHP
PHP防止sql注入小技巧之sql预处理原理与实现方法分析
Dec 13 #PHP
PHP设计模式之外观模式(Facade)入门与应用详解
Dec 13 #PHP
PHP设计模式之装饰器(装饰者)模式(Decorator)入门与应用详解
Dec 13 #PHP
laravel通用化的CURD的实现
Dec 13 #PHP
Vagrant(WSL)+PHPStorm+Xdebu 断点调试环境搭建
Dec 13 #PHP
phpstudy后门rce批量利用脚本的实现
Dec 12 #PHP
PHP设计模式之数据访问对象模式(DAO)原理与用法实例分析
Dec 12 #PHP
You might like
地球防卫队:陪着奥特曼打小怪兽的人类力量 那些经典队服
2020/03/08 日漫
56.com视频采集接口程序(PHP)
2007/09/22 PHP
写一段简单的PHP建立文件夹代码
2015/01/06 PHP
php实现随机显示图片方法汇总
2015/05/21 PHP
php使用正则表达式去掉html中的注释方法
2016/11/03 PHP
ThinkPHP5实现作业管理系统中处理学生未交作业与已交作业信息的方法
2016/11/12 PHP
php 使用curl模拟ip和来源进行访问的实现方法
2017/05/02 PHP
PHP+Ajax实现的博客文章添加类别功能示例
2018/03/29 PHP
Javascript入门学习资料收集整理篇
2008/07/06 Javascript
JavaScript入门教程(6) Window窗口对象
2009/01/31 Javascript
JavaScript中常见陷阱小结
2010/04/27 Javascript
jQuery去掉字符串起始和结尾的空格(多种方法实现)
2013/04/01 Javascript
jQuery.Highcharts.js绘制柱状图饼状图曲线图
2015/03/14 Javascript
javascript基础知识
2016/06/07 Javascript
JQuery为元素添加样式的实现方法
2016/07/20 Javascript
总结AngularJS开发者最常犯的十个错误
2016/08/31 Javascript
JavaScript学习总结之正则的元字符和一些简单的应用
2017/06/30 Javascript
使用vue-router beforEach实现判断用户登录跳转路由筛选功能
2018/06/25 Javascript
jquery实现上传文件进度条
2020/03/26 jQuery
H5 js点击按钮复制文本到粘贴板
2020/11/19 Javascript
Python 用户登录验证的小例子
2013/03/06 Python
Python从零开始创建区块链
2018/03/06 Python
Python实现爬虫设置代理IP和伪装成浏览器的方法分享
2018/05/07 Python
Face++ API实现手势识别系统设计
2018/11/21 Python
python退出命令是什么?详解python退出方法
2018/12/10 Python
pycharm双击无响应(打不开问题解决办法)
2020/01/10 Python
Tkinter中复选菜单是否被选中的判断与设置方式
2020/03/04 Python
django 解决model中类写不到数据库中,数据库无此字段的问题
2020/05/20 Python
英国建筑用品在线:Building Supplies Online(BSO)
2018/04/30 全球购物
基督教卡片、励志礼品、家居装饰等:DaySpring
2018/10/12 全球购物
莱德杯高尔夫欧洲官方商店:Ryder Cup Shop
2019/08/14 全球购物
应届生人事助理求职信
2013/11/09 职场文书
质量管理标语
2014/06/12 职场文书
电影建党伟业观后感
2015/06/01 职场文书
numpy数据类型dtype转换实现
2021/04/24 Python
Python集合set()使用的方法详解
2022/03/18 Python