CakePHP/mysql: select found_rows can't run in a loop? - php

I'm using CakePHP and trying to retrieve FOUND_ROWS() for a query that runs several times in a loop (each time with a different WHERE section). The strangest thing is happening: it returns the correct result for the first query - and then the same exact result for subsequent queries.
This is the code:
$article = new Article();
$query = $db->buildStatement(
array(
'table' => $db->fullTableName($article)
, 'alias' => 'Article'
, 'order' => '`Article`.`publish_date` desc'
, 'offset' => $startIndex
, 'joins' => array(
array(
'table' => 'articles_categories',
'alias' => 'ArticlesCategories',
'type' => 'inner',
'conditions' => array(
'ArticlesCategories.article_id = Article.id',
),
)
)
, 'limit' => $this->maxNumArticlesToLoadPerPage
, 'conditions' => "ArticlesCategories.category_id = $categoryId and publish_date <= now()"
, 'fields' => array('Article.id', 'Article.name', 'Article.search_engine_teaser', 'Article.image_file_name', 'Article.content_modified', 'ArticlesCategories.category_id')),
$article
);
$query = str_replace('SELECT', 'SELECT SQL_CALC_FOUND_ROWS', $query);
$articles = $article->query($query);
// also return how many total articles are in this category
$numArticlesInCategory = $article->query('select found_rows()')[0][0]['found_rows()'];
Now, where real curiosity here is that I used ET GLOBAL general_log = 'ON' in the mysql database to track down what's going on, and it appears that the first query is indeed run several times in a loop, but the query select found_rows() is only run once!
I also tried to run select SQL_NO_CACHE found_rows(), but that didn't help.
Has anyone encountered this?

Check your models $cacheQueries property setting, it might be true, causing the query to be cached on the CakePHP side. Use the second query() parameter to disable caching per query:
query('sql', false)
Btw., ArticlesCategories.category_id = $categoryId looks like a possible SQL injection vulnerability, you should never insert data in queries that way, always use the key => value syntax for conditions, or use prepared statements!
Also model classes shouldn't be instantiated directly, instead ClassRegistry::init() should be used.
See also
Cookbook > Models > Retrieving Your Data > Model::query()
Cookbook > Models > Model Attributes > cacheQueries

Related

CakePHP ignores condition

I have the following problem with CakePHP:
Two tables are joined (filters and accounts). Then I am building conditions and only the second condition Account.active =>1 gets executed. If I print the result, there are still showing filters that are having another mode_id than 3.
$joins= array(
array('table' => 'filters',
'alias' => 'Filter',
'type' => 'right',
'conditions' => array(
'Filter.account_id = Account.id',
)
),
);
Then I execute the request including joins and conditions
$activeAccounts = $this->Account->find('all',array(
'conditions'=>array('AND'=>array('Filter.mode_id'=>3,'Account.active'=>1)),
'joins'=>$joins));
The models were checked and no problems identified. Filter belongs to Account. Account has many Filter.
Below the query that is generated. The results are still showing filters with Filter.mode_id other than 3
Here is the query that is generated. The results are still containing rows with Filter.mode_id other than 3 despite the fact that one condition is 'Filter.mode_id'=>3
SELECT `Account`.`id`, `Account`.`user_id`, `Account`.`name`,
`Account`.`api_key`, `Account`.`account_number`, `Account`.`remaining_balance`,
`Account`.`investment_size`, `Account`.`active`
FROM `baseline_db`.`accounts` AS `Account`
right JOIN `baseline_db`.`filters` AS `Filter`
ON (`Filter`.`account_id` = `Account`.`id`)
WHERE ((`Filter`.`mode_id` = 3) AND
(`Account`.`active` = '1'))
Like say Oldskool, use the Model associations
and for your condition, The "AND" is not necessary,
you cant put :
$activeAccounts = $this->Account->find('all',array(
'conditions' => array(
'Filter.mode_id'=>3,
'Account.active'=>1
)
));
the request you want to make with the type of relation you have, seem to me weird.
If i understand, perhaps with something like that :
$this->loadModel('Filter');
$filters =$this->Filter->find("list", array(
'conditions' => array('Filter.mode_id' => 3),
'fields' => array('Filter.account_id')
));
$activeAccounts = $this->Account->find('all',array(
'conditions' => array(
'Account.account_id'=>$filters,
'Account.active'=>1
)
));

