Doing manual joins in CakePHP does not invoke the constructor of the joined model (Virtual Fields needed) - cakephp

I am trying to build a dynamic query through an array of joined tables. These generate dynamic aliases to avoid conflicts (non unique aliases).
However, in my joined tables - I have some virtualFields which are not getting processed. Upon further inspection, it appears that the joined tables __construct() functions are not getting called.
Is there a way to get virtualFields on a joined table in CakePHP v2.2.8?
Thanks

Manual joins do not use your models
Manual joins do not make use of your models. Using manual joins, you are manually defining a join and giving it an alias. Although this alias may be the same as an existing model, CakePHP will not use your model for the joined tables.
If you need virtualFields, depending on 'what' data is used, you may be able to move the virtualField to the 'main' model you're querying, for example:
$this->Foo->virtualFields['foobar'] = 'CONCAT(\'Hello \', Bar.name)';
$foo = $this->Foo->find(
'all',
array(
'fields' => array(
'Foo.name',
'Foo.foobar',
),
'joins' => array(
array(
'table' => 'bars',
'alias' => 'Bar',
'type' => 'INNER',
'conditions' => array(
'Bar.id = Foo.bar_id',
)
)
),
'recursive' => -1
)
);
debug($foo);
Returns;
array(
(int) 0 => array(
'Foo' => array(
'title' => 'Foo One',
'foobar' => 'Hello World'
)
),
(int) 1 => array(
'Foo' => array(
'title' => 'Foo Two',
'foobar' => 'Hello Planet'
)
),
)

Related

HABTM accessing array value in condition

Hey my problem is to define a condition for a HABTM Model
(int) 0 => array(
'Article' => array(
'id' => '',
'title' => '',
'body' => '',
'created' => null,
'modified' => null
),
'Channel' => array(
'id' => '',
'channelname' => '',
'created' => null,
'modified' => null
),
'Tag' => array(
(int) 0 => array(
'id' => '1',
'value' => 'example',
),
(int) 1 => array(
[maximum depth reached]
),
(int) 2 => array(
[maximum depth reached]
)
),
),
I want to define a condition that searches the Tag values for specific values .. like that:
$this -> Article -> find('all', array('conditions' => array('Tag.0.value' => 'test'))
So does someone knows how to "foreach" that array in this condition? thanks (:
You either need to:
Run your find from the other direction (using CakePHPs awesome Containable Behavior to get the Articles):
$this->Tag->find('all', array(
'conditions' => array(
'Tag.value' => 'test'
),
'contain' => array(
'Article'
)
));
Or, use JOINs. Run your find like you're doing, but do an INNER JOIN on tags where the tag value='test'
You cannot limit the main Model's results based on an associated model's conditions. The reason is, when you rely on recursive, or use Contain, CakePHP actually creates separate queries to pull in your different models' data. Therefore, you can't have a condition in one query affect the other query.
See similar question/answers:
How do I restrict query results based on sub-results in CakePHP?
Conditions in JOINed tables shows error CakePHP
cakephp contain filtering by parent model value
cakephp contain association conditions issue
Adding conditions to Containable in CakePHP

Containable with Condition

I am using containable with CakePHP. My tried code is ...
public function detail($slug = "") {
$this->Poet->contain('User.id', 'User.full_name', 'Song.id', 'Song.name', 'Song.name_hindi', 'Song.slug');
$result = $this->Poet->findBySlug($slug);
if (!$result) {
throw new NotFoundException(__('Invalid Poet - ' . $slug));
}
pr($result);
die();
$this->Poet->id = $result['Poet']['id'];
$this->set('result', $result);
}
Like this. Now I have Song.status as my association with Song table. I want to fetch only those records that has status = 1. Is it possible? Can I select only active records with my piece of code.
Use a normal find
While the magic findBy* methods are handy from time to time, it's a good idea to only use them for trivial queries - your query is nolonger trivial. Instead use a normal find call e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
)
),
'conditions' => array(
'slug' => $slug,
'Song.status' => 1 // <-
)
));
Does a Poet hasMany songs?
You don't mention your associations in the question, which is rather fundamental to providing an accurate answer, however it seems likely that a poet has many songs. With that in mind the first example will generate an sql error, as there will be no join between Poet and Song.
Containable does permit filtering associated data e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
'Song.status = 1' // <-
)
),
'conditions' => array(
'slug' => $slug
)
));
This will return the poet (whether they have relevant songs or not), and only the songs with a status of "1". You can achieve exactly the same thing by defining the condition in the association definition (either directly in the model or by using bindModel).

