Subquerys in cakephp 3.x, new ORM? - cakephp

I'm new in Cakephp 3.x and I'm having some trouble to create a subquery in the new ORM format. I have this report in my application, that needs to return the follow result:
1. There are three entities - Users, Calls, CallStatus.
2. Users hasMany Calls, Calls hasMany CallStatus.
3. I need to count how many CallStatus each user has in Calls.
Now follow the query that I need to put on new ORM format:
SELECT U.name,
(SELECT COUNT(*) FROM calls as C WHERE C.call_status_id =1 and C.user_id=U.id) AS 'Unavailable',
(SELECT COUNT(*) FROM calls as C WHERE C.call_status_id =2 and C.user_id=U.id) AS 'Busy',
(SELECT COUNT(*) FROM calls as C WHERE C.call_status_id =3 and C.user_id=U.id) AS 'Contacted',
(SELECT COUNT(*) FROM calls as C WHERE C.call_status_id =4 and C.user_id=U.id) AS 'Error'
FROM `users` AS U
WHERE U.profile=3 and U.is_active=1
Could someone give me a help, please? Thanks

If I understand you correctly, you want to see the number of calls for every callstatus you have for a specific user.
Try the following. Note that I used the CakePHP convention for naming the callstatuses (which is plural).
// get the tableregistry
use Cake\ORM\TableRegistry;
$callstatuses = Cake\ORM\TableRegistry::get('Callstatuses');
// for user with id 2, get the number of calls for each callstatus
$callstatuses->find()
->contain(['Calls'])
->where(['Calls.user_id' => 2, 'User.is_active' => 1])
->countBy('name')
->toArray();
// output could be:
//[ 'Unavailable' => 2, 'Busy' => 1 ]
You can find information about creating queries in the CakePHP book: see 'Query Builder'.
If you want to know more about working with/on queries, note that queries are Collections. Anything you can do on a Collection object, you can also do in a Query object. See the Collection section in the CakePHP book.

You have to use subqueries, as many as you want!
Here is an example for your case:
$q = $this->Calls->find();
$q1->select([$q->func()->count('*')])
->where(['Calls.user_id = Users.id', 'call_status_id' => 1]);
$q2->select([$q->func()->count('*')])
->where(['Calls.user_id = Users.id', 'call_status_id' => 2]);
$q3->select([$q->func()->count('*')])
->where(['Calls.user_id = Users.id', 'call_status_id' => 3]);
$q4->select([$q->func()->count('*')])
->where(['Calls.user_id = Users.id', 'call_status_id' => 4]);
$qUsers = $this->Users->find()
->select([
'id',
'first_name',
'Unavailable' => $q1,
'Busy' => $q2,
'Contacted' => $q3,
'Error' => $q4
])
->where(['profile' => 3, 'active' => 1])
->all();
Note: That nicer if you use a loop to create suqueries in this case.

Related

union request and pagination in cakephp4

I made two requests. The first one gives me 2419 results and I store the result in $requestFirst. The second, 1 result and I store the result in $requestTwo.
I make a union :
$requestTot = $requestFirst->union($requestTwo);
The total of the $requestTot is 2420 results so all is well so far.
Then :
$request = $this->paginate($requestTot);
$this->set(compact('request'));
And here I don't understand, on each page of the pagination I find the result of $requestTwo. Moreover the pagination displays me :
Page 121 of 121, showing 20 record(s) out of 2,420 total
This is the right number of results except that when I multiply the number of results per page by the number of pages I get 2540. This is the total number of results plus one per page.
Can anyone explain?
Check the generated SQL in Debug Kit's SQL panel, you should see that the LIMIT AND OFFSET clauses are being set on the first query, not appended as global clauses so that they would affect the unionized query.
It will look something like this:
(SELECT id, title FROM a LIMIT 20 OFFSET 0)
UNION
(SELECT id, title FROM b)
So what happens then is that pagination will only be applied to the $requestFirst query, and the $requestTwo query will be unionized on top of it each and every time, hence you'll see its result on every single page.
A workaround for this current limitation would be to use the union query as a subquery or a common table expression from which to fetch the results. In order for this to work you need to make sure that the fields of your queries for the union are being selected without aliasing! This can be achieved by either using Table::subquery():
$requestFirst = $this->TableA
->subquery()
->select(['a', 'b'])
// ...
$requestTwo = $this->TableB
->subquery()
->select(['c', 'd'])
// ...
or by explicitly selecting the fields with aliases equal to the column names:
$requestFirst = $this->TableA
->find()
->select(['a' => 'a', 'b' => 'b'])
// ...
$requestTwo = $this->TableB
->find()
->select(['c' => 'c', 'd' => 'd'])
// ...
Then you can safely use those queries for a union as a subquery:
$union = $requestFirst->union($requestTwo);
$wrapper = $this->TableA
->find()
->from([$this->TableA->getAlias() => $union]);
$request = $this->paginate($wrapper);
or as a common table expression (in case your DBMS supports them):
$union = $requestFirst->union($requestTwo);
$wrapper = $this->TableA
->find()
->with(function (\Cake\Database\Expression\CommonTableExpression $cte) use ($union) {
return $cte
->name('union_source')
->field(['a', 'b'])
->query($union)
})
->select(['a', 'b'])
->from([$this->TableA->getAlias() => 'union_source']);
$request = $this->paginate($wrapper);

