数据库+模型 - AR模型查询优化

  • 作者:KK

  • 发表日期:2017.01.19


查询时声明with附加其它查询

以下查询会有明显的性能问题(代码来自官方文档示例)

// SELECT * FROM customer LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM order WHERE customer_id=...
    $orders = $customer->orders;  //这个orders是指一对多里的getOrders方法返回hasMany的值
	
    // ...处理 $orders 的更多事情...
}

以上代码示例了从100个客户中依次查询这些客户的订单集合再进行处理,这样for循环里就会有100次客户订单的查询,不能这么做,可以通过with这样来优化:

// SELECT * FROM customer LIMIT 100;
// SELECT * FROM orders WHERE customer_id IN (1,2,...)
$customers = Customer::find()->limit(100)->with('orders')->all();

foreach ($customers as $customer) {
    // 没有 SQL 语句被执行
    $orders = $customer->orders;
    // ...处理 $orders 的更多事情...
}

这样就能只执行2次SQL了


查询指定字段时的with注意事项

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
echo $orders[0]->customer; // 空的

不是with一起查出来了吗?为什么是空的呢?

因为with的附加查询里需要关联字段才可以,但是select指定的id和amount两个字段都不是与customer表发生关联的字段,所以在底层进行with附加的查询时,根本找不到关联字段,所以没查到相关数据

所以这里要注意的是,select要附加上关联字段,以下才是正确姿势:

$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

在with时追加其它条件

重复一下上面的代码,第2条SQL的where条件一直是in逻辑:

// SELECT * FROM customer LIMIT 100;
// SELECT * FROM orders WHERE customer_id IN (1,2,...)
$customers = Customer::find()->limit(100)->with('orders')->all();

那如果突然想要在in的同时又加上其它条件,就要在with里换一下参数的写法了:

// SELECT * FROM customer LIMIT 100;
// SELECT * FROM orders WHERE customer_id IN (1,2,...) AND status = 1
$customers = Customer::find()->limit(100)->with([
	'orders' => function($arQuery){
		//传来的参数是一个AR查询器,通过查询器提供的API来调整with查询的条件,比如andWhere
		$query->andWhere(['status' => 1]);
	}
])->all();

逆向关系定义

在学习这个之前先看这段代码:

$user1 = new User();
$user1->id = 1;
$user1->name = 'a';

$user2 = new User();
$user2->id = 1;
$user2->name = 'a';

var_dump($user1 !== $user2); //true

这是两个user对象的属性值一样而已,但根本就不是同一个内存对象变量(内存地址都不一样嘛),这种现象在某种AR模型的用法下会导致一些问题,下面说明一下


以下代码中,Customer通过getOrders定义了它有多个订单的关系,而Order也通过getCustomer定义了它有一个客户的关系

class Customer extends ActiveRecord
{
    ....
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    ....
    public function getCustomer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

可是这样用起来会有个问题:

// SELECT * FROM customer WHERE id=1
$customer = Customer::findOne(1);

// SELECT * FROM order WHERE customer_id=1
// SELECT * FROM customer WHERE id=1   这里又查了一次 id=1 的用户
if ($customer->orders[0]->customer === $customer) {
    echo '相同';
} else {
    echo '不相同'; // 会运行到这里,输出 "不相同"
}

在我们客观看来,两个customer是同一个东西才对,但实际上在程序里,findOne返回的是一个实例,人家的内存地址就是A吧,而$customer->orders[0]->customer这里查询关联数据时又返回了另一个实例,内存地址是B吧,虽然两个对象里的属性值相同,但是不是同一块内存,所以并不相等

最不希望的更加是它多了一个查询(SELECT * FROM customer WHERE id=1)

逆关系这个知识点就是为了解决这个问题的,做法很简单,在Customer->getOrders的hasMany里后追加inverseOf定义就行了:

class Customer extends ActiveRecord
{
    ....
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
    }
}

这里的inverseOf('customer')大概是指hasMany查出来的各个orders(Order模型实例)把这个模型里的getCustomer()这个getter对应的属性customer给查出来,只是这个查询其实并不真正执行,而是直接在当前这个Customer里取引用

则后面调用orders里的customer都不会造成多余的查询了