Query Builder: Subquery with multiple SUMs on associated data - cakephp

I'd like to request advice for an approach to use the Query Builder to get multiple sums from associated models.
There are three tables:
invoices invoice_items payment_receipts
-------- ------------- -------------
id | name id| invoice_id | invoice_qty unit_price id| invoice_id | receipt_amount
===|====== ========================================== ================================
1 |INV01 1| 1 | 1300 |12.00 1 | 1 | 1000
2 |INV02 2| 1 | 2600 |9.00 2 | 1 | 2000
3 |INV03 3| 2 | 1100 |15.00 3 | 3 | 900
4| 3 | 900 |12:00
For each invoice, I want the sum of the items' total amount (qty * price), and also the sum of payment receipts.
This query (with subqueries) correctly gets the result I'm looking for:
SELECT Invoices.id, Invoices.invoice_name, InvoiceItemSum.SumOfAmount, PaymentSum.SumOfPaymentAmount
FROM Invoices
INNER JOIN (
SELECT invoice_id, SUM(Invoice_items.invoice_qty * Invoice_items.unit_price) AS SumOfAmount
FROM Invoice_items
GROUP BY Invoice_id
) InvoiceItemSum ON InvoiceItemSum.Invoice_id = Invoices.id
LEFT JOIN (
SELECT Invoice_id, SUM(Payment_receipts.receipt_amount) AS SumOfPaymentAmount
FROM Payment_receipts
GROUP BY Invoice_id
) PaymentSum ON PaymentSum.Invoice_id = Invoices.id
WHERE Invoices.invoice_id = 33
I can execute this query directly in my CakePhp app and get the results I need, so it works that way.
However I'd like advice on a more elegant CakePHP way to do this via the Query Builder.
I have tried this:
$query = $this->Invoices->find()->contain(['InvoiceItems', 'PaymentReceipts']);
$query->select([
'Invoices.id',
'Invoices.invoice_name',
]);
$query->select([
'total_inv_amt' => $query->func()->sum('InvoiceItems.invoice_qty * InvoiceItems.unit_price'),
'total_paid_amt' => $query->func()->sum('PaymentReceipts.receipt_amount')
])
->innerJoinWith('InvoiceItems')
->leftJoinWith('PaymentReceipts')
->group(['Invoices.id']);
$query->where(['Invoices.id' => 33]);
But this results in doubling the two sums via creating this query:
SELECT
Invoices.id AS Invoices__id,
Invoices.invoice_name AS Invoices__invoice_name,
(
SUM(
InvoiceItems.invoice_qty * InvoiceItems.unit_price
)
) AS total_inv_amt,
(
SUM(PaymentReceipts.receipt_amount)
) AS total_paid_amt
FROM
invoices Invoices
INNER JOIN invoice_items InvoiceItems ON Invoices.id = (InvoiceItems.invoice_id)
LEFT JOIN payment_receipts PaymentReceipts ON Invoices.id = (PaymentReceipts.invoice_id)
WHERE
Invoices.id = 33
GROUP BY
Invoices.id
I've tried subqueries following the documentation with myriad unsuccessful results. I've also played with joins but still no dice.
My question is: what is a good approach to write this query using the Query Builder?
Thanks in advance for any advice!

You can easily do it by cakephp JOIN. Check this solution it may help you.
Direct written in editor , not guaranty without syntax error !
$this->Invoices->find()
->select([
'Invoices.id',
'Invoices.invoice_name',
'total_inv_amt' => $query->func()->sum('InvoiceItems.invoice_qty * InvoiceItems.unit_price'),
'total_paid_amt' => $query->func()->sum('PaymentReceipts.receipt_amount')
])
->join([
'table' => 'InvoiceItems',
'alias' => 'InvoiceItems',
'type' => 'Inner',
'conditions' => [
// your condition for InvoiceItems
],
])
->join([
'table' => 'PaymentReceipts',
'alias' => 'PaymentReceipts',
'type' => 'LEFT',
'conditions' => [
// condition for PaymentReceipts
],
])

OK, finally after reviewing Alimon's answer and several other questions on subqueries such as this and this, I've arrived at the correct Query Builder solution for this. Here it is:
$subquery_a = $this->Invoices->InvoiceItems->find('all');
$subquery_a
->select(['totalinvoiceamt' => $subquery_a->func()->sum('invoice_qty * unit_price') ])
->where([
'InvoiceItems.invoice_id = Invoices.id'
]);
$subquery_b = $this->Invoices->PaymentReceipts->find('all');
$subquery_b
->select(['totalpaymentamt' => $subquery_b->func()->sum('receipt_amount') ])
->where([
'PaymentReceipts.invoice_id = Invoices.id'
]);
$query = $this->Invoices->find('all')
->select([
'Invoices.id',
'Invoices.invoice_name',
'InvoiceItems__total_invoice_amount' => $subquery_a,
'PaymentReceipts__total_payments_amount' => $subquery_b
])
->join([
[
'table' => 'invoice_items',
'alias' => 'InvoiceItems',
'type' => 'INNER',
'conditions'=> [
'Invoices.id = InvoiceItems.invoice_id'
]
]
])
->join([
[
'table' => 'payment_receipts',
'alias' => 'PaymentReceipts',
'type' => 'LEFT',
'conditions'=> [
'Invoices.id = PaymentReceipts.invoice_id'
]
]
])
->group('InvoiceItems.invoice_id');
$query->where(['Invoices.id' => 33]);
The results are the same as the direct query, though the SQL looks a bit different from the manual one, but the results are identical.
Thanks Alimon et al for the assistance.

