Difficulty managing cached relations.

If I have parametrised relations i.e. referring to the same table, I have to manage cached relations which becomes very difficult. The examples below are massively simplified (in actual code mutations and saving may be happening multiple times, across multiple classes, via events etc. all within a single execution). e.g.




class Customer extends ActiveRecord {


    // 'base relation'

    public function getOrders()

    {

        return $this->hasMany(Order::className(), ['id_customer' => 'id'])->inverseOf('customer');

    }


    // 'parametrised relation'

    public function getPaidOrders()

    {

        return $this->getOrders()->andWhere(['status' => Order::STATUS_PAID]);

    }


    // 'parametrised relation'

    public function getUnpaidOrders()

    {

        return $this->getOrders()->andWhere(['not', [status' => Order::STATUS_PAID]]);

    }

}



Issue example 1 using stale cached relations:




foreach ($customer->unPaidOrders as $order) {

   $order->status = Order::STATUS_PAID;

   $order->save();

} 


return $customer->unPaidOrders; // uses cached relation - gets orders now paid.



Issue example 2 using stale DB data on models not yet persisted:




foreach ($customer->unPaidOrders as $order) {

   $order->status = Order::STATUS_PAID;

   // assume cannot persist here

} 

$customer->orders; // queries DB getting unmutated data.



Solutions:

For example 1 issue (which is the one I come across more often):

  1. Unset the relation unset($customer->unPaidOrders), unset($customer->orders) – problem: I have to know which relation to unset and where which may break encapsulation across classes

  2. Refresh the parent $order->refresh() – costly when required to be done multiple times in an execution and in order not to break encapsulation, often superfluously.

  3. Don’t parametrise relations, instead have only one ‘base relation’ and filter the ‘base relation’, thus will always use same object in memory.




// 'base relation' (same as above)

public function getOrders()

{

    return $this->hasMany(Order::className(), ['id_customer' => 'id'])->inverseOf('customer');

}


public function getPaidOrders()

{

    return array_filter($this->orders, function ($order) {

        return $order->status == Order::STATUS_PAID;

    });

}  



With 3, the best solution, I however lose the convenience of an actual relation on any of the non-base relations (parametrised) yii\db\BaseActiveRecord:_related. I cannot perform with() or joinWith() on these ‘relations’; I cannot chain relation definitions

Any suggestions, feedback? As stated earlier, the code I’m dealing with is much more complex which is not at easy to express succinctly, so hopefully above does not trivialise the problem.