CakePHP find with LEFT JOIN and generate_series

Basically, is it possible, and if so, how do you do the following code without using the raw query() method in CakePHP 2.0.6. I am using PostgreSQL (which generate_series() is a function of). So, how do you do this query the CakePHP way?
$sql = "
SELECT new_rank FROM generate_series(1,999) AS new_rank
LEFT JOIN tasks ON tasks.rank = new_rank
WHERE tasks.rank IS NULL LIMIT 1
";
$data = $this->Task->query($sql);
EDIT
One user said I could try to use the find call on Task and right join to generate_series(). Here is my attempt at that. This code throws an error, in that CakePHP is putting double quotes around the function arguments for generate_series. I wonder how I can get it to not do that?
$data = $this->Task->find('all', array(
'conditions' => array('Task.rank' => null),
'limit' => 1,
'recursive' => -1,
'fields' => 'new_rank',
'joins' => array(
array(
'table'=>'generate_series(1,999)',
'alias'=>'new_rank',
'type'=>'RIGHT',
'conditions' => array(
'tasks.rank'=>'new_rank'
)
)
)
));
Which products the following SQL:
SELECT "Task"."new_rank" AS "Task__new_rank"
FROM "tasks" AS "Task"
RIGHT JOIN generate_series("1,999") AS "new_rank" ON ("tasks"."rank" = 'new_rank')
WHERE "Task"."rank" IS NULL LIMIT 1
What you're probably looking for is in DboSource::expression(). It basically allows you to do a SQL expression without any of Cake's escaping. Make sure to sanitize inputs.
To have it show up as a field, you can try adding it to the fields key in your options array:
$ds = $this->Task->getDataSource();
$this->Task->find('all', array(
'fields' => $ds->expression('generate_series(1,999) AS new_rank')
));
If that doesn't work, you can always try DboSource::rawQuery() which doesn't escape anything, afaik.

Forcing a belongs to relation in cakephp paginate

I am trying to force a join on paginate function of cakephp.
A user has messages which means it will be message belongs to user.
I have to show them in a list so i need to use paginate here.
Problem is it doesnt shows me the record of the model which i intend to bind
My code is:
$userId = $this->Session->read('SESSION_ADMIN.id');
$this->helpers['Paginator'] = array('ajax' => 'Ajax');
$this->Message->bindModel(
array(
'belongsTo'=>array(
'Npo'=>array(
'className' => 'Npo',
'foreignKey' => 'reciever_id',
'fields' => 'Npo.username'
)
)
)
);
$this->paginate = array('conditions'=>array('Message.sender_id'=>$userId,'Message.sender'=>'Admin'),
'order' => array('Message.modified DESC'),
'limit' =>'1'
);
$sentMsg = $this->paginate('Message');
//$sentMsg = $this->Message->find('all');
pr($sentMsg);die();
when i uncomment the FIND statement it shows me record but in case of paginate it doesnt.
Also it doesnt shows me the join in the paginate query but it does in counting record.
Any one have an idea.I dont want to use paginate Join here.Is there a way to enforce a belongs to here?
Regards
Himanshu Sharma
Have you tried:
$this->Message->bindModel(
array(
'belongsTo'=>array(
'Npo'=>array(
'className' => 'Npo',
'foreignKey' => 'reciever_id',
'fields' => 'Npo.username'
)
)
), false // Note the false here!
);
The paginator actually executes two queries: one to count the total number of records, and one to actually fetch the desired records. By default, associations created on the fly using bindModel() are reset after each query. It depends on the Cake version which query comes first, but I believe that in your case it is the count query; leaving the actual results query without the association. Setting false on on the second argument of bindModel() prevents the association from being reset after the first query.

