Skip to content

Relation Mapping

Romans Malinovskis edited this page Apr 18, 2016 · 1 revision

A common pattern in ORM is ability to traverse between table using a relation and receive array of related records. In typical implementation, when you execute $client->orders() you would get array with lazy-loaded orders. Sometimes they are not lazy-loaded but either way this operation is a potential landmine, as the user may have hundreds of thousands of records.

Our implementation for traversal is different:

$orders = $client->load(20)->ref('Order')->addCondition('isComplete', false);
$orders->tryLoadAny();

Here $clients is object of type "Model_Client". $orders is object of type "Model_Order". We use explicit loading to load individual client and then we execute ref.

This method will create instance of Model_Order and it will addCondition('user_id', 20); It will NOT actually execute any query.

You can then access orders of client with user_id=20 through active record if you want.

You might wonder - why two queries are necessary? This is because you explicitly loaded the client by id=20. There are few other ways.

First - the wrong way:

$orders = $db->add('Model_Order')
    ->addCondition('user_id', 20)
    ->addCondition('isComplete', false);
$orders->tryLoadAny();

The reason why you shouldn't use this approach is because user_id=20 may not belong to Client but can belong to Admin instead. We haven't loaded Model_Client so this logic was lost. The better way to do same thing would be:

$orders = $client
    ->addCondition('id', 20)
    ->ref('Order')
    ->addCondition('isComplete', false);
$orders->tryLoadAny();

Now ref('Order') will notice that $client is not loaded, so it will load all orders for clients that match the condition. This will generate query like this:

select * from `order` where user_id in (select id from user where id=20 and type="client")

There is actually a shorter syntax which you should use:

$orders = $client
    ->withID(20)
    ->ref('Order')
    ->addCondition('isComplete', false);
$orders->tryLoadAny();

This approach should work even if your "id" field is called differently in your database.

ref() works perfectly fine if your originating DataSet contains multiple records:

$orders = $client
    ->addCondition('is_online', true)
    ->ref('Order')
    ->addCondition('isComplete', false);
$orders->tryLoadAny();

The great thing about our relational mapping is that is_online can be an Expression too.

The final example will include double-traversal and will list discounted items from all the active orders by on-line clients:

$order_line = $client
    ->addCondition('is_online', true)
    ->ref('Order')
    ->addCondition('isComplete', false)
    ->ref('Order_Line');
$order_line->addExpression('has_discount')->set(
    $order_line->ref('Item')->getElement('has_discount')->type('boolean')
);
$order_line->addCondition('has_discount', true);

foreach($order_line as $line) {
  echo "Discounted item: ".json_encode($order_line->get())."\n";
}

The above code sample will perform only a single query.

So far we have been working with Business Models. I showed SQL code which is generated just to point out query efficiency. Next section on Persistence will explain how your Domain Model logic is converted into queries and how can you tweak it.