I'm absolutely new to Yii2 framework. I'm trying to learn filtering, but somehow I don't understand it. I've looked in the documentation, but it didn't helped too. Could someone explain to me what this function does step by step?
public function filteringValues($query)
{
$query->andFilterWhere([
'>',
'table1.column1',
date('Y-m-d H:i:s'),
]);
}
}
just like the doc points out, andFilterWhere applies a condition if the operands are not empty
in your particular case (since date('Y-m-d H:i:s') always returns a value),
$query->andFilterWhere(['>', 'table1.column1', date('Y-m-d H:i:s')]);
will be equivalent to
$query->andWhere(['>', 'table1.column1', date('Y-m-d H:i:s')]);
which is translated to sql to a condtion like
AND (`table1`.`column1`) > '2017-08-12 06:10:32'
a propper use case for andFilterWhere is when comparing with an optional value (received as a filter param).
$query->andFilterWhere(['>', 'table1.column1', $date]);
it's purpose is to not have to check whether $date is empty or not
$query->andFilterWhere([
'table1.column1' => $param1,
'table1.column2' => $param2,
'table1.column3' => $param3,
]);
in this example, the query applies a condtion only for the params that are not empty, ignoring the extra ones
Related
I am having a hard time trying to figure out how to get a sub-query working.
Imagine I have:
$schools
->select($this->Schools)
->select([
'pupilcount' => $this->Pupils
->find()
->select([
$this->Pupils->find()->func()->count('*')
])
->where([
'Pupils.school_id' => 'Schools.id',
]),
The problem I am experiencing (I think) is that Schools.id is always 0 and so the count is returned as 0. I can pull out the Pupils join and it shows Pupils there.
I tried changing my code to add a:
->select(['SCID' => 'Schools.id'])
and reference that in the sub-query but doesn't work, it will always return 0 for the pupilcount.
What am I doing wrong here?
Whenever encountering query problems, check what queries are actually being generated (for example using DebugKit). Unless being an expression object, the right hand side of a condition will always be bound as a parameter, ie you're comparing against a string literal:
Pupils.school_id = 'Schools.id'
Generally for proper auto quoting compatibility, column names should be identifier expressions. While the left hand side will automatically be handled properly, the right hand side would require to be handled manually.
In your specific case you could easily utilize QueryExpression::equalFields(), which is ment for exactly what you're trying to do, comparing fields/columns:
->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
return $exp->equalFields('Pupils.school_id', 'Schools.id');
})
It's also possible to create identifier expressions manually by simply instantiating them:
->where([
'Pupils.school_id' => new \Cake\Database\Expression\IdentifierExpression('Schools.id')
])
or as of CakePHP 3.6 via the Query::identifier() method:
->where([
'Pupils.school_id' => $query->identifier('Schools.id')
])
And finally you could also always pass a single string value, which is basically inserted into the query as raw SQL, however in that case the identifiers will not be subject to automatic identifier quoting:
->where([
'Pupils.school_id = Schools.id'
])
See also
Cookbook > Database Access & ORM > Query Builder > Advanced Conditions
API > \Cake\Database\Expression\QueryExpression::equalFields()
API > \Cake\Database\Expression\IdentifierExpression
I run into this problem, time and time again. It would be nice to find out how to build queries properly so I can stop resorting to Yii::$app->db->createCommand() as a workaround.
My Yii2 query:
$users = UserSpatial::find()
->select('user_id, harvesine(y(coordinates), x(coordinates), :lat, :lon) as dist, astext(coordinates)')
->where('st_within(coordinates, envelope(linestring(point(:rlon1, :rlat1), point(:rlon2, :rlat2))))')
->orderBy('st_distance(point(:lon, :lat), coordinates)')
->params([
':lon' => $geo->lon,
':lat' => $geo->lat,
':rlon1' => $rlon1,
':rlat1' => $rlat1,
':rlon2' => $rlon2,
':rlat2' => $rlat2
])
->all();
The generated query ends up with backticks in all the wrong places and, oddly enough, not all parameters were backticked (sorry but you'll need to look closely for the misplaced backticks because I didn't know how best to highlight the incorrect placements):
SELECT \`user_id\`, harvesine(y(coordinates), x(coordinates), \`32.7699547\`, \`-116.9911288)\` AS \`dist\`, astext(coordinates)
FROM \`user_spatial\`
WHERE st_within(coordinates, envelope(linestring(point(-117.07730792871, 32.697490931884), point(-116.90494967129, 32.842418468116))))
ORDER BY st_distance(point(-116.9911288, \`32.7699547)\`, \`coordinates)\`
The query should look like the following as I did not wrap double-square-brackets around any of the fields or values:
SELECT \`user_id\`, harvesine(y(coordinates), x(coordinates), 32.7699547, -116.9911288) AS dist, astext(coordinates)
FROM \`user_spatial\`
WHERE st_within(coordinates, envelope(linestring(point(-117.07730792871, 32.697490931884), point(-116.90494967129, 32.842418468116))))
ORDER BY st_distance(point(-116.9911288, 32.7699547), coordinates)
I can live with Yii2 adding some backticks around field names and table names but why on earth is it backticking numerical values? (FYI: the $rlon and $rlat values don't seem to get backticked but I was assuming that was because they are a result of math calculations!?!?).
I've already tried forcing $geo->lon and $geo->lat to float values like so:
'lon' => (float)$geo->lon;
or
'lon' => (float)$geo->lon * 1;
but it didn't help.
Try to use array format for select and orderBy methods, like docs suggest:
Besides column names, you can also select DB expressions. You must use
the array format when selecting a DB expression that contains commas
to avoid incorrect automatic name quoting. For example,
$query->select(["CONCAT(first_name, ' ', last_name) AS full_name",
'email']);
In you case it will be like this:
$users = UserSpatial::find()
->select([
'user_id',
'harvesine(y(coordinates), x(coordinates), :lat, :lon) as dist',
'astext(coordinates)'
])
->where('st_within(coordinates, envelope(linestring(point(:rlon1, :rlat1), point(:rlon2, :rlat2))))')
->orderBy(['st_distance(point(:lon, :lat)', 'coordinates)'])
->params([
':lon' => $geo->lon,
':lat' => $geo->lat,
':rlon1' => $rlon1,
':rlat1' => $rlat1,
':rlon2' => $rlon2,
':rlat2' => $rlat2
])
->all();
I have a timestamp behavior (yii2) that looks like this (taken from user guide example)...
public function behaviors()
{
return [
'timestamp' => [
'class' => TimestampBehavior::className(),
'value' => new Expression('NOW()'),
],
];
}
But is it not better to use a PHP DateTime expression like...
'value' => (new /DateTime('NOW'))->format('Y-m-d H:i:s')
Seems to me like it would be better to work with one clock rather than possibly two (in case the database server is separate and time not in sync, which is not unlikely.) Especially if I'm using DateTime in my code to check other conditions.
Which approach is better and why?
Expression class created a DBMS function inside the SQL query, it works no matter which date type (or format, it is possible to force the date format in some DBMS) you have in your column. If you change the value of a behavior you should take care the format and type of your column does match, otherwise some time you would get an error.
I'm trying to use the "HAVING" clause in a SQL query using the CakePHP paginate() method.
After some searching around it looks like this can't be achieved through Cake's paginate()/find() methods.
The code I have looks something like this:
$this->paginate = array(
'fields' => $fields,
'conditions' => $conditions,
'recursive' => 1,
'limit' => 10,
'order' => $order,
'group' => 'Venue.id');
One of the $fields is an alias "distance". I want to add a query for when distance < 25 (e.g. HAVING distance < 25).
I have seen two workarounds so far, unfortunately neither suit my needs. The two I've seen are:
1) Adding the HAVING clause in the "group" option. e.g. 'group' => 'Venue.id HAVING distance < 25'. This doesn't seem to work when used in conjunction with pagination as it messes up the initial count query that is performed. (ie tries to SELECT distinct(Venue.id HAVING distance < 25) which is obviously invalid syntax.
2) Adding the HAVING clause after the WHERE condition (e.g. WHERE 1 = 1 HAVING field > 25) This doesn't work as it seems the HAVING clause must come after the group statement which Cake is placing after the WHERE condition in the query it generates.
Does anyone know of a way to do this with CakePHP's find() method? I don't want to use query() as that would involve a lot of rework and also mean I'd need to implement my own pagination logic!
Thanks in advance
You have to put it with the group conditions. like this
$this->find('all', array(
'conditions' => array(
'Post.length >=' => 100
),
'fields' => array(
'Author.id', 'COUNT(*) as Total'
),
'group' => array(
'Total HAVING Total > 10'
)
));
Hope it helps you
I used the following trick to add my own HAVING clause at the end of my WHERE clause. The "dbo->expression()" method is mentioned in the cake sub-query documentation.
function addHaving(array $existingConditions, $havingClause) {
$model = 'User';
$db = $this->$model->getDataSource();
// Two fun things at play here,
// 1 - mysql doesn't allow you to use aliases in WHERE clause
// 2 - Cake doesn't allow a HAVING clause separate from a GROUP BY
// This expression should go last in the WHERE clause (following the last AND)
$taut = count($existingConditions) > 0 ? '1 = 1' : '';
$having = $db->expression("$taut HAVING $havingClause");
$existingConditions[] = $having;
return $existingConditions;
}
As per the manual, CakePHP/2 supports having at last. It was added as find array parameter on version 2.10.0, released on 22nd July 2017.
From the 2.10 Migration Guide:
Model::find() now supports having and lock options that enable you to
add HAVING and FOR UPDATE locking clauses to your find operations.
Just had the same problem. I know, one is not supposed to modify the internal code but if you open the PaginatorComponent and you modify line 188:
$count = $object->find('count', array_merge($parameters, $extra));
to this:
$count = $object->find(
'count',
array_merge(array("fields" => $fields),$parameters, $extra)
);
Everything will be fixed. You will be able to add your HAVING clause to the 'group' and the COUNT(*) won't be a problem.
Or, make line:
$count = $object->paginateCount($conditions, $recursive, $extra);
to include the $fields:
$count = $object->paginateCount($fields,$conditions, $recursive, $extra);
After that, you can "override" the method on the Model and make sure to include the $fields in the find() and that's it!, =P
Here is another idea that doesn't solve the pagination issue, but it is clean since it just overrides the find command in AppModel. Just add a group and having element to your query and this will convert to a HAVING clause.
public function find($type = 'first', $query = array()) {
if (!empty($query['having']) && is_array($query['having']) && !empty($query['group'])) {
if ($type == 'all') {
if (!is_array($query['group'])) {
$query['group'] = array($query['group']);
}
$ds = $this->getDataSource();
$having = $ds->conditions($query['having'], true, false);
$query['group'][count($query['group']) - 1] .= " HAVING $having";
CakeLog::write('debug', 'Model->find: out query=' . print_r($query, true));
} else {
unset($query['having']);
}
}
return parent::find($type, $query);
}
Found it here
https://groups.google.com/forum/?fromgroups=#!topic/tickets-cakephp/EYFxihwb55I
Using 'having' in find did not work for me. Instead I put into one string with the group
" group => product_id, color_id having sum(quantity) > 2000 " and works like a charm.
Using CakePHP 2.9
I have a large data set (over a billion rows). The data is partitioned in the database by date. As such, my query tool MUST specify an SQL between clause on every query, or it will have to scan every partition.. and well, it'll timeout before it ever comes back.
So.. my question is, the field in the database thats partitioned is a date..
Using CakePHP, how can I specify "between" dates in my form?
I was thinking about doing "start_date" and "end_date" in the form itself, but this may bring me two a second question.. how do I validate that in a model which is linked to a table?
If I am following you correctly:
The user must specify start/end dates for find queries generated from a form
You need to validate these dates so that, for example:
end date after start date
end date not centuries away from start date
You want validation errors appearing inline within the form (even though this isn't a save)
Since you want to validate these dates they will be harder to grab when they are tucked away inside your conditions array. I suggest trying to pass these in separately and then dealing with them later:
$this->Model->find('all', array(
'conditions' => array(/* normal conditions here */),
'dateRange' => array(
'start' => /* start_date value */,
'end' => /* end_date value */,
),
));
You should hopefully be able to handle everything else in the beforeFind filter:
public function beforeFind() {
// perform query validation
if ($queryData['dateRange']['end'] < $queryData['dateRange']['start']) {
$this->invalidate(
/* end_date field name */,
"End date must be after start date"
);
return false;
}
/* repeat for other validation */
// add between condition to query
$queryData['conditions'][] = array(
'Model.dateField BETWEEN ? AND ?' => array(
$queryData['dateRange']['start'],
$queryData['dateRange']['end'],
),
);
unset($queryData['dateRange']);
// proceed with find
return true;
}
I have not tried using Model::invalidate() during a find operation, so this might not even work. The idea is that if the form is created using FormHelper these messages should make it back next to the form fields.
Failing that, you might need to perform this validation in the controller and use Session::setFlash(). if so, you can also get rid of the beforeFind and put the BETWEEN condition array in with your other conditions.
if you want to find last 20 days data .
$this->loadModel('User');
//$this->User->recursive=-1;
$data=$this->User->find('all', array('recursive' => 0,
'fields' => array('Profile.last_name','Profile.first_name'),'limit' => 20,'order' => array('User.created DESC')));
other wise between two dates
$start = date('Y-m-d') ;
$end = date('Y-m-d', strtotime('-20 day'));
$conditions = array('User.created' =>array('Between',$start,$end));
$this->User->find("all",$conditions)
You could write a custom method in your model to search between the dates:
function findByDateRange($start,$end){
return $this->find('all',array('date >= '.$start,'data >= .'$end));
}
As far as validating, you could use the model's beforeValidate() callback to validate the two dates. More info on this here.
function beforeValidate(){
if(Validation::date($this->data['Model']['start_date'])){
return false;
}
if(Validation::date($this->data['Model']['end_date'])){
return false;
}
return parent::beforeValidate();
}
Does that answer your question?