CakePHP and HABTM Model Limit Error - php

I've got a series of Post models that hasAndBelongsToMany Media models. In certain function calls inside of the Post model, I don't need to retrieve the entire list of Media models. However, when I use the following code:
$this->unbindModel( array('hasAndBelongsToMany' => array('Media')) );
// Rebind to get only the fields we need:
$this->bindModel(
array('hasAndBelongsToMany' => array(
'Media' => array(
'className' => 'Media',
'joinTable' => 'media_posts',
'foreignKey' => 'post_id',
'associationForeignKey' => 'media_id',
'limit' => 1,
'fields' => array('Media.type', 'Media.path', 'Media.title')
)
)
)
);
$this->find('all', $params);
This limit only works on one of the first retrieved Post model and all following Post models have no associated Media:
Array
(
[0] => Array
(
[Profile] => Array
(
)
[Media] => Array
(
[0] => Array
(
[type] => photo
[path] => ''
[title] => ''
)
)
)
[1] => Array
(
[Profile] => Array
(
)
[Media] => Array
(
)
)
)
Any suggestions would be great. Thanks!

why not use the containable behaviour
// you would probably want the next line in the app_model ot be able to use it with all models
$this->Post->actsAs = array('Containable')
$params['conditions'] = array(
);
$params['contain'] = array(
'Media' => array(
'fields' => array(
'type', 'path', 'title'
),
'limit' => 1
)
);
$this->Post->find('all', $params);
EDIT:
Just tried that and got this sql (Module <-> Tag):
SELECT `Module`.`id` FROM `modules` AS `Module` WHERE 1 = 1
and
SELECT `Tag`.`id`, `ModulesTag`.`module_id`, `ModulesTag`.`tag_id`
FROM `tags` AS `Tag`
JOIN `modules_tags` AS `ModulesTag`
ON (`ModulesTag`.`module_id` IN (1, 2, 3, 4) AND `ModulesTag`.`tag_id` = `Tag`.`id`)
WHERE `Tag`.`belongs_to` = 'Module'
ORDER BY `Tag`.`name` ASC
LIMIT 1
obviously that cannot return the wanted result, as you would have to do a query for each Module result (which then again would result in way too many queries).
As a conclusion I would return all Tags (in my example) as the overhead in too many result rows is better than the overhead of too many queries..

Cake fetches all the Habtm-related records in one batch query and then assembles them into the results array afterwards. Any additional conditions you specify in the association will be used as is in the query, so it'll look something like this:
SELECT … FROM Media WHERE Media.id in (1, 2, 3, …) LIMIT 1
So it'll only retrieve a single HABTM model.
There's no apparently easy solution for this. Maybe you could think about the original premise again and why the "first" (LIMIT 1) record is supposedly the right one, maybe you can find a different condition to query on.
Failing that, you could rebind your models so Media has a hasMany relationship to medias_posts, the pivot table. For hasMany and belongsTo queries, Cake automatically does JOIN queries. You could use a GROUP BY clause then, which would give you the desired result:
SELECT … FROM Media JOIN medias_posts … GROUP BY medias_posts.post_id
You might also want to experiment with passing the 'join' parameter with the query, to achieve that effect without extensive rebinding.
$this->Media->find('all', array('join' => array(…), …));

Try this:
$this->yourModel->hasAndBelongsToMany['Media'] = false; // or null
And then set your HABTM association manually
$this->yourModel->hasAndBelongsToMany['Media'] = array(........);
Or simply modify the association without nulling it:
$this->yourModel->HABTM['Media']['fields'] = array(....)

CakePHP has a very powerful tool for this containable behaviour

Related

CakePHP query returns two rows instead of one

