construct a complex SQL query (or queries) - php

As part of a larger web-app (using CakePHP), I'm putting together a simple blog system. The relationships are exceedingly simple: each User has a Blog, which has many Entries, which have many Comments.
An element I'd like to incorporate is a list of "Popular Entries." Popular Entries have been defined as those with the most Comments in the last month, and ultimately they need to be ordered by the number of recent Comments.
Ideally, I'd like the solution to stay within Cake's Model data-retrieval apparatus (Model->find(), etc.), but I'm not sanguine about this.
Anyone have a clever/elegant solution? I'm steeling myself for some wild SQL hacking to make this work...

Heh, I was just about to come back with essentially the same answer (using Cake's Model::find):
$this->loadModel('Comment');
$this->Comment->find( 'all', array(
'fields' => array('COUNT(Comment.id) AS popularCount'),
'conditions' => array(
'Comment.created >' => strtotime('-1 month')
),
'group' => 'Comment.blog_post_id',
'order' => 'popularCount DESC',
'contain' => array(
'Entry' => array(
'fields' => array( 'Entry.title' )
)
)
));
It's not perfect, but it works and can be improved on.
I made an additional improvement, using the Containable behaviour to extract the Entry data instead of the Comment data.

Shouldn't be too bad, you just need a group by (this is off the type of my head, so forgive syntax errors):
SELECT entry-id, count(id) AS c
FROM comment
WHERE comment.createdate >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)
GROUP BY entry-id
ORDER BY c DESC

If you weren't fussed about the time sensitive nature of the comments, you could make use of CakePHP's counterCache functionality by adding a "comment_count" field to the entries table, configuring the counterCache key of the Comment belongsTo Entry association with this field, then call find() on the Entry model.

You probably want a WHERE clause to get just last 30 days comments:
SELECT entry-id, count(id) AS c
FROM comment
WHERE comment_date + 30 >= sysdate
GROUP BY entry-id
ORDER BY c DESC

Related

Union select with CakePHP

