I'm trying to write a query using CakePHP 3.7 ORM where it needs to add a column to the result set. I know in MySQL this sort of thing is possible: MySQL: Dynamically add columns to query results
So far I've implemented 2 custom finders. The first is as follows:
// src/Model/Table/SubstancesTable.php
public function findDistinctSubstancesByOrganisation(Query $query, array $options)
{
$o_id = $options['o_id'];
$query = $this
->find()
->select('id')
->distinct('id')
->contain('TblOrganisationSubstances')
->where([
'TblOrganisationSubstances.o_id' => $o_id,
'TblOrganisationSubstances.app_id IS NOT' => null
])
->orderAsc('Substances.app_id')
->enableHydration(false);
return $query;
}
The second custom finder:
// src/Model/Table/RevisionSubstancesTable.php
public function findProductNotifications(Query $query, array $options)
{
$date_start = $options['date_start'];
$date_end = $options['date_end'];
$query = $this
->find()
->where([
'RevisionSubstances.date >= ' => $date_start,
'RevisionSubstances.date <= ' => $date_end
])
->contain('Substances')
->enableHydration(false);
return $query;
}
I'm using the finders inside a Controller to test it out:
$Substances = TableRegistry::getTableLocator()->get('Substances');
$RevisionSubstances = TableRegistry::getTableLocator()->get('RevisionSubstances');
$dates = // method to get an array which has keys 'date_start' and 'date_end' used later.
$org_substances = $Substances->find('distinctSubstancesByOrganisation', ['o_id' => 123);
if (!$org_substances->isEmpty()) {
$data = $RevisionSubstances
->find('productNotifications', [
'date_start' => $dates['date_start'],
'date_end' => $dates['date_end']
])
->where([
'RevisionSubstances.substance_id IN' => $org_substances
])
->orderDesc('RevisionSubstances.date');
debug($data->toArray());
}
The logic behind this is that I'm using the first custom finder to produce a Query Object which contains unique (DISTINCT in SQL) id fields from the substances table, based on a particular company (denoted by the o_id field). These are then fed into the second custom finder by implementing where(['RevisionSubstances.substance_id IN' ....
This works and gives me all the correct data. An example of the output from the debug() statement is as follows:
(int) 0 => [
'id' => (int) 281369,
'substance_id' => (int) 1,
'date' => object(Cake\I18n\FrozenDate) {
'time' => '2019-09-02T00:00:00+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'comment' => 'foo',
'substance' => [
'id' => (int) 1,
'app_id' => 'ID000001',
'name' => 'bar',
'date' => object(Cake\I18n\FrozenDate) {
'time' => '2019-07-19T00:00:00+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
]
],
The problem I'm having is as follows: Each of the results returned contains a app_id field (['substance']['app_id'] in the array above). What I need to do is perform a count (COUNT() in MySQL) on another table based on this, and then add that to the result set.
I'm unsure how to do this for a couple of reasons. Firstly, my understanding is that custom finders return Query Objects, but the query is not executed at this point. Because I haven't executed the query - until calling $data->toArray() - I'm unsure how I would refer to the app_id in a way where it could be referenced per row?
The equivalent SQL that would give me the required results is this:
SELECT COUNT (myalias.app_id) FROM (
SELECT
DISTINCT (tbl_item.i_id),
tbl_item.i_name,
tbl_item.i_code,
tbl_organisation_substances.o_id,
tbl_organisation_substances.o_sub_id,
tbl_organisation_substances.app_id,
tbl_organisation_substances.os_name
FROM
tbl_organisation_substances
JOIN tbl_item_substances
ON tbl_organisation_substances.o_sub_id = tbl_item_substances.o_sub_id
JOIN tbl_item
ON tbl_item.i_id = tbl_item_substances.i_id
WHERE
tbl_item.o_id = 1
AND
tbl_item.date_valid_to IS NULL
AND
tbl_organisation_substances.app_id IS NOT NULL
ORDER BY
tbl_organisation_substances.app_id ASC
) AS myalias
WHERE myalias.app_id = 'ID000001'
This does a COUNT() where the app_id is ID000001.
So in the array I've given previously I need to add something to the array to hold this, e.g.
'substance' => [
// ...
],
'count_app_ids' => 5
(Assuming there were 5 rows returned by the query above).
I have Table classes for all of the tables referred to in the above query.
So my question is, how do you write this using the ORM, and add the result back to the result set before the query is executed?
Is this even possible? The only other solution I can think of is to write the data (from the query I have that works) to a temporary table and then perform successive queries which UPDATE with the count figure based on the app_id. But I'm really not keen on that solution because there are potentially huge performance problems of doing this. Furthermore I'd like to be able to paginate my query so ideally need everything confined to 1 SQL statement, even if it's done across multiple finders.
I've tagged this with MySQL as well as CakePHP because I'm not even sure if this is achievable from a MySQL perspective although it does look on the linked SO post like it can be done? This has the added complexity of having to write the equivalent query using Cake's ORM.
Related
Hello Friends I am using the following query :
$cur_date = date('Y-m-d');
$clientTemp = DB::table('clients')->where('quotations.exp_date','<',$cur_date)
->join('quotations','quotations.customer_id','=','clients.id')
->get()
->map(function ($clientTemp) {
return [
'id' => $clientTemp->id,
'hash' => $clientTemp->hash,
'name' => $clientTemp->first_name.' '.$clientTemp->last_name,
'email' => $clientTemp->email,
'mobile' => $clientTemp->mobile
];
});
I am getting this data from two tables :
1. Qutoations and 2. Clients.
In quotations table if the exp_date is less than current date then the details will be fetched from client table.
But there is possibility then there are more than 1 rows in quotation table but I want to fetch only one table from that for which customer_id is unique. How can I fetch unique row with same customer_id from quotations table
You'd need to use a GROUP BY clause but due to MySQL's default ONLY_FULL_GROUP_BY mode, you must aggregate any column that has more than one value.
You don't seem to be actually using any values from quotations, so you could just add:
DB::table('clients')->select('clients.*')->groupBy('clients.id')...
Otherwise, you'd need to tell MySQL how to aggregate any rows that have multiple values, like:
DB::table('clients')->selectRaw('clients.*, MIN(quotations.id)')->groupBy('clients.id')...
You should use groupby
$cur_date = date('Y-m-d'); $clientTemp = DB::table('clients')->where('quotations.exp_date','<',$cur_date)
->join('quotations','quotations.customer_id','=','clients.id')
->->groupBy('quotations.customer_id')
->get()
->map(function ($clientTemp) {
return [
'id' => $clientTemp->id,
'hash' => $clientTemp->hash,
'name' => $clientTemp->first_name.' '.$clientTemp->last_name,
'email' => $clientTemp->email,
'mobile' => $clientTemp->mobile
];
});
I'm trying to receive some ids from my database for an autocomplete search on my CAKEPHP 3.3 site. But my problem is that its only returning the id if I type in the exact id and not part of it.
Here is my function to search the data. The name variable is what is being passed from input.
public function search()
{
if ($this->request->is('ajax'))
{
$name = $this->request->query['term'];
$resultArr = $this->Invoices->find('all', [
'conditions' => ['Invoices.id LIKE' => ($name . '%')]
]);
$resultsArr = [];
foreach ($resultArr as $result)
{
$resultsArr[] = ($result['id']);
}
$this->set('resultsArr', $resultsArr);
// This line is what handles converting your array into json
// To get this to work you must load the request handler
$this->set('_serialize', ['resultsArr']);
}
}
For example there is a id in the table '5254' and I type in part of the id '52' nothing is returned but when I type in the whole id '5254' the id is returned.
I'm unsure why this is the case because in my sql query i'm using the percent sign to say any characters after what has been typed into the input.
Here is part of my table
SQL debug when 52 is entered.
object(Cake\ORM\Query) {
'(help)' => 'This is a Query object, to get the results execute or iterate it.',
'sql' => 'SELECT Invoices.id AS `Invoices__id`, Invoices.start_date AS `Invoices__start_date`, Invoices.close_date AS `Invoices__close_date`, Invoices.customer_id AS `Invoices__customer_id`, Invoices.invoice_to_address AS `Invoices__invoice_to_address`, Invoices.ship_to_address AS `Invoices__ship_to_address`, Invoices.customer_contact_id AS `Invoices__customer_contact_id`, Invoices.aircraft_registration_id AS `Invoices__aircraft_registration_id`, Invoices.shipping_company_id AS `Invoices__shipping_company_id`, Invoices.notes AS `Invoices__notes`, Invoices.worksheet_notes AS `Invoices__worksheet_notes`, Invoices.closed AS `Invoices__closed`, Invoices.times_printed AS `Invoices__times_printed`, Invoices.payment_due AS `Invoices__payment_due`, Invoices.GST_rate AS `Invoices__GST_rate`, Invoices.opening_notes AS `Invoices__opening_notes`, Invoices.courier_ticket AS `Invoices__courier_ticket`, Invoices.job_description AS `Invoices__job_description`, Invoices.worksheets_printed AS `Invoices__worksheets_printed`, Invoices.supervising_engineer_id AS `Invoices__supervising_engineer_id`, Invoices.job_type_id AS `Invoices__job_type_id`, Invoices.opened_by_id AS `Invoices__opened_by_id`, Invoices.assigned_to_id AS `Invoices__assigned_to_id`, Invoices.certification_required AS `Invoices__certification_required`, Invoices.currency_id AS `Invoices__currency_id`, Invoices.xero_batch_number AS `Invoices__xero_batch_number`, Invoices.xero_amount AS `Invoices__xero_amount`, Invoices.exchange_rate AS `Invoices__exchange_rate`, Invoices.payment_instructions AS `Invoices__payment_instructions`, Invoices.email AS `Invoices__email`, Invoices.inv_email AS `Invoices__inv_email` FROM invoices Invoices WHERE Invoices.id like :c0',
'params' => [
':c0' => [
'value' => '52%',
'type' => 'integer',
'placeholder' => 'c0'
]
The id column is of type INTEGER, and therefore the value is being bound as such, as can be seen in your Query dump, it says 'type' => 'integer'. Being bound as an integer will cause it to be casted, and you'll end up with a comparison against 52 only.
You can workaround that by telling the query builder to treat the column as a string type. This can be done via the second argument ($types) of the query builders *where() methods:
$this->Invoices
->find()
->where(
['Invoices.id LIKE' => ($name . '%')],
['Invoices.id' => 'string']
);
See also
API > \Cake\ORM\Query::where()
In this case You can "inject" plain query - array values with numeric index in conditions are treated as plain query, and it will not be parametrized. Be carefull: Typecast to integer is necessary in this case to prevent SQL Injection:
$result = $this->Invoinces->find('all' , [
'conditions' => [
'id LIKE "'.(int)$input.'%" '
]
])
->toArray();
Try it like this:
'conditions' => ['Invoices.id LIKE' => '"' . $name . '%"']
you can still do it like this in cakephp 3
$results = $clients->find()->select(['id','email','name','accountid','created','status'])
->Where(function (QueryExp $exp, Query $q) use ($requestData) {
$orCond = $exp->or_([
new Comparison('accountid',$requestData['search']['value'],null,'LIKE'),
new Comparison('email',$requestData['search']['value'],null,'LIKE'),
new Comparison('name',$requestData['search']['value'],null,'LIKE'),
new Comparison('created',$requestData['search']['value'],null,'LIKE'),
new Comparison('status',$requestData['search']['value'],null,'LIKE'),
]);
return $exp->add($orCond);
});
I have a table with a field called 'year'. It has many repetitions in the column so I want to find the distinct group. I have 4 different 'years' in about 20 rows and I dont get these values from the query. Instead what is returned are 4 numbers which are not the years (5,14,4,70). The same code worked fine when I used this with suburb field in another table where there were multiple values of this field. I dont get why this isnt working.
//in view
echo $this->Form->input('year', ['label' => 'Year','options' => $allyears]);
//controller
$allyears = $this->TimesheetDates->find('list')
->select(['TimesheetDates.id', 'TimesheetDates.year'])
->group(['TimesheetDates.year'])->autoFields(true)
->order(['TimesheetDates.year'=> 'ASC'])
->hydrate(false);
$this->set('allyears',$allyears);
//another controller and this code worked fine
$suburb = $this->Students->find('list')->where(['Students.address_suburb !=' => '','Students.student_inactive' => 0])
->select(['Students.id','Students.address_suburb'])
->group(['Students.address_suburb'])->autoFields(true)
->order(['Students.address_suburb' => 'ASC'])
->hydrate(false);
take a look at the documentation about how find('list') works
$allyears = $this->TimesheetDates->find('list', [
'keyField' => 'id',
'valueField' => 'year']
)
->group(['year'])
->order(['year'=> 'ASC']);
Note that it has no meaning selecting the id of the TimesheetDates Table as you are grouping by year and the id is choosen randomly between all the records that share the same year
I have three working queries:
To find the rows with keyword in title field
$cursor = $collection->find(['title' => array('$regex'=>new MongoRegex($title_query))])->sort(array('timestamp'=>-1));
To find the rows with keyword in the author field
$cursor = $collection->find(['author' => array('$regex'=>new MongoRegex($author_query))])->sort(array('timestamp'=>-1));
To find the rows within a date range
$rangeQuery = array('timestamp' => array( '$gte' => $from_Id, '$lte' => $to_Id ));
$cursor = $collection->find($rangeQuery)->sort(array('timestamp'=>-1));
I want to combine the queries into 1&2, 1&3 and 2&3. However I am not able to write the correct query...
Here is my query for combining 1&2:
$cursor = $collection->find('title' => array('$regex'=>new MongoRegex($title_query)),
'author' => array('$regex'=>new MongoRegex($author_query)))->sort(array('timestamp'=>-1));
query for combining 1&3:
$rangeQuery = array('timestamp' => array( '$gte' => $from_Id, '$lte' => $to_Id ));
$cursor = $collection->find($rangeQuery, ['title' => array('$regex'=>new MongoRegex($title_query))])->sort(array('timestamp'=>-1));
Can anyone tell me how to write the correct query?
The $regex query operator should not be necessary if you are using the BSON regex type (i.e. MongoRegex in the PHP driver). Let's rewrite the original three queries:
Matching keyword in title, sorted by time descending:
$collection->find([
'title' => new MongoRegex($title_query),
])->sort(['timestamp' => -1]);
Matching keyword in author, sorted by time descending:
$collection->find([
'author' => new MongoRegex($author_query),
])->sort(['timestamp' => -1]);
Matching within a date range, sorted by time descending:
$collection->find([
'timestamp' => [
'$gte' => $from_Id,
'$lte' => $to_Id,
],
])->sort(['timestamp' => -1]);
There are several errors in the combined query examples you shared in the OP. For the "1&2" query, you were not passing an array as the first argument to find(), so that would have been a syntax error. For the "1&3" query, you're only passing the timestamp range as find() criteria, and the title regex is being incorrected passed as the second find() argument, which is reserved for specifying a project (i.e. which fields to return).
Combining the three queries is actually as easy as merging the criteria arrays. For example, we could combine all three like so:
$collection->find([
'title' => new MongoRegex($title_query),
'author' => new MongoRegex($author_query),
'timestamp' => [
'$gte' => $from_Id,
'$lte' => $to_Id,
],
])->sort(['timestamp' => -1]);
In some cases, it's not possible to merge criteria for the same field. For that reason, MongoDB has an $and query operator (see the examples for some use cases); however, in the examples above, the criteria is simple enough that you can simply combine the arrays.
I am trying to write a query using the doctrine query builder based on the parameters i am getting from an array.
Here's my array,
$query = array('field' => 'number,
'from' => '1',
'to' => '100',
'Id' => '2',
'Decimation' => '10'
);
The query that i am trying to write is,
select * from table where (number between 1 AND 100) AND (Id = 2) AND number mod 10 = 0
Here's where i stand now,
if (is_array($parameters['query'])) {
$queryBuilder->select()
->where(
$queryBuilder->expr()->between($parameters['query']['field'], $parameters['query']['from'], $parameters['query']['to']),
$queryBuilder->expr()->eq('Id', '=?1'),
$queryBuilder->expr()->eq($parameters['query']['field'],'mod 10 = 0')
)
->setParameter(array(1 => $parameters['query']['Id']));
}
I just cant wrap my head around this for some reason. Help !! anyone?
Not tested or anything, just directly typed into SO answer box:
$queryBuilder->select('table')
->from('My\Table\Entity', 'table')
->where($queryBuilder->expr()->andX(
$queryBuilder->expr()->between('table.number', ':from', ':to'),
$queryBuilder->expr()->eq->('table.id', ':id'),
))
->andWhere('MOD(table.number, :decimation) = 0')
->setParameters(array(
'from' => $parameters['query']['from'],
'to' => $parameters['query']['to'],
'id' => $parameters['query']['id'],
'decimation' => $parameters['query']['decimation']
));
It does not dynamically let you set which field to apply the where condition to. However, this is most likely a bad idea without whitelisting the values you want to allow. Once this is done a simple modification to the code above (just interpolate the value in place of number in the table.number string).
I kinda got it,
The main idea here was to use the andWhere clause. Multiple where clauses was what i wanted. and the mod operator that doctrine gives. Worked out pretty well. In between the requirement changed as well.