I'm trying to translate an SQL query into a CakePHP query. Where the SQL query returns one row, the CakePHP query returns 2 rows with the columns id & cover in the first row & files_with_cover in the second.
Naturally I'd like one row with all columns.
SQL query:
SELECT s1.id, s1.cover, COUNT(s2.id) files_with_cover FROM songs s1 LEFT JOIN songs s2 ON s2.cover=s1.cover WHERE s1.source_path=$path
CakePHP query:
$result = $this->Song->find('first', array(
'joins' => array(
array(
'table' => 'songs',
'alias' => 'Song2',
'type' => 'LEFT',
'conditions' => array('Song2.cover = Song.cover')
)
),
'fields' => array('Song.id', 'Song.cover', 'count(Song2.id) as files_with_cover'),
'conditions' => array('Song.source_path' => $file)
));
EDIT, example data set:
id cover source_path
-----------------------
1 image1 /example/1
2 image1 /example/2
3 image1 /example/3
4 image2 /example/4
When doing a select with the conditional source_path "/example/1" I want the following result:
id = 1, cover = image1, files_with_cover = 3.
The printout from the $result array may give a clue. Not sure why the files_with_cover column ends up in a separate row.
Array
(
[Song] => Array
(
[id] => 1
[cover] => cover1.jpeg
)
[0] => Array
(
[files_with_cover] => 3
)
)
Using CakePHP 2.8.1 & MySQL
What you receive there isn't two (database) rows, just a not entirely grouped single row. You need to use virtual fields if you want everything grouped under the main model alias.
$this->Song->virtualFields['files_with_cover'] = 'count(Song2.id)';
'fields' => array('Song.id', 'Song.cover', 'files_with_cover'),
See also Cookbook > Models > Virtual Fields

Get all the belongsTo relations plus some HABTM at CakePHP

I'm trying to get only one HABTM relationship when using paginate using CakePHP 2.3.
Right now I'm getting this result:
Array
(
[0] => Array
(
[Ticket] => Array ( .....)
[User] => Array ( .....)
[Priority] => Array ( .....)
[Status] => Array(....)
[Attachment] => Array(....)
....
)
[1] => ....
[2] => ....
)
By using:
$this->Ticket->recursive = 0;
$this->paginate = array(
'limit' => 20
);
I am using $this->Ticket->recursive = 0; because I'm only interested in the belongsTo relationships with other tables, but at the same time, I would like to get only one of the HABTM relationships.
I know I could achieve it adding each of the single relationships in the contain array, like so:
$this->Ticket->recursive = 0;
$this->paginate = array(
'limit' => 20,
'contain' => array(
'Ticket`, `User`, `Priority`, `Status`, `Attachment`, `Tags`...
)
);
But I have plenty of tables and I was wondering if there's any way to do it without having to name every single table in the contain array.
I've also tried to use the recursive array inside the contains one like so:
//getting the filtered tickets
$this->paginate = array(
'limit' => 20,
'contain' => array(
'Ticket' => array('recursive' => 1),
'Tag' //this is the hasMany relationship
)
);
But it didn't work. Is there any way to achieve this without having to name all the tables to make it more simple to maintain?
Ok, as it seems there's no solution for it, I managed to do it by getting the associated models of the type belongsTo and then adding the model name of the HABTM relationship:
$associations = $this->Ticket->getAssociated('belongsTo');
$associations[] = 'Tag';
//getting the filtered tickets
$this->paginate = array(
'limit' => 20,
'contain' => $associations
);

CakePHP join with comma separated ids

