I'm trying to make query builder to output the following MySQL query:
SELECT p1.id, p1.product_id, p1.updated_at
FROM tbl_scrape_data p1
INNER JOIN
(
SELECT max(updated_at) MaxDate, product_id
FROM tbl_scrape_data
WHERE product_id IN (1,2,3)
GROUP BY product_id
) p2
ON p1.product_id = p2.product_id
AND p1.updated_at = p2.MaxDate
WHERE p1.product_id IN (1,2,3)
order by p1.updated_at desc
Here's what I tried:
$scrapeData = (new Query() )
->select(['p1.product_id', 'p1.id', 'p1.updated_at'])
->from('tbl_scrape_data p1')
->innerJoin([
'p2' => (new Query)
->select(['MAX(updated_at) MaxDate', 'product_id' ])
->from('tbl_scrape_data')
->where([ 'product_id' => [1, 2, 3, 15, 4] ])
->groupBy('product_id'),
//->all(),
['p1.product_id' => 'p2.product_id', 'p1.updated_at' => 'p2.MaxDate']
])
->where([ 'p1.product_id' => [1, 2, 3, 15, 4] ])
->orderBy('p1.updated_at DESC')
->all();
Yii2 throws an error trying to perform this query. Can someone tell me if this is a Yii2 bug or if I'm missing something? Or maybe the way I formatted the query builder is wrong?
I'm using Yii 2.0.1 and the error is
strpos() expects parameter 1 to be string, array given
\vendor\yiisoft\yii2\db\QueryBuilder.php at line 715
The reason of the error is in this section of code:
->innerJoin([
'p2' => (new Query)
->select(['MAX(updated_at) MaxDate', 'product_id' ])
->from('tbl_scrape_data')
->where([ 'product_id' => [1, 2, 3, 15, 4] ])
->groupBy('product_id'),
//->all(),
['p1.product_id' => 'p2.product_id', 'p1.updated_at' => 'p2.MaxDate']
])
Instead of passing two parameters - table and on separately you actually passing them in one parameter - array.
Also the first parameter (table) should be array with one element.
In other words, the placement of square brackets is incorrect.
So here is correct code of the INNER JOIN section:
->innerJoin(
['p2' => (new Query)
->select(['MAX(updated_at) MaxDate', 'product_id' ])
->from('tbl_scrape_data')
->where([ 'product_id' => [1, 2, 3, 15, 4] ])
->groupBy('product_id'),
//->all(),
],
['p1.product_id' => 'p2.product_id', 'p1.updated_at' => 'p2.MaxDate']
)
Check the official documentation for innerJoin() method.
Related
How can I get all the instances of a Table where the fields are not NULL ?
Here is the configuration:
I have a Table 1 where the instances have a relationship "hasmany" with a Table 2. I want to get all the instances of Table 1 linked with a Table 2 instance not NULL.
The CakePHP doc helped me finding the exists() and isNotNull() conditions but I did not achieve.
Here is how I imagined:
$Table1 = TableRegistry::get('Table1')->find('all')->contain([
'Table2' => [
'sort' => ['Table2.created' => 'desc']
]
])->where([
'Table1.id' => $id,
'Table2 IS NOT NULL'
]);
$this->set(compact('Table1'));
But it obviously does not work.
edit : I expect to get all the line of the Table1 which contain existing Not NULL Table2 line(s) linked. The problem is in the 'where' array with the 'Table2 IS NOT NULL', it does not work.
And without this line 'Table2 IS NOT NULL', I get all the Table1 line which contain a Table2 line or not (because some line of Table1 are not linked at all and I don't want to get these lines).
Assuming the tables follow convention and use "id" as the primary key, I suggest the easiest fix would be testing that field for NOT NULL.
I.e., replace this:
'Table2 IS NOT NULL'
with this:
'Table2.id IS NOT NULL'
or:
'Table2.id !=' => null
or:
'Table2.id >' => 0
I've successfuly get the Table1 lines with its existing Table2 line(s) associated.
query = TableRegistry::get('Table1')->find();
$query->select(['Table1.id', 'count' => $query->func()->count('Table2.id')])->matching('Table2')->group(['Table1.id'])->having(['count
>' => 0]);
$table1Ids = [];
foreach ($query as $z)
{
$table1Ids[] = $z->id;
}
$table1= TableRegistry::get('Table1')->find('all')->contain([
'Table2' => [
'sort' => ['Table2.created' => 'desc']
]
])->where([
'id IN' => $table1Ids,
]);
So I need to write SQL statement using CakePhp ORM, but I have problem how to write in Cakephp GROUP BY IF NULL(condition).
Here is SQL Statement:
SELECT COUNT(*) FROM
(
SELECT i.id
FROM items i
INNER JOIN orders o ON i.order_id = o.id
WHERE (
(i.type = 1 AND i.status = 50) OR ((i.type = 2 OR i.type = 4) AND i.status = 60))
AND i.date_of_completion IS NOT NULL
GROUP BY IFNULL(i.vessel_change_identifier, i.id)
) AS temptbl;
This is my CakePhp Query
$query = TableRegistry::get('Items')
->find('all')
->innerJoinWith('Orders', function ($q) {
return $q->where(['Orders.id' => 'Items.order_id']);
})
->Where([
'Items.type' => 1,
'Items.status' => 50,
])
->orWhere([
'Items.type IN ' => [2, 4],
'Items.status' => 60,
])
->andWhere([
'Items.date_of_completion IS NOT' => NULL
]);
$query->select([
'count' => $query->func()->count('*')
]);
Thank you!
Try to use ->ifnull()
$query = TableRegistry::get('Items')
->find()
->innerJoinWith('Orders')
->where([
'Items.type' => 1,
'Items.status' => 50,
])
->orWhere([
'Items.type IN ' => [2, 4],
'Items.status' => 60,
])
->andWhere([
'Items.date_of_completion IS NOT' => NULL
]);
$query
->group(
$query
->func()
->ifnull([
'Items.vessel_change_identifier',
'Items.id'
])
);
$query->select([
'count' => $query->func()->count('*')
]);
Use it and enjoy it :D
The Problem:
I have a cakephp 3.x query object with two nested associations, Organizations.Positions.Skills, that is being set to a view variable, $Organizations. I'm trying to sort the query's resulting top level array by a column in the first nested association. That is, I want to sort $Organizations by a column in Positions, specifically Positions.Ended).
public function index()
{
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills'
])
);
}
Model Info
Organizations has many Positions
Positions has many Skills
Research I've Done
order option
According to the cookbook find() has an order option: find()->order(['field']); or find('all', ['order'=>'field DESC']);
However, this only applies to fields in the table find() is being called upon. In this case, Organizations. For example, this is how it's typically used.
//This works.
//Sorts Organizations by the name of the organization in DESC.
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills'
])
->order(['organization DESC'])
);
but, trying to use it for nested associations doesn't work:
$this->set('Organizations',
this->Organizations->find(...)
->contain(...)
->order(['Organizations.Positions.ended DESC'])
);
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Organizations.Positions.ended' in 'order clause'
and altering it to refer to the field that'll be nested doesn't work either:
//also doesn't work.
$this->set('Organizations',
$this->Organizations->find(...)
->contain(...)
->order([Positions.ended DESC])
);
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Positions.ended' in 'order clause'
In both cases, the sql error is created when cakephp executes the PDO statement generated by the query.
sort option
Similarly, according to the cookbook, eager loading / associations has the 'sort' option:
$this->loadModel('Organizations');
$this->set('Organizations',
$this->Organizations->find('all')
->contain([ //eager loading
'Positions.Skills',
'Positions' => [
'sort' => ['Positions.ended'=>'DESC']
]
])
);
But, this only sorts the nested association.Specifically, it sorts the associations that are nested. It does not sort the entire resulting set by the nested association (ergo, a multidimensional sort).
For example:
The Organization, Organization C (org id 1), has two positions:
Position 5. Ended 2012
Position 4. Ended 2014
And the Organization, Organization B (org id 2), has two positions:
Position 3 Ended 2013
Position 2 Ended 2015
The above code and data results in the following array being generated when the query is evaluated:
Organizations => [
0 => [
'organization' => 'Organization A',
positions => [
0 => [
'position' => 'Position 1',
'ended' => '2016'
]
]
],
1 => [
'organization' => 'Organization C',
'positions' => [
0 => [
'position' => 'Position 4',
'ended' => '2014'
],
1 => [
'position' => 'Position 5',
'ended' => '2012'
]
]
],
2 => [
'organization' => 'Organization B',
'positions' => [
0 => [
'position' => 'Position 2',
'ended' => '2015'
],
1 => [
'position' => 'Position 3',
'ended' => '2013'
]
]
]
]
other research
Likewise the following stackoverflow questions came up in my research:
http://stackoverflow.com/questions/26859700/cakephp-order-not-working
http://stackoverflow.com/questions/17670986/order-by-doesnt-work-with-custom-model-find-while-paginating
http://stackoverflow.com/questions/18958410/cakephp-paginate-and-sort-2nd-level-association
http://stackoverflow.com/questions/34705257/cakephp-paginate-and-sort-hasmany-association
Furthermore, I do know that PHP has its own sorting functions like sort() and multisort(); but, those can only be called once the query has been evaluated (by foreach). Alternatively, there's calling $organization->toArray() then using multisort; but, this would have to be done in the view, would break the MVC convention of separations of concerns (data and queries are manipulated by the controller and model, not the view!), and would be quite inefficient as it'll be called while the page is loading.
How then, do I sort a cakephp query by its nested associations?
Or, put more simply, how do I order/sort the query to produce the following array upon evaluation:
Organizations => [
0 => [
'organization' => 'Organization A',
'positions' => [
0 => [
'position' => 'Position 1',
'ended' => '2016'
]
]
],
0 => [
'organization' => 'Organization B',
'positions' => [
0 => [
'position' => 'Position 2',
'ended' => '2015'
],
1 => [
'position' => 'Position 3',
'ended' => '2013'
]
]
],
1 => [
'organization => 'Organization C',
'positions' => [
0 => [
'position' => 'Position 4',
'ended' => '2014'
],
1 => [
'position' => 'Position 5',
'ended' => '2012'
]
]
]
]
Background & Context:
I'm building a [portfolio website][7] for myself with cakephp 3.2 to showcase my web dev skills and assist in my quest for a dev career. For my resume page, I'm organizing the massive amount of data with nested accordions to mimic the resume style recruiters would expect to see on an actual resume. As a result, my view does the following:
Looping through the top level view variable (Organizations)
Rendering the organization details
Looping through that organization's positions (still inside 1)
render the position details
loop through the position's relevant skills
render each skill w/ the appropriate link to filter by that skill.
List item
Only hasOne and belongsTo associations are being retrieved via a join on the main query. hasMany associations are being retrieved in a separate queries, hence the errors when you try to refer to a field of Positions.
What you want should be fairly easy to solve, on SQL level, as well as on PHP level.
SQL level
On SQL level you could join in Positions, and order by a computed max(Positions.ended) column, like:
$this->Organizations
->find('all')
->select(['max_ended' => $this->Organizations->query()->func()->max('Positions.ended')])
->select($this->Organizations)
->contain([
'Positions.Skills',
'Positions' => [
'sort' => ['Positions.ended' => 'DESC']
]
])
->leftJoinWith('Positions')
->order([
'max_ended' => 'DESC'
])
->group('Organizations.id');
And that's all, that should give you the results that you want. The query will look something like:
SELECT
MAX(Positions.ended) AS max_ended,
...
FROM
organizations Organizations
LEFT JOIN
positions Positions
ON Organizations.id = (
Positions.organization_id
)
GROUP BY
Organizations.id
ORDER BY
max_ended DESC
PHP level
On PHP level it's also pretty easy to solve to using collections (note that queries are collections - kind of), however it would only make sense if you'd intend to retrieve all rows, or must deal with a set of unordered rows for whatever reason... something like:
$Organizations->map(function ($organization) {
$organization['positions'] =
collection($organization['positions'])->sortBy('ended')->toList();
return $organization;
});
$sorted = $sorted->sortBy(function ($organization) {
return
isset($organization['positions'][0]['ended']) ?
$organization['positions'][0]['ended'] : 0;
});
This could also be implemented in a result formatter, so things happen on controller or model level if you insist.
$query->formatResults(function ($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
$results = $results->map(function ($row) {
$row['positions'] =
collection($row['positions'])->sortBy('ended')->toList();
return $row;
});
return $results->sortBy(function ($row) {
return isset($row['positions'][0]['ended']) ?
$row['positions'][0]['ended'] : 0;
});
});
See
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Queries Are Collection Objects
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
Cookbook > Collections > Sorting
This worked for me:
$this->Organizations->find('all')
->contain(['Positions' => ['sort' => ['Positions.ended' => 'DESC']]])
->contain('Postions.Skills');
I did not find any example of yii2 query builder yii\db\Query using in operator with where clause. for the time being I am using it this way
$result = (new \yii\db\Query)
->select('*')
->from('customer c')
->where('c.status in (' . implode(',', [0,1]) . ')')->all();
But there must be a better way of doing this. Thanks in advance
$result = (new \yii\db\Query)
->select('*')
->from('customer c')
->where(['c.status' => [0, 1]])->all();
In the yii\db\QueryInterface::where() API:
The $condition specified as an array can be in one of the following two formats:
hash format: ['column1' => value1, 'column2' => value2, ...]
operator format: [operator, operand1, operand2, ...]
... In case when a value is an array, an IN expression will be generated.
['id' => [1, 2, 3], 'status' => 2] generates (id IN (1, 2, 3)) AND (status = 2).
I'm having a hard time with YII2's ORM, which does not document some quite simple typical SQL cases like
Pseudo-Code
SELECT * FROM table WHERE (a=1 AND b=2) OR (a=3 AND b=4)
What I've tried:
// should represent the commented logic, but does not
Demo::find()
->where(...) // ( condition one
->andWhere(...) // AND condition two )
->orWhere(...) // OR (!) ( condition three
->andWhere(...) // AND condition four )
Question:
In YII2, the where()-method does not allow to "nest" queries, and this is where I'm stuck. YII2 only allows to say simple AND .. OR costructs, never a set of ANDs together.
where() method allows nested conditions:
Demo::find()->where(['or', ['a' => 1, 'b' => 2], ['a' => 3, 'b' => 4]]);
There are some examples in official docs here.
More complex example (as you asked in comment):
Demo::find()->where([
'or',
['and', ['not in', 'a' => [1, 2]], ['not in', 'b' => [3, 4]]],
['a' => [5, 6]], 'b' => [7, 8]],
]);
Another example can be found in similar question here.
You can do it like this
Demo::find()
->where('(a = :a AND b = :b)', [
':a' => 'bla',
':b' => 'bla',
])
->orWhere('(c = :c AND d = :d)', [
':c' => 'bla',
':d' => 'bla',
]);
The docs http://www.yiiframework.com/doc-2.0/yii-db-queryinterface.html#where()-detail give a pretty good explanation of the possibilities of the where options. Including how to use a subquery.
The and and or description explains how to create sets in a different way than shown above.
Demo::find()->where('a=1 and b=2 or f=3')->one();