Related

Cakephp left outer join from query builder is not giving correct sql format

I want to make sql query like this. Which is working fine. but if I am trying with using cakephp query builder I could not find any path. I am also posting what I have done in cakephp query builder. Please check...
$query = '
select
schemes.name,
most_recent_export_product.itc_hs_code,
sum(epinvoicevalueusd) from oc
join (
select
oc_id, itc_hs_code
from
e_products
where
itc_hs_code is not null
group
by oc_id
order
by oc_id, created desc
)
as most_recent_export_product
on oc.id = most_recent_export_product.oc_id
join
schemes on schemes.id = oc.scheme_id
where
application_status_id = 8 AND
certificate_issue_date >= 2022-09-28
group by
scheme_id, most_recent_export_product.itc_hs_code
order by
scheme_id, most_recent_export_product.itc_hs_code';
$connection = ConnectionManager::get('default');
$ocData = $connection->execute($query)->fetchAll('assoc');
In cake php I am using query builder to do the same like
$exdata = $this->EProducts
->find()
->select(['oc_id', 'itc_hs_code'])
->group('oc_id')
->order(['created' => 'DESC']);
$ocData = $this->oc
->find()
->select([
'scheme_name' => 'Schemes.name',
'itc_hs_code' => 'ExportProducts.itc_hs_code',
'epinvoicevalueusd' => 'sum(oc.epinvoicevalueusd)'
])
->join([
'exp' => [
'type' => 'LEFT',
'conditions' => 'exp.oc_id = oc.id',
'group' => 'exp.oc_id'
]
])
->contain('Schemes')
->where([
'application_status_id' => 8,
'certificate_issue_date >=' => '2022-09-28'
])
->group(['scheme_id'])
->order(['scheme_id']);
I dont how to make it with using query builder in cakephp. Please help anyone. :)

Cakephp get associative sum group by in postgres

I have two tables
coins summary_coins
-------- -------------
id | name id| date | get_count | coin_id
===|==== ==============================================
1 |lira 1 | 2020-02-16 16:55:50 | 20 | 1
2 |A 1 | 2020-03-16 16:55:50 | 12 | 1
3 |B 1 | 2020-03-16 16:55:50 | 20 | 1
My Expected result
name get_count previous_month_count
Lira 32 20
I have tried below cakephp method to get expected result
public function getCount($start_date,$end_date,$previous_month_start_date,$previous_month_end_date)
{
$query = $this->find();
$query
->select([
'Coins.id',
'Coins.name',
'curr_get_count' => $query->func()->sum('SummaryCoins.get_count'),
'prev_get_count' => $query->func()->sum('SummaryCoins.get_count'),
])
->matching('SummaryCoins')
->where([
'SummaryCoins.user_id' => 1,
'SummaryCoins.date >' => '2021-04-01',
'SummaryCoins.date <' => '2021-05-31'
])
->group([
'Coins.id',
]);
return $query;
}
Here I am getting present month range, how I will get previous month count ?
There's many ways to achieve this, subqueries, multiple custom joins, case expressions, etc...
I would suggest that you try case expressions first, where depending on your date conditions you either return SummaryCoins.get_count, NULL (which is being ignored by SUM) or 0 (in case you want the sum to be 0 instead of NULL if no rows are matching your conditions), eg generate SQL like:
SUM(
CASE WHEN
SummaryCoins.date >= '2020-03-01' AND
SummaryCoins.date <= '2020-03-31'
THEN
SummaryCoins.get_count
ELSE
0
END
)
$query
->select([
'Coins.id',
'Coins.name',
'curr_get_count' => $query->func()->sum(
$query
->newExpr()
->addCase(
[
$query->newExpr()->add([
'SummaryCoins.date >=' => $start_date,
'SummaryCoins.date <=' => $end_date,
])
],
[$query->identifier('SummaryCoins.get_count'), 0]
[null, 'integer']
)
),
'prev_get_count' => $query->func()->sum(
$query
->newExpr()
->addCase(
[
$query->newExpr()->add([
'SummaryCoins.date >=' => $previous_month_start_date,
'SummaryCoins.date <=' => $previous_month_end_date,
]),
],
[$query->identifier('SummaryCoins.get_count'), 0]
[null, 'integer']
)
),
])
Also note that you should use >= and <= to make the conditions inclusive, otherwise the first day and the last day of the month would be ignored in case the time part is 00:00:00.
See also
Cookbook > Database Access & SQL > Query Builder > Case Statements

Conditionally reuse a `hasOne` association multiple times inside the same query

