AR(ActiveRecord)中查询额外字段

技巧库 · yiqing · 于 7年前 发布 · 11301 次阅读

AR中查询额外字段

AR 是用sql查询填充而来的 每个AR中的字段列表默认是跟其对应的表是一致的

如果要查询不属于自己表中的额外字段,并且想通过Ar来访问 此时有两种做法

  • 张冠李戴: 把查询上来的数据强行赋值给已经存在的属性
  • 声明额外的字段

例子

user 与 comment 为一对多关系

此时需要在查询user实体的基础上查询其对应的评论数,关于这个场景在yii中有个STAT关系是来处理的,但yii2中取消了这个关系。 如果要实现可以参考:yii2-activerecord-stat-relations

// You should simply use the same ActiveQuery, e.g. :
public function getOrders()
{
    return $this->hasMany(Order::className(), ['customer_id' => 'id']);
}

public function getOrdersCount()
{
    return $this->getOrders()->count();
}

对应上面的例子 就是需要查询用户 连带其评论数。

上面给出了两种解决方法:

  • 张冠李戴 : 就是随便找一个ar已经有的属性 把查询的评论统计数赋值给他 。 还有一种设计,user表中多一个额外字段就好了(comment_count),这个字段在评论增删时需要联动修改。 评论增加 ,递增此表的值,评论删除,递减此表。

    目前我还没有做评论的联动修改,此值为零。准确值是想在实际使用时通过子查询动态统计得来 。

  • 声明额外字段 假设User 类本身没有comment_count 字段,那么想在查询时赋值,可以声明此字段:

class User extends ActiveRecord
{
    /**
     * $var int $commentCount 额外字段 评论数
     */
     public $commentCount ;
}      

在查询(index,后者view中)通过query 来赋值此字段:

$model = User::findBySql(
        "SELECT user.* ,cmtSum.cmtAmount as commentCount
        FROM user
        LEFT JOIN (SELECT entity_id, SUM(entity_id) as cmtAmount FROM comment WHERE entity_type='User' GROUP BY entity_id) cmtSum
              ON cmtSum.entity_id = id
              WHERE id=:userId
        ",
        [':userId'=>$userId]
    )
    ->one();  

看到上面的用法的select 部分多了额外字段 commentCount,如果你的实体User 中没有这个属性,那么这个值会被丢弃的!!!

关于上面的查询写法,其实还有另外版本,可以参考: filter-sort-by-summary-data-in-gridview-yii-2-0; 悲催的是在这里上面的做法行不通,生成的最终sql是有问题的

$mainModelTableName = User::tableName();

$subQuery = Comment::find()
    ->select(['entity_id',' SUM(entity_id) as cmtAmount '])
    ->groupBy('entity_id')
    ->onCondition([
        'entity_type' => 'User',
    ]);

$model = User::find()
    ->where(['id' => $userId])
    ->select(["{$mainModelTableName}.*",  'cmtSum.cmtAmount as commentCount'])
    // 计算从表的评论数
    ->leftJoin(['cmtSum' => $subQuery], 'cmtSum.entity_id = id')
    ->one();

上面的做法只在postGres下有问题 ,没有测过sql是否可行!

最后的坑

上面的做法看似已经可以了 ,但实际在postgre情况下出了个问题!这个源于额外字段的大小写! 我们声明的是 commentCount , 但pg 好像对sql是大小写不敏感的,这样导致他最终的结果集是commentcount。

AR在填充自身时使用底层数据库返回的结果集来完成的,当碰到自身对应的表(user表)外的字段时对应的代码逻辑是:

// 位于BaseActiveRecord
public static function populateRecord($record, $row)
{
    $columns = array_flip($record->attributes());
    foreach ($row as $name => $value) {
        if (isset($columns[$name])) {
            $record->_attributes[$name] = $value;
        } elseif ($record->canSetProperty($name)) {
            // 注意这里!!
            $record->$name = $value;
        }
    }
    $record->_oldAttributes = $record->_attributes;
}
    
// 继续追踪 $record->canSetProperty
// 位于 yii\base\Model  
public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
{
   if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) {
       return true;
   } elseif ($checkBehaviors) {
       $this->ensureBehaviors();
       foreach ($this->_behaviors as $behavior) {
           if ($behavior->canSetProperty($name, $checkVars)) {
               return true;
           }
       }
   }
   return false;
}

可以看到虚拟属性 setXxx 或者自身声明的变量是可以被填充的 behavior也是会被检查的

我们自己不是声明了commentCount属性么 理论上是可以被填充的,但悲催的事情发生在:property_exists property_exists 这个方法时大小写敏感的,即

if (property_exists(User::className(), 'commontcount')) {
    die('1');
} else {
    die('0');
}

//   注意大小写
if (property_exists(Restaurant::className(), 'commentCount')) {
    die('1');
} else {
    die('0');
}

上面的测试得到不同结果!

在回到查询sql语句上 SELECT user. ,cmtSum.cmtAmount as commentCount 这里的语句实际会被postgre 忽略掉大小写 变为: SELECT user. ,cmtSum.cmtAmount as commentcount 这样结果集中就没有commentCount了!所以自然也不会被填充。

这样最后只有重命名为 蛇形名称:commentCount --> commont_count .

全文完!


补充: pg数据库大小写不区分的; 但可以通过添加双引号 强制其区分大小写

共收到 5 条回复 ar Yii2 relation
yiqing#17年前 0 个赞

关于张冠李戴没有具体提及,补充下 就是:

        
       SELECT user.* ,cmtSum.cmtAmount as xxx

上例的xxx 可以是user表中的字段 这个字段就是被强行赋值掉了

Simon#27年前 0 个赞

事实证明 SQL 中用驼峰命名就是个巨大的错误

forecho#37年前 0 个赞

@Simon #2楼 表示从来不用驼峰命令数据库字段 :joy:

Simon#47年前 0 个赞

@forecho #3楼 我那帅气拉轰屌炸天的头像呢?

forecho#57年前 0 个赞

@Simon #4楼 之前是读取的 gravatar.com 的头像,现在用自己的头像了,你可以去个人设置中再上传头像 :smiley:

添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册