Containable behavior doesn't return deeper model association and selecting fields

I want to limit the fields returned by a deeper association using containable.
My associations:
Game hasMany Review
The paginate and containable code:
$this->paginate = array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Game' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
),
)
);
$games = $this->paginate('Game');
Currently, all of the fields in the Review table are returned. 'ROUND(AVG(Review.score),1)' is never returned. How can I specify what fields I want returned from the Review association?
SQL dumps for two search results using #theJetzah's answer. The first is a search with one game as a result and the second is a search returning three games.
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` = (55)
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` IN (55, 56, 57)
Not a full answer, but an attempt to get it working :)
Approach1 (UPDATE: Containable doesn't support 'group by')
First of all, try to add the 'Game' model to the $uses array of your Controller, if it is not included yet, and re-organise the pagination array (as previously suggested by Sam), so that you'll be pagination the Game model itself.
Then, It may help to create a virtual field for the calculated score, but the results of 'Review' need to be grouped, otherwise you'll not be able to calculate the average score.
I'm not able to test this, but it may worth trying
something like this;
public $uses = array(
'Game',
// other models
);
public function myfunction()
{
$this->Game->Review->virtualFields['average_score'] = 'ROUND(AVG(Review.score),1)';
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array(
'Review.game_id,
'Review.average_score',
),
'group' => array(
'Review.game_id,
),
)
)
)
);
// Conditions can be passed to paginate,
// that way you can specify 'paginate' at
// one place and don't have to modify it
// to include the conditions
$games = $this->paginate('Game', $conditions);
}
Alternative approach: Using joins and a database-view
Apparently, the Containable behavior doesn't like group-by clauses; See this ticket for more information: Containable behavior does not implement 'group' option
CakePHP allows you to manually specify a join: Joining Tables
To simplify things and to prevent having to add a 'group by' for all fields, create a simple database-view in your database;
CREATE VIEW review_scores AS
SELECT
game_id,
ROUND(AVG(score),1) AS average_score,
COUNT(id) AS total_reviews
FROM
reviews
GROUP BY
game_id;
If you're unfamiliar with this; a database 'view' is basically a 'stored query', which can be accessed as if it was a regular table. See Create View
Then, use a 'manual' join, using the newly created database-view as the source-table. In your case, this will look something like this;
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name',
'ReviewScore.average_score',
'ReviewScore.total_reviews',
),
'joins' => array(
array(
'table' => 'review_scores',
'alias' => 'ReviewScore',
'type' => 'LEFT',
'conditions' => array(
'ReviewScores.game_id = Game.id',
)
)
)
)
);
Hope this helps
I think your array is a configured a little wrong, try:
$this->paginate = array(
'Game' => array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
)
)
);
$games = $this->paginate('Game');
As an aside, from personal experience, specifying the fields in a query doesn't always speed it up (certainly for small number of fields), assuming this is the motive for doing so. It does reduce memory occupancy but this is only relative to original size of the record and the number of records returned.

Add tables to custom query in cakephp