Print SQL query of ORM query builder in cakephp3

How to print ORM query
$query = $articles->find('all')->contain(['Comments']);
For example print =>
SELECT * FROM comments WHERE article_id IN (comments);
Wrapping your ORM query result with the debug function will show the SQL and bound params:
debug($query);
You can also similarly look at the query results with the debug function.See CakePHP 3: retrieving data and result sets — Debugging Queries and ResultSets
what about $query->sql()?
$qb = $this->Person->find()->select(["id", "text" => "concat(Name,' ',Family)"])
->where(['id >' => 0])
->where($query ? ["OR" => $filters] : null)
->limit(10);
dd($qb->sql());
and result:
.../src/Controller/ClientController.php (line 86)
'SELECT Person.id AS `Person__id`, concat(Name,' ',Family) AS `text` FROM person Person WHERE (id > :c0 AND (Family like '%sam%' OR Name like '%sam%' OR Family like '%sam%' OR Name like '%sam%')) LIMIT 10'
I prefer this:
public function __debugInfo()
{
return [
'query' => $this->_query,
'items' => $this->toArray(),
];
}
// Print the query
debug($query->__debugInfo()['sql']);
// Prints this
SELECT * FROM comments WHERE article_id IN (comments);

Paginate results filtered by condition on associated model (HABTM) using Containable