CakePHP - problem with HABTM paginate query

Tables
restaurants
cuisines
cuisines_restaurants
Both restaurant and cuisine model are set up to HABTM each other.
I'm trying to get a paginated list of restaurants where Cuisine.name = 'italian' (example), but keep getting this error:
1054: Unknown column 'Cuisine.name' in 'where clause'
Actual query it's building:
SELECT `Restaurant`.`id`, `Restaurant`.`type` .....
`Restaurant`.`modified`, `Restaurant`.`user_id`, `User`.`display_name`,
`User`.`username`, `User`.`id`, `City`.`id`,`City`.`lat` .....
FROM `restaurants` AS `Restaurant` LEFT JOIN `users` AS `User` ON
(`Restaurant`.`user_id` = `User`.`id`) LEFT JOIN `cities` AS `City` ON
(`Restaurant`.`city_id` = `City`.`id`) WHERE `Cuisine`.`name` = 'italian'
LIMIT 10
The "....." parts are just additional fields I removed to shorten the query to show you.
I'm no CakePHP pro, so hopefully there's some glaring error. I'm calling the paginate like this:
$this->paginate = array(
'conditions' => $opts,
'limit' => 10,
);
$data = $this->paginate('Restaurant');
$this->set('data', $data);
$opts is an array of options, one of which is 'Cuisine.name' => 'italian'
I also tried setting $this->Restaurant->recursive = 2; but that didn't seem to do anything (and I assume I shouldn't have to do that?)
Any help or direction is greatly appreciated.
EDIT
models/cuisine.php
var $hasAndBelongsToMany = array('Restaurant');
models/restaurant.php
var $hasAndBelongsToMany = array(
'Cuisine' => array(
'order' => 'Cuisine.name ASC'
),
'Feature' => array(
'order' => 'Feature.name ASC'
),
'Event' => array(
'order' => 'Event.start_date ASC'
)
);
As explained in this blogpost by me you have to put the condition of the related model in the contain option of your pagination array.
So something like this should work
# in your restaurant_controller.php
var $paginate = array(
'contain' => array(
'Cuisine' => array(
'conditions' => array('Cuisine.name' => 'italian')
)
),
'limit' => 10
);
# then, in your method (ie. index.php)
$this->set('restaurants', $this->paginate('Restaurant'));
This fails because Cake is actually using 2 different queries to generate your result set. As you've noticed, the first query doesn't even contain a reference to Cuisine.
As #vindia explained here, using the Containable behavior will usually fix this problem, but it doesn't work with Paginate.
Basically, you need a way to force Cake to look at Cuisine during the first query. This is not the way the framework usually does things, so it does, unfortunately, require constructing the join manually
. paginate takes the same options as Model->find('all'). Here, we need to use the joins option.
var $joins = array(
array(
'table' => '(SELECT cuisines.id, cuisines.name, cuisines_restaurants.restaurant_id
FROM cuisines_restaurants
JOIN cuisines ON cuisines_restaurants.cuisines_id = cuisines.id)',
'alias' => 'Cuisine',
'conditions' => array(
'Cuisine.restaurant_id = Restaurant.id',
'Cuisine.name = "italian"'
)
)
);
$this->paginate = array(
'conditions' => $opts,
'limit' => 10,
'joins' => $joins
);
This solution is a lot clunkier than the others, but has the advantage of working.
a few ideas on the top of my mind:
have you checked the model to see if the HABTM is well declared?
try using the containable behavior
in none of those work.. then you could always construct the joins for the paginator manually
good luck!
Cuisine must be a table (or alias) on the FROM clausule of your SELECT.
so the error:
1054: Unknown column 'Cuisine.name' in 'where clause'
Is just because it isn't referenced on the FROM clausule
If you remove the Feature and Event part of your HABTM link in the Restaurant model, does it work then?
Sounds to me like you've failed to define the right primary and foreing keys for the Cuisine model, as the HABTM model is not even including the Cuisine tabel in the query you posted here.