Im trying to replicate the following query in cakephp:
SELECT *
FROM uploads, proposals
WHERE proposals.id = uploads.proposal_id AND proposals.tender_id = 10
Im using the find method in the Upload model with the following conditions:
$conditions = array(
'Proposal.id' => $id,
'AND' => array(
'Upload.proposal_id' => 'Proposal.id'
)
);
return($this->find('list', array('conditions' => $conditions)));
but im getting this query instead
SELECT `Upload`.`id`, `Upload`.`title`
FROM `kumalabs_lic`.`uploads` AS `Upload`
WHERE `Proposal`.`id` = 10 AND `Upload`.`proposal_id` = 'Proposal.id'
as you can see, the proposals table is missing, can somebody explain me how can i make this query?
Thanks :)
I would recommend you use the linkable behaviour for this. It is much easier than the default way of doing joins in CakePHP. It works with the latest version of CakePHP, as well as 1.3.
CakePHP Linkable Behavior
You would then modify your find to look like this:
return($this->find('list', array(
'link' => array('Proposal'),
'conditions' => array(
'Proposal.id' => $id,
),
'fields' => array(
'Upload.*',
'Proposal.*',
),
)));
CakePHP will automatically join on your primary / foreign key, so no need to have the
'Upload.proposal_id' => 'Proposal.id'
condition.
Though you don't need that condition, I also want to point out that you are doing your AND wrong. This is how you do AND and OR in CakePHP
'conditions' => array(
'and' => array(
'field1' => 'value1', // Both of these conditions must be true
'field2' => 'value2'
),
'or' => array(
'field1' => 'value1', // One of these conditions must be true
'field2' => 'value2'
),
),
If the model have association, CakePHP automatically join the table by 'contain' keyword. Try code bellow:
public function getProposalsFromTender($id){
$data = $this->find('all', array(
'conditions' => array('Proposal.id' => $id),
'fields' => array('Upload.*', 'Proposal.*'),
'contain' => array('Proposal')
));
return($data);
}
Note:
CakePHP use explicit join instead of implicit join like ...from proposals, uploads...
I'm not familiar with that JOIN syntax but I believe it equals this:
SELECT *
FROM uploads
INNER JOIN proposals ON proposals.id = uploads.proposal_id
WHERE proposals.tender_id = 10
... so you need something similar to this:
// Untested
$conditions = array(
'Proposal.id' => $id,
'joins' => array(
array(
'alias' => 'Proposal',
'table' => 'proposals',
'type' => 'INNER',
'conditions' => 'Proposal.id = Upload.proposal_id',
),
),
);
Of course, this is a direct translation of your JOIN. If your models are properly related, it should all happen automatically.

Error in association or containment

This is one of those times when I know I'm doing something wrong, but I'm apparently deaf, dumb and blind with respect to seeing it. I'm hoping that another pair of eyes can open my own.
I have a ZipCode model and an Incentive model. There is a glorified join table (glorified because it has its own key) sitting in the middle. The join table has id, incentive_id and zip fields (legacy database). My ZipCode model HABTM Incentive as shown:
public $hasAndBelongsToMany = array(
'Incentive' => array(
'with' => 'ZipCodeIncentive',
'foreignKey' => 'zip',
'associationForeignKey' => 'incentive_id',
),
);
My Incentive model HABTM ZipCode as follows:
public $hasAndBelongsToMany = array(
'ZipCode' => array(
'with' => 'ZipCodeIncentive',
'foreignKey' => 'incentive_id',
'associationForeignKey' => 'zip',
),
I have a method, ZipCode::incentives( $zip ) that wants to pull all of the incentives relevant to the specified zip code:
$incentives = $this->Incentive->find(
'all',
array(
'contain' => array( 'ZipCode' ),
'fields' => array( 'Incentive.id', 'Incentive.name', 'Incentive.it_name', 'Incentive.amount', 'Incentive.state', 'Incentive.entire_state', 'Incentive.excluded', 'Incentive.is_active' ),
'conditions' => array(
'Incentive.excluded' => 0,
'Incentive.is_active' => 1,
'OR' => array(
'Incentive.state' => 'US', # nationwide incentives
array(
# Incentives that apply to the entire state the zip code belongs to
'Incentive.entire_state' => 1,
'Incentive.state' => $state,
),
'ZipCode.zip' => $zip # specific to the building's zip code
)
),
'order' => array( 'Incentive.amount DESC' ),
)
);
What I get for my trouble is the following error:
SQL Error: 1054: Unknown column 'ZipCode.zip' in 'where clause'
The ZipCode model's table isn't getting joined in the SQL, but I haven't grasped why yet. It's worth mentioning that the Incentive model is tied to a MySql view, not a table, via $useTable. I haven't seen anything to suggest that it should be a problem in this scenario, but it's non-standard.
If you see what I'm missing, please call 911 or at least drop an answer.
Rob
Move the condition
'ZipCode.zip' => $zip
To your contain declaration like this
array(
'contain' => array( 'ZipCode'=>
array('conditions'=>
array('ZipCode.zip' => $zip ))),
Then continue with the rest of your statement as usual

Resources