This is a two part question. I found something similar, but it was more complicated with more tables and used joins making it much more complex and difficult to translate to my more simplified situation. Also, it doesn't cover the second part of my question.
This takes place in my OrdersController. I have also have ArchivedordersControler and ArchivedOrder model
I'm trying to search for an email address in two different tables(orders and archived_orders). I don't need to join anything (at least I don't think I have to). Both tables have the exact same structure, one is just for archived values.
With MySQL I'd just do something like
select * from orders where orders.email = '$email'
Union
select * from archived_orders where archived_orders.email = '$email'
How can I add some sort of identifier to know which table it was selected from? The email can appear in both tables but the options displayed based on which table it was pulled from will be different.
You'll want to create a Model for both tables, Order and ArchivedOrder. That way you can easily find the data you are looking for in both tables by:
// From OrdersController
$this->Order->find('first', array('conditions' => array('email' => $email)));
// From ArchivedOrdersController
$this->ArchivedOrder->find('first', array('conditions' => array('email' => $email)));
If you want to fetch the archived data from the original OrdersController, you can load the model from there as well, like:
$this->loadModel('ArchivedOrder');
$this->ArchivedOrder->find('first', array('conditions' => array('email' => $email)));
That way, you don't need a separate controller for it. It will return the data as an array that looks like:
array(
'Order' => array(
'id' => 12,
'email' => 'customer#example.com'
// And other data...
)
)
So from the Order you can tell it was selected from the original table. Otherwise, it will be ArchivedOrder.

CakePhp Contain Model Cache

I'm using cakephp. In some query I use these kind of find:
$this->Photo->Behaviors->attach('Containable', array('autoFields' => true));
This is the Contain array that i use in the find:
'contain'=>array(
'User'=>array('fields'=>array('User.Name','User.Username')),
'Like' => array('User'=>array('fields'=>'Name'),
'order'=>'Timestamp DESC'
)),
'recursive' => 2,
The problem is that every time i want the Name of User that liked a photo. Cakephp does this query.
For example: SELECT `User`.`Name` FROM `Users` AS `User` WHERE `User`.`id` = 2175
If i have 300 likes on one photo i will make another 300queries for the User.Name. So, I would like to cache this kind of request. I've installed memcache correctly in my server, it's working normally. But I can't find a way to cache the query that cake make with the Containable Behaviors.
Has some one had this problem?
Thanks
G.
Why not use cakephp's counterCache feature instead?
There is a lot of documentation in the cakephp book about caching queries not just in me cache but many other ways as well such. Check out the CakeCache class.
there are a number of approaches on this topic.
if it is only about belongsTo relations it boils down to manually join tables using bindModel().
here is a good behavior: http://planetcakephp.org/aggregator/items/891-linkable-behavior-taking-it-easy-in-your-db
it takes care of that itself.

Keep selected fields order with HYDRATE_ARRAY in Doctrine 1.2

My question is quite simple but I can't manage to find an answer.
When I execute a query like:
$query->select('t2.name as t2_name, t1.name as t1_name')
->from('table1 t1')
->leftJoin('t1.table2 t2')
->execute(array(), Doctrine_Core::HYDRATE_ARRAY);
Doctrine returns me an array like:
array(
[0] => array(
't1_name' => 'foo',
't2_name' => 'bar'
)
)
Where i expected to get field t2_name to be set in the array before t1_name.
Is there anyway to keep the order of these selected fields in Doctrine ?
Doctrine will automatically include the primary (root) table's key field and automatically make it the first column in any query, in almost all hydration types.
Since table1 is the root table in your query, it moves that to the beginning for its own internal processing benefits.
I find this behavior annoying and somewhat unpredictable at times also, but have found great relief by creating custom hydrators.
There's a good example of creating a key/value hydrator which I have used beneficially many times in our code.
You could do something similar to rearrange the fields in the order you want.
Also I have posted an explanation to a very similar question here which may be beneficial.

Why is CakePHP duplicating my queries? (this is not the same "too many queries" problem that is usually asked)

I've defined these relationships in my models:
Lead hasMany Job
Job HABTM Employee
Job HABTM Truck
I'm trying to do a find('all') from my Truck model, and limit the results to:
All Trucks,
all jobs associated with those trucks that have a certain pickup date,
the employees assigned to those jobs,
and the lead associated with the job.
Here is my find operation:
// app/models/truck.php
$this->find('all', array(
'contain' => array(
'Job' => array(
'Employee',
'Lead',
'conditions' => array(
'Job.pickup_date' => $date
)
)
)
));
For some reason, Cake does the query to find Employees TWICE. This leads to having all employees represented two times for each job. Here is the SQL dump:
SELECT `Truck`.`id`, `Truck`.`truck_number`
FROM `trucks` AS `Truck`
WHERE 1 = 1;
SELECT `Job`.`id`, `Job`.`lead_id`, `Job`.`city`,
`JobsTruck`.`id`, `JobsTruck`.`job_id`, `JobsTruck`.`truck_id`
FROM `jobs` AS `Job`
JOIN `jobs_trucks` AS `JobsTruck` ON (`JobsTruck`.`truck_id` IN (2, 3)
AND `JobsTruck`.`job_id` = `Job`.`id`)
WHERE `Job`.`pickup_date` = '2010-10-06'
SELECT `Lead`.`id`, `Lead`.`name`, `Lead`.`created` FROM `leads` AS `Lead`
WHERE `Lead`.`id` = 4
SELECT `Employee`.`id`, `Employee`.`name`, `Employee`.`created`,
`EmployeesJob`.`id`, `EmployeesJob`.`employee_id`,
`EmployeesJob`.`job_id`
FROM `employees` AS `Employee`
JOIN `employees_jobs` AS `EmployeesJob`
ON (
`EmployeesJob`.`job_id` = 1 AND
`EmployeesJob`.`employee_id` = `Employee`.`id`
)
SELECT `Lead`.`id`, `Lead`.`name`, `Lead`.`created` FROM `leads` AS `Lead`
WHERE `Lead`.`id` = 4
SELECT `Employee`.`id`, `Employee`.`name`, `Employee`.`created`,
`EmployeesJob`.`id`, `EmployeesJob`.`employee_id`,
`EmployeesJob`.`job_id`
FROM `employees` AS `Employee`
JOIN `employees_jobs` AS `EmployeesJob`
ON (
`EmployeesJob`.`job_id` = 1 AND
`EmployeesJob`.`employee_id` = `Employee`.`id`
)
Notice that the last two queries are duplicates. Did I do something wrong that I'm missing?
UPDATE
It seems Cake sends a duplicate query for every truck. Now that I have 15 records in the trucks table, the queries to leads and employees are duplicated 15 times each.
I don't know why there are two queries duplicated but maybe this behavior can help:
https://github.com/Terr/linkable
UPDATE
This kind of problem is well known:
Duplicate Queries in
MODEL->HABTM->HABTM->HasMany
relationship
Duplicate Queries Problem with
Containable
Ticket
There is nothing to join the found Job/s to a specific Truck.
(I hope my explanation isn't too hard to understand, but CakePHP can be that way sometimes! imho)
The Jobs are being attributed to the Trucks in an almost arbitrary way (my memory of Cake is that this can happen); the nature of the HABTM call attaches the Job/s to each of the 15 Trucks. This seems to be the current process from my point of view;
Get all trucks.
Get all jobs for those trucks where the date is x.
[Problem 1] Attach the found Jobs to each Truck (that is your 15 trucks), but attached to every Truck.
[Problem 2] Get all Leads/Employees related to that Job, again for each Truck.
Problem 1: The source of the issue. You can see in the second query (SELECT Job...), where it uses the correct truck_id's in the ON statement, but Cake cannot "join" these Jobs back into the right Truck, because it is a different query! So it joins the found jobs to each Truck.
Problem 2: This is the 'real' problem, for Cake does not construct long JOIN statements, so there is no way to find the Employees/Leads only for those Trucks that you want. That is why it finds them for each Truck, this is because you are doing a FindAll on Truck.
I hope that makes sense. You need to do a find all on Jobs, since that is the 'center' of the query (pickup_date).
$this->loadModel('Job');
$whatev = $this->Job->find('all', array(
'contain' => array(
'Job' => array(
'Truck',
'Employee',
'Lead',
'conditions' => array(
'Job.pickup_date' => $date
)
)
)
));
CakePHP queries really only work when you find one/all of a certain model, it is better to start in the middle and work either side if you have a double HABTM relationship. If you wish to 'sort' by Truck, then you might have to write you're own query (model method) to accomplish the task yourself. In raw SQL this query may be easy, in abstracted PHP super-cake-ness this is difficult for CakePHP to allow.
Happy baking, as they say!
You want LEFT JOIN instead of JOIN.
Don't ask me how to do it in cakephp, i'm happy my code currently works. ^^

Querying data based on 3rd level relationship in CakePHP

I have the following relationships set up:
A HABTM B
B belongsTo C
C hasMany B
Now, for a given A, I need all C with the B's attached. I can write the SQL queries, but what's the proper CakePHP way? What method do I call on which model, and with which parameters?
I'd go with Aziz' answer and simply process the data as it comes in. If you need C to be your primary model though, you'll have to do a little workaround. Cake is not terrifically good with conditions on related models yet, especially on removed 3rd cousins kind of queries. It usually only does actual JOIN queries on belongsTo or hasMany relations; not on HABTM relations though, those it gets in separate queries. That means you can't include conditions on related HABTM models.
Your best bet then might be something like this:
// get related records as usual with the condition on A, limit to as little data as necessary
$ids = $this->A->find('first', array(
'conditions' => array('A.id' => 'something'),
'recursive' => 2,
'fields' => array('A.id'),
'contain' => array('B.id', 'B.c_id', 'B.C.id') // not quite sure if B.C.id works, maybe go with B.C instead
));
// find Cs, using the ids we got before as the condition
$Cs = $this->C->find('all', array(
'conditions' => array('C.id' => Set::extract('/B/C/id', $ids)),
'recursive => 1
);
Note that this produces quite a bunch of queries, so it's not really an optimal solution. Writing your own SQL might actually be the cleanest way.
EDIT:
Alternatively, you could re-bind your associations on the fly to make them hasMany/belongsTo relationships, most likely using the join table/model of A and B. That might enable you to use conditions on related models more easily, but it's still tricky to fetch Cs when the condition is on A.
$this->A->find(
'first',
array('conditions'=>array('id'=>'someword'),
'recursive'=>2)
);
like this?
this might work
$this->C->find('all',
array('conditions'=>array('id'=>'someword','B.aid'=>$a.id)));
I'd think of "Containable" behaviour (http://book.cakephp.org/view/474/Containable)... gives a lot control on finding related data.

Categories