CakePHP order query results on more than 1 level

I'm using the Containable behavior to get a list of Comments (belongsTo Post, which belongs to Question; Question hasMany Post, and Post hasMany Comments; all of these belong to Users).
$data = $this->Question->find ( 'first',
array ('contain' =>
array ('User',
'Post' => array ('User', /* 'order' => 'User.created DESC'*/ )
)
)
);
It works, when I comment out the section in comments above. I suppose this is to be expected, but what I want is all of the Posts that are found, should be sorted in order of the 'created' field of the 'User' they belong to. How do I accomplish this deeper level sorting in CakePHP? I always get, "Warning (512): SQL Error: 1054: Unknown column 'User.created' in 'order clause'"
Thanks for your help!
Also, you might be trying to group on a related table from a find call that doesn't use joins.
Set your debug level to something greater than 1 so you can see the query log and make sure that Cake isn't doing two queries to fetch your data. If that is the case then the first query is not actually referencing the second table.
If you want to manually force a join in these situations you can use the Ad-Hoc joins method outlined by Nate at the following link.
http://bakery.cakephp.org/articles/view/quick-tip-doing-ad-hoc-joins-in-model-find
I have found two ways to get around this.
The first is to define the second level associacion directly in the model.
Now you will have access to this data everywhere.
It should look something like this.....
var $belongsTo = array(
'Foo' => array(
'className' => 'Foo', //unique name of 1st level join ( Model Name )
'foreignKey' => 'foo_id', //key to use for join
'conditions' => '',
'fields' => '',
'order' => ''
),
'Bar' => array(
'className' => 'Bar', //name of 2nd level join ( Model Name )
'foreignKey' => false,
'conditions' => array(
'Bar.id = Foo.bar_id' //id of 2nd lvl table = associated column in 1st level join
),
'fields' => '',
'order' => ''
)
);
The problem with this method is that it could make general queries more complex than they need be.
You can thus also add the second level queries directly into te find or paginate statement as follows: (Note: I found that for some reason you can't use the $belongsTo associations in the second level joins and will need to redefine them if they are already defined. eg if 'Foo' is already defined in $belongsTo, you need to create a duplicate 'Foo1' to make the association work, like the example below.)
$options['joins'] = array(
array('table' => 'foos',
'alias' => 'Foo1',
'type' => 'inner',
'conditions' => array(
'CurrentModel.foo_id = Foo1.id'
)
),
array('table' => 'bars',
'alias' => 'Bar',
'type' => 'inner',
'foreignKey' => false,
'conditions' => array(
'Bar.id = Foo1.bar_id'
)
)
);
$options['conditions'] = array('Bar.column' => "value");
$this->paginate = $options;
$[modelname] = $this->paginate();
$this->set(compact('[modelname]'));
I hope this is clear enough to understand and that it helps someone.
Check your recursive value. If it's too limiting, it will ignore the containable links, IIRC. I remember bumping into this a few times. I'd try containing multiple models, but my recursive option was set to 0 and nothing would get pulled. For your example, I'd think that a value of 1 (the default) would suffice, but maybe you've explicitly set it to 0 somewhere?
You can add before your call to find() the following:
$this->Question->order = 'Question.created DESC';
Yeah, I couldn't work out how to sort based on the related/associated model, so ended up using the Set::sort() method. Checkout this article for a good explanation.
// This finds all FAQ articles sorted by:
// Category.sortorder, then Category.id, then Faq.displaying_order
$faqs = $this->Faq->find('all', array('order' => 'displaying_order'));
$faqs = Set::sort($faqs, '{n}.Category.id', 'ASC');
$faqs = Set::sort($faqs, '{n}.Category.sortorder', 'ASC');
...And yes, it should probably be a Category->find() but unfortunately the original developer didn't code it that way, and I didn't wanna rework the views.

Categories