fralbo
fralbo

Reputation: 2664

How to select only those records that have at least one associated record?

I have the following associations: Orders belongsTo Sites

I execute the following query:

$sites = $this->Sites->find('all')
->select(['id', 'name'])
->contain([
        'Users' => function ($q) {
                return $q->select(['id', 'owner_id', 'firstname', 'lastname']);
        },
        'Orders' => function ($q) {
                return $q->where(['status >' => 0]);
        },
        'Orders.Agplans'
])
->matching('Users', function ($q) use($owner_id)
{
    return $q->where([
        'Users.owner_id' => $owner_id
    ]);
})
->all();

But most of the sites have no orders so I get some kind of results:

(int) 100 => object(App\Model\Entity\Site) {
    'id' => (int) 7966,
    'name' => 'Site example',
    'orders' => [],
    'users' => [
        (int) 0 => object(App\Model\Entity\User) {

    ...

Is it possible to specify in the query that I only want sites with not empty orders?

Upvotes: 0

Views: 776

Answers (1)

ndm
ndm

Reputation: 60463

Since what you have there is a hasMany association, you'll have to take measures additionally to containing Orders.

Use an inner join

You could use an INNER join, that would select only those rows where a matching order is present. For filtering purposes, this can easily be done using Query::innerJoinWith(), which is similar to Query::matching(), but it won't load any results.

$sites = $this->Sites
    ->find('all')
    ->select(['id', 'name'])

    // ...

    ->innerJoinWith('Orders', function ($q) {
        return $q->where(['Orders.status >' => 0]);
    })
    ->group(['Sites.id']) // may or may not be necessary
    ->all();

Use matching like you already do

You are already using Query::matching() for Users, so I'd assume that you know what it does... filtering. Adding a further matching for Orders is an option too, it will however fetch additional data.

$sites = $this->Sites
    ->find('all')
    ->select(['id', 'name'])

    // ...

    ->matching('Orders', function ($q) {
        return $q->where(['Orders.status >' => 0]);
    })
    ->group(['Sites.id']) // may or may not be necessary
    ->all();

Last but not least, use a counter cache

Using a properly indexed counter cache is probably the best performing option, as filtering would be done via a column in the same table.

You'd define it in your Orders model with the proper conditions, and then just use where() in your query, like

$this->addBehavior('CounterCache', [
    'Sites' => [
        'active_orders_count' => [
            'conditions' => ['Orders.status >' => 0]
        ]
    ]
]);
$sites = $this->Sites
    ->find('all')
    ->select(['id', 'name'])

    // ...

    ->where([
        'active_orders_count >' => 0
    ])
    ->all();

See also

Upvotes: 6

Related Questions