With CakePHP 2.x and MySQL 5.6.40 i'm creating the following pagination:
$options = array(
'contain' => array(
'Category',
'ProductImage'
),
'limit' => 10,
'order' => array(
'Product.year' => 'DESC',
'Product.month' => 'DESC'
)
);
But i forgot my products.month column is varchar so i have to do a cast to convert the Product.month to integer:
$options = array(
'contain' => array(
'Category',
'ProductImage'
),
'limit' => 10,
'order' => array(
'Product.year' => 'DESC',
'CAST(Product.month as INT)' => 'DESC'
)
);
But on the Query output, it removes the Order By CAST as INT
SELECT
...
FROM
`web_quality`.`products` AS `Product`
LEFT JOIN `web_quality`.`categories` AS `Category` ON (
`Product`.`category_id` = `Category`.`id`
)
WHERE
`Product`.`category_id` IN (
SELECT
id
FROM
categories
WHERE
tag = 'revistas'
AND company_id IN (
SELECT
id
FROM
companies
WHERE
tag = 'dr'
)
)
AND NOT (
(
(`Product`.`year` = '2022')
AND (`Product`.`month` = '2')
)
)
AND `Product`.`enabled` = '1'
ORDER BY
`Product`.`year` DESC,
LIMIT 10
Also it doesn't generate any kind of error, if i try manually the query with the CAST as INT it works, but i have to do it using the CakePHP Model, what could be wrong ?
Thank you so much !
From what I remember you should use a virtual field for that, as the paginator checks whether the given field exists on the model, and whitelisting an SQL snippet doesn't seem like an overly good idea.
So in your model you'd do something like:
public $virtualFields = array(
'month_number' => 'CAST(Product.month as INT)',
);
and then you order by Product.month_number.
See also
Cookbook > Models > Virtual fields
Cookbook > Core Libraries > Components > Pagination > Control which fields used for ordering
Related
I have a Venue table with 20+ columns.
I also have a favorite_venues table with columns
id | userId | venueId
Here is my code to get venues
$this->Venue->virtualFields = array(
'bookedCount' => "SELECT count(*) FROM bookings WHERE venueId = Venue.id"
);
$result = $this->Venue->find('all', array('conditions'=>$conditions,
'order' => array('Venue.bookedCount DESC'),
'limit' => 20,
'offset' => $offset * 20
));
i want to add a condition if i send userId, it should check every venue if its added to favourite list or not and set
$venue['isFavorite'] = yes/no
I dont want a for loop and check every venue i get. is there any way i can incorporate in same Mysql query in cakephp. I am not sure how to put yes/no as virtual field
You can LEFT join in your favorite_venues table, and then for example use a CASE statement in a virtual field that checks whether there is a linked row.
Here's an untested example to illustrate what I mean. I don't know how the user ID is involved, so you got to figure that on your own.
$this->Venue->virtualFields = array(
'bookedCount' => "SELECT count(*) FROM bookings WHERE venueId = Venue.id",
'isFavorite' => 'CASE WHEN FavoriteVenues.id IS NOT NULL THEN "yes" ELSE "no" END'
);
$result = $this->Venue->find('all', array(
'conditions' => $conditions,
'order' => array('Venue.bookedCount DESC'),
'limit' => 20,
'offset' => $offset * 20,
'joins' => array(
array(
'table' => 'favorite_venues',
'alias' => 'FavoriteVenues',
'type' => 'LEFT',
'conditions' => array(
'FavoriteVenues.venueId = Venue.id',
)
)
),
// don't forget to group to prevent duplicate results
'group' => 'Venue.id'
));
See also
Cookbook > Models > Virtual Fields > Virtual fields set in controller with JOINS
Cookbook > Models > Associations: Linking Models Together > Joining Tables
MySQL 5.7 Reference Manual / SQL Statement Syntax / ... / CASE Syntax
How can I transform the SQL statement into the CakePHP Code? I am able to get the group by result. But how can I add the inner Select Count(*) statement?
I've updated the code under "Revised" However, I am getting this error. Parse error: syntax error, unexpected 'INCOMPLETE' (T_STRING), expecting ')'. I am sure I check my open and close brackets.
SQL Statement below
SELECT
`id` ,
`mtpToken` ,
`mtpCreator_id` ,
`school_id` ,
`user_id` ,
`mtpStatus` ,
`created` ,
`modified` ,
(
(SELECT COUNT( * ) FROM mtpScreenings
WHERE (mtpStatus = 'INCOMPLETE' OR mtpStatus = 'HALFWAY') AND mtpToken = `mtpToken` )
) AS `mainStatus`
FROM `mtpScreenings` WHERE `mtpScreenings`.`school_id` = 15 GROUP BY `mtpToken`
CakePHP Code below
$setting = $this->paginate = array(
conditions' => array('MtpScreening.school_id' => 15),
'recursive' => -1,
'fields' => array('MtpScreening.field1'),
'group' => array('MtpScreening.mtpToken'),
'limit' => 1000
);
Revised
$setting = $this->paginate = array(
conditions' => array('MtpScreening.school_id' => 15),
'recursive' => -1,
'fields' => array('SELECT COUNT(*) FROM mtpScreenings
WHERE (mtpStatus = "INCOMPLETE" OR mtpStatus = "HALFWAY") AND mtpToken = `mtpToken`
) AS mainStatus'),
'group' => array('MtpScreening.mtpToken'),
'limit' => 1000
);
Virtual Field
Code example of virtual field:
public $virtualFields = array(
'count' => "SELECT COUNT( * ) FROM MtpScreening
WHERE (MtpScreening.mtpStatus = 'INCOMPLETE' OR MtpScreening.mtpStatus = 'HALFWAY') AND MtpScreening.mtpToken = 'mtpToken'"
);
This code will produce virtual field:
$this->find('all', array(
'fields'=>array(
'MtpScreening.id',
'MtpScreening.mtpToken',
'MtpScreening.mtpCreator_id',
'MtpScreening.school_id',
'MtpScreening.count'/*virtual field*/
)
)
);
Pagination and virtual fields
Since virtual fields behave much like regular fields when doing
find’s, Controller::paginate() will be
able to sort by virtual fields too.
OK, another cake question from me.
I have rather a complex table structure with quite a few joins. Here's the jist of it:
So, in summary:
RiskCategory $hasMany Client [Client $belongsTo RiskCategory]
Client $hasMany Account [Account $belongsTo Client]
Account $hasMany Holding [Holding $belongsTo Account]
In my RiskCategory model I want to run a query that basically returns the sum of the holdings in each riskCategory id, grouped by date. There's a few other conditions but here is what I've put together:
$findParams = array(
'recursive' => 4,
'fields' => array(
'Holding.holding_date',
'SUM(Holding.value) AS risk_category_value'
),
'group' => array('Holding.holding_date'),
'order' => 'Holding.holding_date ASC'
);
$findParams['conditions'] = array(
'Client.active' => true,
'Client.model' => true,
'Client.currency' => 'GBP',
'OR' => array(
'Holding.holding_date' => $this->end_date,
array(
'Holding.holding_date = LAST_DAY(Holding.holding_date)',
'MONTH(Holding.holding_date)' => array(3,6,9,12)
)
)
);
$valuations = $this->Client->Account->Holding->find( 'all', $findParams );
When I run the above cake is giving me an error saying various fields are not present in the query. The raw query created is as follows:
SELECT `Holding`.`holding_date`,
Sum(`Holding`.`value`) AS risk_category_value
FROM `ips_client_db`.`holdings` AS `Holding`
LEFT JOIN `ips_client_db`.`accounts` AS `Account`
ON ( `Holding`.`account_id` = `Account`.`id` )
LEFT JOIN `ips_client_db`.`sedols` AS `Sedol`
ON ( `Holding`.`sedols_id` = `Sedol`.`id` )
WHERE `client`.`active` = '1'
AND `client`.`model` = '1'
AND `client`.`currency` = 'GBP'
AND ( ( `Holding`.`holding_date` = '2013-10-01' )
OR (( ( `Holding`.`holding_date` = Last_day(
`Holding`.`holding_date`) )
AND ( Month(`Holding`.`holding_date`) IN ( 3, 6, 9, 12 ) ) )
) )
GROUP BY `Holding`.`holding_date`
ORDER BY `Holding`.`holding_date` ASC
It looks as though cake is not doing all the joins. It is only joining Account to Holding and then Holding to Sedol (which is another joined table in the database but is not needed for this query so I've omitted it from the diagram)
Why are the joins not being made properly and how to acheive this? I'd like to avoid writing a raw statement if possible.
EDIT: The joins should be as follows:
...
FROM risk_categories
LEFT JOIN ((clients
LEFT JOIN accounts
ON clients.id = accounts.client_id)
LEFT JOIN holdings
ON accounts.id = holdings.account_id)
ON risk_categories.id = clients.risk_category_id
...
CakePHP does not perform deep joins for associations that are more than 1 level deep. That is why you're getting an error for references to the Client table in the conditions.
For these types of problems it's easier to use the Containable behavior. I usually add this behavior to my AppModel as the default handler for associations.
Containable allows you to define the associations (only those already defined) with their fields and conditions as part of the find operation. This is done by adding a new contain key in the find parameters. Below I've taking a guess at what you might need.
$findParams = array(
'fields' => array(
'Holding.holding_date',
'SUM(Holding.value) AS risk_category_value'
),
'conditions'=>array(
'OR' => array(
'Holding.holding_date' => $this->end_date,
array(
'Holding.holding_date = LAST_DAY(Holding.holding_date)',
'MONTH(Holding.holding_date)' => array(3,6,9,12)
)
)
),
'group' => array('Holding.holding_date'),
'order' => 'Holding.holding_date ASC',
'contain'=>array(
'Account'=>array(
'Client'=>array(
'RiskCategory',
'conditions'=>array(
'Client.active' => true,
'Client.model' => true,
'Client.currency' => 'GBP',
)
)
)
)
);
$valuations = $this->Client->Account->Holding->find( 'all', $findParams );
No matter what I do I can't get it to respect the order I specify.
$this->paginate = array(
'Car' => array(
'limit' => 6,
'order' => array(
'Car.year' => 'desc'
),
'table' => 'cars'
)
);
Generated SQL:
SELECT `Car`.`id`, ... `Car`.`year`,... FROM `cars` AS `Car` WHERE 1 = 1 LIMIT 6
Turns out I was using a :sort in my url parameters. Once I took that out all was well :)
If you only have one item, you don't need/shouldn't use an array:
//one thing
var $order = "Model.field DESC";
//multiple things
var $order = array("Model.field" => "asc", "Model.field2" => "DESC");
(per this page)
Note too that virtual fields are ignored by default when paginating.
See "Control which fields used for ordering" in Cake 2.0 Pagination documentation.
Example:
$this->MyModel->virtualFields['count'] = 0;
$this->Paginator->settings = array(
'fields' => 'COUNT(id) AS MyModel__count',
'group' => ('MyModel.group_id'),
'order' => array('MyModel__count' => 'DESC'),
);
// IMPORTANT: pass sortable fields including the virtual field as 3rd parameter:
$log = $this->Paginator->paginate('MyModel', null, array('MyModel__count', 'id'));
I'm trying to join a Users table to my curent hasMany Through table which has Interest and User model id's.
Below is the find query with options:
$params = array(
'fields' => array('*', 'COUNT(DISTINCT(InterestsUser.interest_id)) as interest_count'),
'limit' => 15,
'recursive' => -1,
'offset' => $offset,
'conditions' => array('InterestsUser.interest_id' => $conditions),
'group' => array('InterestsUser.user_id'),
'order' => array('interest_count DESC', 'InterestsUser.user_id ASC', 'InterestsUser.interest_id ASC'),
'joins' => array(
array('table' => 'users',
'alias' => 'User',
'type' => 'LEFT',
'conditions' => array(
'User.id' => 'InterestsUser.user_id',
)
)
)
);
$results = $this->InterestsUser->find('all', $params);
This returns InterestsUser table fine but does not return any values for Users table. It only returns field names.
What could be wrong?
UPDATE:
OK, above is generating below SQL which I got from Cake's datasources sql log:
SELECT *, COUNT(DISTINCT(InterestsUser.interest_id)) as interest_count
FROM `interests_users` AS `InterestsUser`
LEFT JOIN users AS `User` ON (`User`.`id` = 'InterestsUser.user_id')
WHERE `InterestsUser`.`interest_id` IN (3, 2, 1)
GROUP BY `InterestsUser`.`user_id`
ORDER BY `interest_count` DESC, `InterestsUser`.`user_id` ASC, `InterestsUser`.`interest_id` ASC
LIMIT 15
Why is users table values returning NULL only for all fields?
UPDATE:
OK I tried below but this is working fine...What am I missing here!!??
SELECT * , COUNT( DISTINCT (
interests_users.interest_id
) ) AS interest_count
FROM interests_users
LEFT JOIN users ON ( users.id = interests_users.user_id )
WHERE interests_users.interest_id
IN ( 1, 2, 3 )
GROUP BY interests_users.user_id
ORDER BY interest_count DESC
LIMIT 15
The array syntax for join conditions should be like the following
array('User.id = InterestsUser.user_id')
as opposed to array('User.id' => 'InterestsUser.user_id'). For more, see http://book.cakephp.org/view/1047/Joining-tables.