I need to paginate list of Products belonging to specific Category (HABTM association).
In my Product model I have
var $actsAs = array('Containable');
var $hasAndBelongsToMany = array(
'Category' => array(
'joinTable' => 'products_categories'
)
);
And in ProductsController
$this->paginate = array(
'limit' => 20,
'order' => array('Product.name' => 'ASC'),
'contain' => array(
'Category' => array(
'conditions' => array(
'Category.id' => 3
)
)
)
);
$this->set('products', $this->paginate());
However, resulting SQL looks like this:
SELECT COUNT(*) AS `count`
FROM `products` AS `Product`
WHERE 1 = 1;
SELECT `Product`.`*`
FROM `products` AS `Product`
WHERE 1 = 1
ORDER BY `Product`.`name` ASC
LIMIT 20;
SELECT `Category`.`*`, `ProductsCategory`.`category_id`, `ProductsCategory`.`product_id`
FROM `categories` AS `Category`
JOIN `products_categories` AS `ProductsCategory` ON (`ProductsCategory`.`product_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) AND `ProductsCategory`.`category_id` = `Category`.`id`)
WHERE `Category`.`id` = 3
(I.e. it selects 20 Products and then queries their Categories)
while I'd need
SELECT COUNT(*) AS `count`
FROM `products` AS `Product`
JOIN `products_categories` AS `ProductsCategory` ON `ProductsCategory`.`product_id` = `Product`.`id`
JOIN `categories` AS `Category` ON `Category`.`id` = `ProductsCategory`.`category_id`
WHERE `Category`.`id` = 3;
SELECT `Product`.*, `Category`.*
FROM `products` AS `Product`
JOIN `products_categories` AS `ProductsCategory` ON `ProductsCategory`.`product_id` = `Product`.`id`
JOIN `categories` AS `Category` ON `Category`.`id` = `ProductsCategory`.`category_id`
WHERE `Category`.`id` = 3
ORDER BY `Product`.`name` ASC
LIMIT 20;
(I.e. select top 20 Products which belong to Category with id = 3)
Note:
Possible solution without Containable would be (as Dave suggested) using joins.
This post offers a very handy helper to build $this->paginate['joins'] to paginate over HABTM association.
Note: Still looking for more elegant solution using Containable than fake hasOne binding.
Finally I found a way to do what I want, so posting it as an answer:
To force JOIN (and be able to filter via condition on associated model) in Containable - you've got to use fake hasOne association.
In my case, code in ProductsController should be:
$this->Product->bindModel(array('hasOne' => array('ProductsCategory')), false);
$this->paginate = array(
'limit' => 20,
'order' => array('Product.name' => 'ASC'),
'conditions' => array(
'ProductsCategory.category_id' => $category
),
'contain' => 'ProductsCategory'
);
$this->set('products', $this->paginate());
Note false as a second argument to bindModel - which makes binding persistent. This is needed because paginate() issues find('count') before find('all'), which would reset temporary binding. So you might want to manually unbindModel afterwards.
Also, if your condition includes multiple IDs in HABTM associated model, you might want to add 'group' => 'Product.id' into your $this->paginate[] (as Aziz has shown in his answer) to eliminate duplicate entries (will work on MySQL only).
UPDATE:
However, this approach has one serious drawback compared to joins approach (suggested by Dave): condition can apply only to intermediate model's foreign key (category_id in my case); if you want to use condition on any other field in associated model - you'd probably have to add another bindModel('hasOne'), binding intermediate model to HABTM associated model.
When you put the condition in the nested Contain, you're asking it to retrieve only the Categories with that ID. So - it's doing what you're asking, but that's not what you want.
Though it seems like it should be possible, the only luck I've had doing what you're trying to do (after MANY hours and a few stackoverflow questions) is via Joins instead of Contain.
http://book.cakephp.org/view/1047/Joining-tables
It's not the exact same problem, but you can go through some of my code where I query against HABTM conditions (I answered my question at the bottom) here: Select All Events with Event->Schedule->Date between start and end dates in CakePHP

How to enable select list when related checkbox is checked in interactive report in APEX 5.1

I have created an interactive report on SQL query in Oracle APEX 5.1 and i have added two more columns to query. One is checkbox for each row in report and other one is select list containing numbers for each row. SQL query is written below. What i want to do is enabling select list when related checkbox is checked otherwise end-user shouldn't be able to select number from list. I know this can be done with dynamic actions but i couldn't point out solution.
Why i want to disable select list: I can reach returned value of check-boxes via array named APEX_APPLICATION.G_F01 by for loop in pl/sql. Inside the array, there are checked rows but I couldnt reach amounts of checked rows via APEX_APPLICATION.G_F02 which is array of values of select list because this array also contains amounts undesired, unchecked columns of values. I found solution on enabling select list when checkbox is checked.
QUERY:
SELECT APEX_ITEM.CHECKBOX2(1,URUN.BARKOD) "Select", U.AD, MA.AD MAGAZA_ADI, FIYAT, APEX_ITEM.SELECT_LIST(
p_idx => 2,
p_list_values => '1;1,2;2,3;3,4;4,5;5,6;6,7;7,8;8,9;9,10;10',
p_show_null => 'YES',
p_null_value => NULL,
p_null_text => '-0-',
p_item_id => 'f03_#ROWNUM#',
p_item_label => 'Label for f03_#ROWNUM#') "Adet"
FROM URUNSATIS URUN, UYE UN, ADRES AD, ANLASMALAR AN, MAGAZA MA, URUN U
WHERE UN.USERNAME = :SESSION_USER_NAME AND UN.ID = AD.UYE_ID AND AD.APARTMAN_ID = AN.APARTMAN_ID AND AN.MAGAZA_ID = URUN.MAGAZA_ID AND MA.ID = URUN.MAGAZA_ID AND U.BARKOD=URUN.BARKOD
ORDER BY 1;
İdea behind this question: I want end-users to select a product from report and when they select checkbox of desired product they should able to choose amount of product that they want to order from market. I got stuck on this phase of app. I am open to any new idea for solving this problem.
You could use the p_checked_attributes option on the checkbox to keep track of which ones had been checked via a hidden item, and then have a dynamic action to disable / enable based on if the value is contained in a hidden item.
So your select query would look something like this:
SELECT
APEX_ITEM.CHECKBOX2(p_idx => 1,
p_value => URON.BARKOD,
-- p_attributes => 'class="barkod"', use this value if you want to keep track after refresh
p_checked_values => :P1_BARKOD_LIST,
p_checked_values_delimiter => ',') AS "SELECT",
U.AD, MA.AD MAGAZA_ADI, FIYAT,
APEX_ITEM.SELECT_LIST(
p_idx => 2,
p_list_values => '1;1,2;2,3;3,4;4,5;5,6;6,7;7,8;8,9;9,10;10',
p_show_null => 'YES',
p_null_value => NULL,
p_null_text => '-0-',
p_item_id => 'f03_#ROWNUM#',
p_item_label => 'Label for f03_#ROWNUM#') "Adet"
FROM URUNSATIS URUN, UYE UN, ADRES AD, ANLASMALAR AN, MAGAZA MA, URUN U
WHERE UN.USERNAME = :SESSION_USER_NAME AND UN.ID = AD.UYE_ID AND AD.APARTMAN_ID = AN.APARTMAN_ID AND AN.MAGAZA_ID = URUN.MAGAZA_ID AND MA.ID = URUN.MAGAZA_ID AND U.BARKOD=URUN.BARKOD
ORDER BY 1;
add a class attribute to your checkbox
name your select list id to be fixe
SELECT APEX_ITEM.CHECKBOX2(1,URUN.BARKOD,'class=indCheck') "Select", U.AD, MA.AD MAGAZA_ADI, FIYAT, APEX_ITEM.SELECT_LIST(
p_idx => 2,
p_list_values => '1;1,2;2,3;3,4;4,5;5,6;6,7;7,8;8,9;9,10;10',
p_show_null => 'YES',
p_null_value => NULL,
p_null_text => '-0-',
p_item_id => 'indSelect',
p_item_label => 'Label for f03_#ROWNUM#') "Adet"
FROM URUNSATIS URUN, UYE UN, ADRES AD, ANLASMALAR AN, MAGAZA MA, URUN U
WHERE UN.USERNAME = :SESSION_USER_NAME AND UN.ID = AD.UYE_ID AND AD.APARTMAN_ID = AN.APARTMAN_ID AND AN.MAGAZA_ID = URUN.MAGAZA_ID AND
MA.ID = URUN.MAGAZA_ID AND U.BARKOD=URUN.BARKOD
ORDER BY 1;
Now create a dynamic action on click on your checkbox execute javascript code:
here the javascript code
$(this.triggeringElement).closest('tr').find('td select#indSelect').attr('disabled',false);
if ( $(this.triggeringElement).is(':checked') )
$(this.triggeringElement).closest('tr').find('td select#indSelect').attr('disabled',true);

CakePHP 3 find() with beforeFind() callback SQL issue

I'm having a problem with data integrity when using find() in my controller in conjunction with beforeFind() in a behavior callback. The WHERE Submissions.site_id is not being added in the WHERE clause like it should be. I get different result sets depending on where the WHERE clause is set.
in my SubmissionsController:
public function index()
{
$query = $this->Submissions->find('all')
->where(['user_id' => $this->Auth->user('id')])
->contain(['Users', 'Categories']);
$this->set('submissions', $this->paginate($query));
}
In my beforeFind() Model callback (attached as a 'TenantBehavior' to
$query->where([$this->_table->alias().'.'.'site_id' => 3]);
The problem is that with the above, the SQL generated puts the "WHERE" clause as an AND on the JOIN condition like so, and NOT on the actual WHERE:
...
FROM
submissions Submissions
INNER JOIN users Users ON (
Users.id = (Submissions.user_id)
AND Users.site_id = 3
)
INNER JOIN categories Categories ON (
Categories.id = (Submissions.category_id)
AND Categories.site_id = 3
)
WHERE
user_id = 315
If I remove the beforeFind() ->where and instead place it on the controller ->where I get the expected SQL and result set like so:
...
FROM
submissions Submissions
INNER JOIN users Users ON (
Users.id = (Submissions.user_id)
AND Users.site_id = 3
)
INNER JOIN categories Categories ON (
Categories.id = (Submissions.category_id)
AND Categories.site_id = 3
)
WHERE
(
user_id = 315
AND Submissions.site_id = 3
)
Thoughts? Suggestions?
EDIT
As #ndm's suggestion, I began to update and provide much more context. In doing so I discovered (like an idiot) that I was missing the $this->addBehavior('Tenant'); on my 'SubmissionsTable' model. Adding this of course solved the issue.
$query = $this->Submissions->find('all', [
'conditions' => [
'user_id' => $this->Auth->user('id')
],
'contain' => [
'Users',
'Categories',
]
]);
Passing conditions as inline array will solve your issue and append beforeFind() where conditions properly.

Resources