What I have
In my OrdersTable.php:
$this->hasOne('Total', [
'className' => 'App\Model\Table\TotalsTable',
'foreignKey' => 'order_id',
'propertyName' => 'Total'
]);
Actual totals table:
| id | order_id | type | value |
|----|----------|----------|-------|
| 1 | 1 | total | 100 |
| 2 | 1 | tax | 20 |
| 3 | 1 | shipping | 5 |
The structure and logic come from opencart/opencart and I have no control over that.
What I want
This is a non-functional concept:
$query = $ordersTable->find('all', array(
'contain' => array(
'TotalTax' => [
'associationName' => 'Total',
'conditions' => function($query) {
return $query->where([
'TotalTax.type' => 'tax',
]);
},
],
'TotalShipping' => [
'associationName' => 'Total',
'conditions' => function($query) {
return $query->where([
'TotalShipping.type' => 'shipping',
]);
},
],
),
));
Do you guys think something like this is possible?
UPD: Creating an association for each type isn't an option since there may be too many of them
If this functionality is something that will be reused with your codebase, I would implement this logic at the table level and have two different conditional associations:
In OrdersTable.php
$this->hasOne('TotalTax', [
'className' => 'Totals'
])
->setConditions(['TotalTax.type' => 'tax'])
->setDependent(true);
$this->hasOne('TotalShipping', [
'className' => 'Totals'
])
->setConditions(['TotalShipping.type' => 'shipping'])
->setDependent(true);
Then you can simply contain them in the query:
$query = $ordersTable->find()->contain(['TotalTax', 'TotalShipping'];
An example of this can be found in the CakePHP documentation

How to create a join that uses a subquery?

How do you convert the following SQL Query to a CakePhp Find Query?
SELECT
a.id, a.rev, a.contents
FROM
YourTable a
INNER JOIN (
SELECT
id, MAX(rev) rev
FROM
YourTable
GROUP BY
id
) b ON a.id = b.id AND a.rev = b.rev
I have tried the code below:
return $model->find('all', [
'fields' => $fields,
'joins' => [
[
'table' => $model->useTable,
'fields' => ['id','MAX(rev) as rev'],
'alias' => 'max_rev_table',
'type' => 'INNER',
'group' => ['id'],
'conditions' => [
$model->name.'.id= max_rev_table.id',
$model->name.'.rev = max_rev_table.rev'
]
]
],
'conditions' => [
$model->name.'.emp_id' => $empId
]
]);
But it seems that in the generated SQL, the fields under the joins is not included. So I don't get the max(rev) which I need to get only the rows with max(rev).
I have tried rearranging the items inside joins but still produces same auto-generated SQL.
Can you please help me?
There are no fields or group options for joins, the only options are table, alias, type, and conditions.
If you want to join a subquery, then you need to explicitly generate one:
$ds = $model->getDataSource();
$subquery = $ds->buildStatement(
array(
'fields' => array('MaxRev.id', 'MAX(rev) as rev'),
'table' => $ds->fullTableName($model),
'alias' => 'MaxRev',
'group' => array('MaxRev.id')
),
$model
);
and pass it to the joins table option:
'table' => "($subquery)"
See also
Cookbook > Models > Associations: Linking Models Together > Joining tables
Cookbook > Models > Retrieving Your Data > Sub-queries

Cakephp retrieving HABTM which Conditions

I'm using cakephp and would like to display all Submissions which are part of Category 'X'
I have 4 tables with a HABTM relationship.
Users -> (haveMany) -> Submissions <-> (hasAndBelongsToMany) <-> Categories
but I would like to do so using the $this->paginate() and for each submission I would like to display the user who posted the submission.
User Table
Id | Name
-----+-------------------
1 | User 1
2 | User 2
Submission Table
Id | Name | User_id
-----+-------------------+--------------
1 | Submission 1 | 1
2 | Submission 2 | 2
Category Table
Id | Name
-----+-------------------
1 | Category 1
2 | Category 2
SubmissionCategory Table
Id | Submission_id | Category_id
-----+-------------------+-------------------
1 | 1 | 1
2 | 1 | 2
3 | 2 | 1
I am having really trouble creating a paginate which can do this, I'm starting to think its not possible unless I'm missing something.
If I was not using cakephp this is the query I would want to do
SELECT
*
FROM
submissions_categories,
submissions,
users
WHERE
submissions_categories.category_id = 8
AND
submissions_categories.submission_id = submissions.id
AND
submissions.user_id = users.id
HABTM relationships are very unwieldy in CakePHP I find. You are going to need to set up the joins manually using the $paginate variable. Then you can pass in the optional conditions array to the paginate() function. Example:
<?php
class SubmissionsController extends AppController {
var $name = 'Submissions';
var $helpers = array('Html', 'Form');
var $paginate = array('joins' => array(
array(
'table' => 'submissions_categories',
'alias' => 'SubmissionsCategory',
'type' => 'inner',
'conditions'=> array('SubmissionsCategory.submission_id = Submission.id')
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions'=> array(
'Category.id = SubmissionsCategory.category_id'
)
)));
function index() {
$this->Submission->recursion = 1;
$this->set('submissions', $this->paginate(array('Category.id'=>1)));
}
}
?>

Resources