I need to count the users, but my condition is only if their account have been created today. I have a users table with a created field (datetime) for each rows. How can i do it in Cakephp, i didn't find the answer in the documentation.
$usersNewCount = Number::format($this->Users->find()->where(['created' => 'CURDATE()'])->count());
I tried with CURDATE, and of course it's not working, i guess Cakephp has a specific function for te datetime field ?
What you are doing there won't work for various reasons.
You cannot pass SQL snippets in the value part of the conditions array, it will be escaped and you'll end up with a string comparison like created = 'CURDATE()', you'd either have to pass the whole condition as a string, or use raw expressions.
Even when properly passing CURDATE(), the comparison won't work as the created column has a time part.
While it is possible to circumvent the former problem by transforming the column, you should try to avoid that whenever possible! Comparing to calculated columns like DATE(created) = CURDATE() will make using indices impossible, and thus massively degrade performance!
So unless you have an extra column that holds just the date part, your best bet is a BETWEEN comparison which is the equivalent to a >= x AND a <= y, and in order to stay cross DBMS compatible, this is best to be done by passing dates from PHP, ie not using DBMS specific date and time functions like CURDATE().
$this->Users
->find()
->where(function (\Cake\Database\Expression\QueryExpression $exp, \Cake\ORM\Query $query) {
$from = (new \DateTime())->setTime(0, 0, 0);
$to = (new \DateTime())->setTime(23, 59, 59);
return $exp->between('Users.created', $from, $to, 'datetime');
})
->count()
This will create a query similar to
SELECT
(COUNT(*)) AS `count`
FROM
users Users
WHERE
Users.created BETWEEN '2015-05-26 00:00:00' AND '2015-05-26 23:59:59'
See also
API > \Cake\Database\Expression\QueryExpression::between()
You can do it this way
$usersNewCount = Number::format($this->Users->find()->where([
'DATE(created) = CURDATE()'
])->count());
Note that passing it in form where(['DATE(created)' => 'CURDATE()']) will not work, since CURDATE() will be interpreted as a string.
When doing 'created' => 'CURDATE()' you are checking for a complete match, getting '2015-05-26', without a time. You need to check for a time interval:
$usersNewCount = Number::format(
$this->Users->find()->where([
'created >=' => date('Y-m-d').' 00:00:00',
'created <=' => date('Y-m-d').' 23:59:59'
])->count());
Related
I have the followwing problem with creating a query in CakePHP 3:
For the Entity Recordings I want to calculate the age as the difference between the year of the two dates "collection date" and "birthdate"
$query = $this->Recordings->find()
->select(['age' => 'Year(Recordings.collection_date) - Year(Athletes.birthdate)'])
->select($this->Recordings)
->select($this->Recordings->Athletes);
When I try to filter with the following where clause
$query = $query->where(['Year(Recordings.collection_date) - Year(Athletes.birthdate) =' => $age]);
The SQL Code which is created has the Athletes beeing modified to lowercase.
WHERE
( Year(Recordings.collection_date) - year(athletes.birthdate) = '17' )
How do I have to write the where clause correctly? I havent found a way to use Identifiers or query functions like
$year = $q->func()->year(['Athletes.birthdate' => 'identifier']);
to bild the conditions.
Any hints are welcome.
I am trying to use a CASE statement on the order of a MySQL statement in CakePHP 3.x app. The simple select is as follows:
$articles = $this->Articles->find()
->where($conditions)
->order(function ($exp, $q) {
return $exp->addCase(
[
$q->newExpr()->gt('Articles.modified', (new Time())->subDays(365)) // article has been updated in the last x days
],
['priority'], # values matching conditions
['string'] # type of each value
);
})
->limit(15)
->all();
The following SQL is generated:
SELECT `Articles`.`id` AS `Articles__id`, ....
FROM `articles` `Articles`
WHERE (`publish` < :c0 AND `Articles`.`publish` > :c1)
ORDER BY CASE WHEN `Articles`.`modified` > :c2 THEN :param3 END LIMIT 15
The case statement is not correct because it is missing the DESC order which should come after the 'END' - see this fiddle:
http://sqlfiddle.com/#!9/8df161/5
I'm not sure if this is a limitation with how CakePHP handles CASE?
Further I require a second order after the case statement to order by 'publish' desc.
Expressions passed to Query::order() must generate everything required by the ORDER BY clause, including the direction keyword.
If the expression that you're using doesn't support that, then you can use Query::oderAsc() or Query::oderDesc(), which will append the respective direction keyword accordingly.
$query = $this->Articles->find();
$query
->where($conditions)
->orderDesc(
$query->newExpr()->addCase(/* ... */)
)
// ...
See also
Cookbook > Database Access & ORM > Query Builder > Selecting Data
I have a datetime field in my model. In a query I want to select all rows created on a specific day/date (the time doesn't matter). What is the simplest way of doing that in CakePHP 3.8 ?
Per other answers, use of a custom function to cast the column with MySQL DATE() may slow your query down at scale. Besides, there's probably nothing simpler than plain arrays to build conditions:
$query = $this->YourTable->find()
->where([
'date_field >=' => '2020-01-01 00:00:0', // Or pass a Time() object..
'date_field <' => '2020-01-02 00:00:00',
]);
This kind of stuff is covered in the Query Builder docs.
I use a union to join two datasets and then the following query to setup for pagination correctly
$paginationQuery = $this->find('all')
->contain(['EmailAddresses' => [
'foreignKey' => false,
'queryBuilder' => function($q) {
return $q->where(['Members__id' => 'EmailAddresses.member_id']);
}
]])
->select( $selectMainUnion )
->from([$this->getAlias() => $query])
->order(['Members__last_name' => 'ASC', 'Members__first_name' => 'ASC']);
I have also tried
$paginationQuery = $this->find('all')
->contain(['EmailAddresses'])
->select( $selectMainUnion )
->from([$this->getAlias() => $query])
->order(['Members__last_name' => 'ASC', 'Members__first_name' => 'ASC']);
and tried
$query->loadInto($query, ['EmailAddresses']); where $query is the result of the union.
Neither of these result in email addresses added to $paginationQuery.
Is there a way to do this?
Adding to clarify the code
$selectMain =['Members.id',
'Members.member_type',
'Members.first_name',
'Members.middle_name',
'Members.last_name',
'Members.suffix',
'Members.date_joined'];
foreach($selectMain as $select) {
$selectMainUnion[] = str_replace('.', '__', $select);
}
$this->hasMany('EmailAddresses', [
'foreignKey' => 'member_id',
'dependent' => true,
]);
Looking at the SQL in DebugKit SQL Log, there is no reference to the EmailAddresses table.
Generally containments do work fine irrespective of the queries FROM clause, whether that's a table or a subquery should be irrelevant. The requirement for this to work however is that the required primary and/or foreign key fields are being selected, and that they are in the correct format.
By default CakePHP's ORM queries automatically alias selected fields, ie they are being selected like Alias.field AS Alias__field. So when Alias is a subquery, then Alias.field doesn't exist, you'd have to select Alias.Alias__field instead. So with the automatic aliases, your select of Members__id would be transformed to Members.Members__id AS Members__Members__id, and Members__Members__id is not something the ORM understands, it would end up as Members__id in your entities, where the eager loader would expect id instead, ie the name of the primary key which is used to inject the results of the queried hasMany associated records (this happens in a separate query), your custom queryBuilder won't help with that, as the injecting happens afterwards on PHP level.
Long story short, to fix the problem, you can either change how the fields of the union queries are selected, ie ensure that they are not selected with aliases, that way the pagination query fields do not need to be changed at all:
$fields = $table->getSchema()->columns();
$fields = array_combine($fields, $fields);
$query->select($fields);
This will create a list of fields in the format of ['id' => 'id', ...], looks a bit whacky, but it works (as long as there's no ambiguity because of joined tables for example), the SQL would be like id AS id, so your pagination query can then simply reference the fields like Members.id.
Another way would be to select the aliases of the subquery, ie not just select Member__id, which the ORM turns into Member__Member__id when it applies automatic aliasing, but use Members.Member__id, like:
[
'Member__id' => 'Members.Member__id',
// ...
]
That way no automatic aliasing takes place, on SQL level it would select the field like Members.Member__id AS Member__id, and the field would end up as id in your entities, which the eager loader would find and could use for injecting the associated records.
I'm a little surprised I haven't found any information on the following question, so please excuse if I've missed it somewhere in the docs. Using SQL Server (2016 locally and Azure) and EFCore Code First we're trying to create a computed table column with a persisted value. Creating the column works fine, but I don't have a clue how to persist the value. Here's what we do:
modelBuilder.Entity<SomeClass>(entity =>
{
entity.Property(p => p.Checksum)
.HasComputedColumnSql("(checksum([FirstColumnName], [SecondColumnName]))");
});
And here is what we'd actually like to get in T-SQL:
CREATE TABLE [dbo].[SomeClass]
(
[FirstColumnName] [NVARCHAR](10)
, [SecondColumnName] [NVARCHAR](10)
, [Checksum] AS (CHECKSUM([FirstColumnName], [SecondColumnName])) PERSISTED
);
Can anyone point me in the right direction?
Thanks in advance, Tobi
UPDATE: Based on a good idea by #jeroen-mostert I also tried to just pass the PERSISTED string as part of the formula:
modelBuilder.Entity<SomeClass>(entity =>
{
entity.Property(p => p.Checksum)
.HasComputedColumnSql("(checksum([FirstColumnName], [SecondColumnName]) PERSISTED)");
});
And also outside of the parentheses:
modelBuilder.Entity<SomeClass>(entity =>
{
entity.Property(p => p.Checksum)
.HasComputedColumnSql("(checksum([FirstColumnName], [SecondColumnName])) PERSISTED");
});
However und somehow surprisingly, the computed column is still generated with Is Persisted = No, so the PERSISTED string simply seems to be ignored.
Starting with EF Core 5, the HasComputedColumnSql method has a new optional parameter bool? stored to specify that the column should be persisted:
modelBuilder.Entity<SomeClass>()
.Property(p => p.Checksum)
.HasComputedColumnSql("checksum([FirstColumnName], [SecondColumnName])", stored: true);
After doing some reading and some tests, I ended up trying the PERSISTED inside the SQL query and it worked.
entity.Property(e => e.Duration_ms)
.HasComputedColumnSql("DATEDIFF(MILLISECOND, 0, duration) PERSISTED");
The generated migration was the following:
migrationBuilder.AddColumn<long>(
name: "duration_ms",
table: "MyTable",
nullable: true,
computedColumnSql: "DATEDIFF(MILLISECOND, 0, duration) PERSISTED");
To check on the database whether it is actually persisted I ran the following:
select is_persisted, name from sys.computed_columns where is_persisted = 1
and the column that I've created is there.
" You may also specify that a computed column be stored (sometimes called persisted), meaning that it is computed on every update of the row, and is stored on disk alongside regular columns:"
modelBuilder.Entity<SomeClass>(entity =>
{
entity.Property(p => p.Checksum)
.HasComputedColumnSql("(checksum([FirstColumnName], [SecondColumnName]), stored: true);
});
This is taken (and slightly modified) from Microsoft Docs.: https://learn.microsoft.com/en-us/ef/core/modeling/generated-properties?tabs=data-annotations#computed-columns