Here I want to join two table with comma separated ids
For example my data is like:
[Restaurant] => Array
(
[RST_ID] => 171
[RST_NAME] => oneone
[RST_IMAGE] =>
[RST_CAT_ID] => 2,4,6
[RST_CT_ID] => 27
[RST_IS_TOP] => 3
[RST_QR_CODE] =>
[RST_CREATED_DATE] => 1394536725
[RST_MODIFIED_DATE] => 1394536725
[RST_STATUS] => 1
)
[Category] => Array
(
[CAT_ID] => 2
[CAT_NAME] => Vegetarian
[CAT_CREATED_DATE] => 1375175962
[CAT_MODIFIED_DATE] => 1375175962
[CAT_STATUS] => 1
)
My Model Code:
var $belongsTo = array(
'Category' => array(
'className' => 'Category',
'foreignKey' => 'RST_CAT_ID',
'conditions' => array('Category.CAT_ID IN ( Restaurant.RST_CAT_ID)')
)
);
Real Query:
SELECT
`Restaurant`.`RST_ID`, `Restaurant`.`RST_NAME`, `Restaurant`.`RST_IMAGE`,
`Restaurant`.`RST_CAT_ID`, `Restaurant`.`RST_CT_ID`, `Restaurant`.`RST_IS_TOP`,
`Restaurant`.`RST_QR_CODE`, `Restaurant`.`RST_CREATED_DATE`,
`Restaurant`.`RST_MODIFIED_DATE`, `Restaurant`.`RST_STATUS`,
`Category`.`CAT_ID`, `Category`.`CAT_NAME`, `Category`.`CAT_CREATED_DATE`,
`Category`.`CAT_MODIFIED_DATE`, `Category`.`CAT_STATUS`, `City`.`CT_ID`,
`City`.`CT_NAME`, `City`.`CT_CREATED_DATE`, `City`.`CT_MODIFIED_DATE`,
`City`.`CT_STATUS`
FROM `dailybit_dailybites`.`restaurant` AS `Restaurant`
LEFT JOIN `dailybit_dailybites`.`category` AS `Category`
ON (`Restaurant`.`RST_CAT_ID` = `Category`.`CAT_ID`
AND `Category`.`CAT_ID` IN ( `Restaurant`.`RST_CAT_ID`))
LEFT JOIN `dailybit_dailybites`.`city` AS `City`
ON (`Restaurant`.`RST_CT_ID` = `City`.`CT_ID`)
WHERE 1 = 1
So what’s the solution here?
It's giving me just one category data that for first id only.
First have a look at this question: MySQL search in comma list
As you can see the belongsTo query is just generating a join on the single id, CakePHP by default doesn't respect this special case. You will have to alter your query and pass all the ids manually, but your DB design is bad and it doesn't follow the CakePHP conventions at all.
How do you prevent duplicates (which would waste space)
How do you remove a given value (Requires custom function, leading to possibility of errors?
How do you respond to performance issues as the size of my tables increase?
Instead of changing the query you should change this awkward DB design. You want to use HABTM here and a join table: Restaurant hasAndBelongsToMany Categoryy.
restaurants <-> restaurants_categories <-> categories
If you insist on using this bad DB design you'll have to use bindModel() and set the conditions manually:
'conditions' => array('FIND_IN_SET (Category.CAT_ID, ' . $listOfIds. ')')
I haven't tested this, try it yourself, see FIND_IN_SET() vs IN()
You'll have to have another method that gets you all the ids you want here before. Like I said, this is ineffectice and bad design.
You have to set your foreign Key false and find_in_set condition
var $belongsTo = array(
'Category' => array(
'className' => 'Category',
'foreignKey' => false,
'conditions' => array('FIND_IN_SET(Category.CAT_ID,Restaurant.RST_CAT_ID)')
)
);
// you can pass an array at the place of 'Restaurant.RST_CAT_ID'

Zend subquery arguments

The query below represents what I am trying to do, I need to pull in a list of blog_posts and also join with a users table.
What it is also doing is pulling in a random 'picture_filename' from blog_updates_pictures. It needs blog_updates as a join to reference the blog_update_id.
What I'd like to do now is also COUNT the number of blog_updates for each blog_post. I think this is a subquery but every implementation fails. It would also be good to have the count accept arguments (ie. blog_updates where date = ?). Also, there may be no updates or pictures to a blog_post.
$select = $db->select ();
$select->from ( array ('b' => 'blog_posts' ), array('headline', 'date_created'));
$select->join ( array ('u' => 'users' ), 'u.user_id = b.user_id', array ( 'email' ) );
$select->joinLeft ( array ('bu' => 'blog_updates' ), 'bu.blog_id = b.blog_id', array () );
$select->joinLeft ( array ('bup' => 'blog_updates_pictures' ), 'bu.blog_update_id = bup.blog_update_id', array ('picture_filename' ) );
Can someone show me the way?
Thanks
What I'd like to do now is also COUNT the number of blog_updates for each blog_post.
You can achieve that using aggregation - use GROUP BY bu.blog_id, and as additional column COUNT(bu.blog_id) AS blog_updates_count. It should work.
Create subselects as:
$subselect = $db->select()
->from(
array ('bu' => 'blog_updates' ),
array(
'blog_id',
'updates' => 'count(*)'
)
)
->group("bu.blog_id");
And then join the $subselect with your main $select as:
$select->join(
array( 't' => $subselect),
"t.blog_id = b.blog_id",
array( 'updates' )
);
If we had the table structure you might get a more complete answer

CakePHP conditional query with controller that 'hasAndBelongsToMany'

I apologize for the horrible title, I couldn't think of how to explain my problem.
In my database I have the following tables, articles, tags, and articles_tags. An article can have many tags.
Currently I am able to grab all the articles, with all the tags, but I want to be able to find articles based upon it's tags.
My select is simple:
$articles = $this->Article->find('all', array(
// extra condition to check for tag, maybe?
'conditions' => array('Article.status' => 'active'),
'limit' => $this->articles_per_page,
'offset' => ($page_num-1)*$this->articles_per_page
));
My return from the database is as follows:
Array
(
[0] => Array
(
[Article] => Array
(
[id] => 1
)
[Tag] => Array
(
[0] => Array
(
[id] => 1
[name] => Ruby
[slug] => ruby
[uses] => 1
[ArticlesTag] => Array
(
[id] => 1
[article_id] => 1
[tag_id] => 1
)
)
)
)
What do I do if I only want to return the articles with a Ruby tag?
Try this
// In your Article model
function getArticleByTagSql($tag)
{
$dbo = $this->getDataSource();
$subQuery = $dbo->buildStatement(
array(
'fields' => array('DISTINCT(ArticlesTag.article_id)'),
'table' => "articles_tags",
'joins' => array(
array('table' => 'tags',
'alias' => 'Tag',
'type' => 'INNER',
'conditions' => array('ArticlesTag.tag_id = Tag.id')
)
),
'alias'=>"ArticlesTag",
'conditions' => array("Tag.name"=>Sanitize::clean($tag_words)),
'order' => null,
'group' => "ArticlesTag.article_id"
),
$this
);
$subQuery = ' Article.id IN (' . $subQuery . ')';
return $dbo->expression($subQuery);
}
// In your Articles Controller
$this->paginate['conditions'][] = $this->Article->getArticleByTagSql($tag_name);
$this->paginate['conditions'][] = array('Article.status' => 'active');
$this->paginate['limit'] = $this->articles_per_page;
// or as per your example
$articles = $this->Article->find('all', array(
// extra condition to check for tag, maybe?
'conditions' => array('Article.status' => 'active',$this->Article->getArticleByTagSql($tag_name)),
'limit' => $this->articles_per_page,
'offset' => ($page_num-1)*$this->articles_per_page
));
For conditions like this you can use the LinkableBehavior. It's designed for exact your wanted result. In case you don't want to use it, you have to do a query on the tag controller:
$this->Tag->find('all', array('conditions' => array('Tag.name' => 'ruby')));
Not possible directly.
The easiest way is to make the query through the tag controller
I'm updating this question with an answer for CakePHP 3.x.
You can find all articles for a given tag using an INNER JOIN. The benefit of this join is that it keeps articles as the primary table in the query. Making this an easy solution when you're paginating results by a HABTM join.
$tag_id = 3; // the ID of the tag
$query = $this->Articles->find('all')
->innerJoin('articles_tags', [
'Articles.id = articles_tags.article_id',
'articles_tags.tag_id' => $tag_id
]);
The first join condition has to be the array key. If you use a => array assignment then CakePHP will pass the articles_tags.article_id as a string argument to the join condition. Which won't work.
You can also stack innerJoin for multiple HABTM conditions. For example; find all articles for a tag and also for a category. You can also use the in query expression to match multiple tags.
You can also use the above in pagination like this $articles = $this->paginate($query);
I'm sure this information is out on the web, but this question was the top of google's results. So maybe this will help others.

Categories