I have a query like this:
SELECT jo . * , pc.checklist_id FROM job_order_candidates jo,
pdpa_checklist pc WHERE jo.job_id = '5755' AND jo.shortlisted = '1'
AND pc.job_id = '5755'
I need to write it in Yii format, how to do it? At the moment I have this code:
$dataProviderSL=new CActiveDataProvider('JobApplication', array(
'criteria'=>array(
'condition'=>'shortlisted='.JobApplication::SL_Y.' AND job_id='.$jobOrder->job_id,
'order'=>'applied_date DESC',
'with'=>array('candidate'),
),
'pagination'=>array(
'pageSize'=>20,
),
));
It's only select single table, I need to select 2 tables. Can anyone help me?
Thanks alot.
It's hard to find the correspondance bewteen your ActiveRecords and your table but you should try something like:
$dataProviderSL=new CActiveDataProvider('JobApplication', array(
'criteria'=>array(
'condition'=>'shortlisted= :shortlisted',
'order'=>'applied_date DESC',
'params' => array(
':shortlisted' => JobApplication::SL_Y,
':job_id' => $jobOrder->job_id,
),
'with'=>array('candidate' => array(
'together' => true,
'condition' => 'job_id= :job_id'
)),
),
pagination'=>array(
'pageSize'=>20,
),
));
I'm note sure if job_id is in the relation or the main model, if you want a more precise answer you have to provide us more infos.
That SQL does not make sense if you are to use CActiveDataProvider philosophy.
CActiveDataProvider would provide you with an array of JobApplication models, which should be stored on the rows of your job_order_candidates table. Since pc.checklist_id belongs to another table, you should have another model that represents those entries, and on your JobApplication class you need a relation with that model. Assuming it would be called PdpaChecklist, your JobApplication would look like this:
class JobApplication extends CActiveRecord {
// your stuff
public function relations() {
return array(
'pdpaChecklist' => array(self::HAS_ONE, 'PdpaChecklist', 'job_id'),
);
}
}
That given, you can query JobApplications as you currently do and access that pc.checklist_id like the following:
foreach ($dataProviderSl->getData() as $jobApplication) {
$jobApplication->pdpaChecklist->checklist_id;
}
Since you will be iterating through JobApplications, it is a good idea to use eager loading, preventing Yii to run a query everytime you do $jobApplication->pdpaChecklist. To do so, include 'pdpaChecklist' to your 'with' statement.
'with'=>array('candidate', 'pdpaChecklist'),
That is probably more work than you would have imagined, but that is how Yii organizes things. When you synergizes with the phylosophy it goes mych easily and faster. Totally worth it.
Related
I am building a small core piece for my DataMapper ORM library in CodeIgniter to control user's access/edit rights (CRUD) over DataMapper objects itself. For this I want to link a DataMapper Entity, Userrole and Userright together.
The documentation is here http://datamapper.wanwizard.eu/pages/advancedrelations.html (Under head; Multi-table Relationships).
So if I fill in the join table by hand I can just get the values without any problems. The only problem is saving the relationship.
Here are my Model-rules
Userrole
var $has_many = array(
'userright' => array(
'join_table' => 'dm_entities_userrights_userroles',
),
'dm_entity' => array(
'join_table' => 'dm_entities_userrights_userroles',
),
);
Userright
var $has_many = array(
'dm_entity' => array(
'join_table' => 'dm_entities_userrights_userroles',
),
'userrole' => array(
'join_table' => 'dm_entities_userrights_userroles',
),
);
Dm_entity
var $has_many = array(
'userrole' => array(
'join_table' => 'dm_entities_userrights_userroles',
),
'userright' => array(
'join_table' => 'dm_entities_userrights_userroles',
),
);
It seems odd I declared the table twice constantly, but it seems not to work if I don't.
So here's my retrieval code for this relation form the perspective of the Userright;
$userrole = new Userrole;
$userrole->get_where(array('name' => 'Administrator'));
$dm_entity = new Dm_entity;
$dm_entity->get_where(array('name' => 'User'));
$userrights = new Userright;
$userrights->where_related($userrole);
$userrights->where_related($dm_entity)->get();
$output = '';
foreach ($userrights as $userright) {
$output .= '<i><b>'.$userright->name.'</b></i> and ';
}
$output = substr($output, 0, -5);
echo '<b>'.$userrole->name.'</b> has the right to '.$output.' the DataMapper entity <b>'.$dm_entity->name.'</b>.'.br();
So this works without any problems either. But now the saving part:
$new_userright = new Userright;
$new_userright->get_where(array('name' => 'Update'));
$new_userright->save(array($userrole, $dm_entity));
Which results in two entries in the dm_entities_userrights_userroles table where 1 entry has dm_entity_id empty and the other the userrole_id empty.
I hope to avoid making a separate Model for the join table to solve this.
Does anyone know how I can get this to work so it makes one correct entry, instead of two scattered ones?
This is not going to work (as you have noticed).
A relation is between two models, and for each Many to Many relation you need a separate join table.
Although technically you can create a join table with 10 FK's to 10 different models, Datamapper will not be aware of that, and will treat the table as different for every relation, causing duplicates to appear.
The only way to make this work, is to define a model for the join table too, and give that one-to-many relations to each of the other models. You can then use that model to manually add relations by assigning or updating the FK values, and still use the many-to-many relations for retrieval and update.
I have 2 tables namely Equipment and Supply.
I have used array_merge in yii php to serve as union for two different tables for the purpose of diplaying them in a single grid.
Everything works fine with the fields that is common with the two tables. The problem is when I try to display a field that is only existing in one of these two tables. It says Property "Supply.equipType" is not defined" because only equipment has the equipType relation.
in my gridView:
array(
'name'=>'equipment_type',
'value'=>'$data->equipType->name',
),
in my controller where I did the merging:
$prov1 = new CActiveDataProvider('BaseEiEquipItem', array(
'criteria' => array(
'condition' => 'id>0'
)));
$prov2 = new CActiveDataProvider('BaseSiReceivedItem', array(
'criteria' => array(
'condition' => 'id>0'
)));
$records=array_merge($prov1->data , $prov2->data);
$provAll = new CArrayDataProvider($records,
array(
'sort' => array( //optional and sortring
'keyField'=>false,
'attributes' => array(
'id', 'description',),
),
'pagination' => array('pageSize' => 10) //optional add a pagination
)
);
$this->render('create',array(
'model'=>$model,
'searchModel'=>$searchModel,
'modelGrid'=>$modelGrid,
'provAll' => $provAll,
));
in my equipment model :
public function relations() {
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'equipType' => array(self::BELONGS_TO, 'BaseRefEquipmentType', 'equipment_type'),
);
}
Any idea on how to solve this? Is there any way to fake a relation or something?
thanks in advance
Just add condition to check if equipType is there:
'value'=>'isset($data->equipType) ? $data->equipType->name : ""'
Using sql to merge results would be better, though.
Can't you just use plain SQL union?
If Equipment has 2 fields (a, b) and Supply has 2 fields (b, c), you can do union like this:
SELECT a, b, null FROM Equipment
UNION ALL
SELECT null, b, c FROM Supply
This problem shows, that it's probably not a good idea to mix different model types in one gridview. If you have a model that has no equipType then it's pretty obvious, that you can't show this column in a gridview. So what would you expect?
As a (dirty) workaround you can add all missing columns as pseudo attributes to the models where they are missing:
public $equipType;
I wrote 2 days ago to ask about andConditions and it appeared that I didn't understand the idea but the fact is that for two days now I am stuck with the next step using CakeDC:
How do I implement complex HABTM conditions in "query" methods for CakeDC search plugin?
I have Offer HABTM Feature (tables: offers, features, features_offers) and the below works just fine when used in controller:
debug($this->Offer->find('all', array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id in (8, 10)',
)
)
)
)
)
);
The problem comes when I want to use the same conditions in the search:
public $filterArgs = array(
array('name' => 'feature_id', 'type' => 'query', 'method' => 'findByFeatures'),
);
........
public function findByFeatures($data = array()) {
$conditions = '';
$featureID = $data['feature_id'];
if (isset($data['feature_id'])) {
$conditions = array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id' => $data['feature_id'],
)
)
)
);
}
return $conditions;
}
I get an error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'contain' in 'where clause'
which makes me think that I cannot perform this search and/or use containable behavior in searches at all.
Can someone with more experience in the field please let me know if I am missing something or point me to where exactly to find a solution for that - perhaps a section in the cookbook?
EDIT: Also tried the joins. This works perfectly fine in the controller, returning all the data I need:
$options['joins'] = array(
array('table' => 'features_offers',
'alias' => 'FeaturesOffers',
'type' => 'inner',
'conditions' => array(
'Offer.id = FeaturesOffers.offer_id'
),
array('table' => 'features',
'alias' => 'F',
'type' => 'inner',
'conditions' => array(
'F.id = FeaturesOffers.feature_id'
),
)
),
);
$options['conditions'] = array(
'feature_id in (13)' //. $data['feature_id']
);
debug($this->Offer->find('all', $options));
... and when I try to put in the search method I get the returned conditions only in the where clause of the SQL
WHERE ((joins = (Array)) AND (conditions = ('feature_id in Array')))
...resulting in error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'joins' in 'where clause'
EDIT: Maybe I am stupid and sorry to say that but the documentation of the plugin sucks a ton.
I double, triple and quadruple checked (btw, have lost already 30 hours at least on 1 filed of the search form facepalm) and the stupid findByTags from the documentation still doesn't make any sense to me.
public function findByTags($data = array()) {
$this->Tagged->Behaviors->attach('Containable', array('autoFields' => false));
$this->Tagged->Behaviors->attach('Search.Searchable');
$query = $this->Tagged->getQuery('all', array(
'conditions' => array('Tag.name' => $data['tags']),
'fields' => array('foreign_key'),
'contain' => array('Tag')
));
return $query;
}
As I understand it
$this->Tagged
is supposed to be the name of the model of the HABTM association.
This is quite far from the standards of cakePHP though: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
The way it is described here, says that you don't need another model but rather you associate Recipe with Ingredient as shown below:
class Recipe extends AppModel {
public $hasAndBelongsToMany = array(
'Ingredient' =>
array(
'className' => 'Ingredient',
'joinTable' => 'ingredients_recipes',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'ingredient_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
meaning that you can access the HABTM assoc table data from Recipe without needing to define model "IngredientRecipe".
And according to cakeDC documentation the model you need is IngredientRecipe and that is not indicated as something obligatory in the cakePHP documentation. Even if this model is created the HABTM assoc doesn't work properly with it - I tried this as well.
And now I need to re-write the search functionality in my way, using only cakePHP even though I spent already 30 hours on it... so unhappy. :(
Every time I come to do this in a project I always spend hours figuring out how to do it using CakeDC search behavior so I wrote this to try and remind myself with simple language what I need to do. I've also noticed that although Using the CakeDC search plugin with associated models this is generally correct there is no explanation which makes it more difficult to modify it to one's own project.
When you have a "has and belongs to many" relationship and you are wanting to search the joining table i.e. the table that has the two fields in it that joins the tables on either side of it together in a many-to-many relationship you want to create a subquery with a list of IDs from one of the tables in the relationship. The IDs from the table on the other side of the relationship are going to be checked to see if they are in that record and if they are then the record in the main table is going to be selected.
In this following example
SELECT Handover.id, Handover.title, Handover.description
FROM handovers AS Handover
WHERE Handover.id in
(SELECT ArosHandover.handover_id
FROM aros_handovers AS ArosHandover
WHERE ArosHandover.aro_id IN (3) AND ArosHandover.deleted != '1')
LIMIT 20
all the records from ArosHandover will be selected if they have an aro_id of 3 then the Handover.id is used to decide which Handover records to select.
On to how to do this with the CakeDC search behaviour.
Firstly, place the field into the search form:
echo $this->Form->create('Handover', array('class' => 'form-horizontal'));?>
echo $this->Form->input('aro_id', array('options' => $roles, 'multiple' => true, 'label' => __('For', true), 'div' => false, true));
etc...
notice that I have not placed the form element in the ArosHandover data space; another way of saying this is that when the form request is sent the field aro_id will be placed under the array called Handover.
In the model under the variable $filterArgs:
'aro_id' => array('name' => 'aro_id', 'type' => 'subquery', 'method' => 'findByAros', 'field' => 'Handover.id')
notice that the type is 'subquery' as I mentioned above you need to create a subquery in order to be able to find the appropriate records and by setting the type to subquery you are telling CakeDC to create a subquery snippet of SQL. The method is the function name that are going to write the code under. The field element is the name of the field which is going to appear in this part of the example query above
WHERE Handover.id in
Then you write the function that will return the subquery:
function findByAros($data = array())
{
$ids = ''; //you need to make a comma separated list of the aro_ids that are going to be checked
foreach($data['aro_id'] as $k => $v)
{
$ids .= $v . ', ';
}
if($ids != '')
{
$ids = rtrim($ids, ', ');
}
//you only need to have these two lines in if you have not already attached the behaviours in the ArosHandover model file
$this->ArosHandover->Behaviors->attach('Containable', array('autoFields' => false));
$this->ArosHandover->Behaviors->attach('Search.Searchable');
$query = $this->ArosHandover->getQuery('all',
array(
'conditions' => array('ArosHandover.aro_id IN (' . $ids . ')'),
'fields' => array('handover_id'), //the other field that you need to check against, it's the other side of the many-to-many relationship
'contain' => false //place this in if you just want to have the ArosHandover table data included
)
);
return $query;
}
In the Handovers controller:
public $components = array('Search.Prg', 'Paginator'); //you can also place this into AppController
public $presetVars = true; //using $filterArgs in the model configuration
public $paginate = array(); //declare this so that you can change it
// this is the snippet of the search form processing
public function admin_find()
{
$this->set('title_for_layout','Find handovers');
$this->Prg->commonProcess();
if(isset($this->passedArgs) && !empty($this->passedArgs))
{//the following line passes the conditions into the Paginator component
$this->Paginator->settings = array('conditions' => $this->Handover->parseCriteria($this->passedArgs));
$handovers = $this->Paginator->paginate(); // this gets the data
$this->set('handovers', $handovers); // this passes it to the template
If you want any further explanation as to why I have done something, ask and if I get an email to tell me that you have asked I will give an answer if I am able to.
This is not an issue of the plugin but how you build the associations. You need to properly join them for a search across these three tables. Check how CakePHP is fetching the data from HABTM assocs by default.
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#joining-tables
Suppose a Book hasAndBelongsToMany Tag association. This relation uses
a books_tags table as join table, so you need to join the books table
to the books_tags table, and this with the tags table:
$options['joins'] = array(
array('table' => 'books_tags',
'alias' => 'BooksTag',
'type' => 'inner',
'conditions' => array(
'Books.id = BooksTag.books_id'
)
),
array('table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'conditions' => array(
'BooksTag.tag_id = Tag.id'
)
)
);
$options['conditions'] = array(
'Tag.tag' => 'Novel'
);
$books = $Book->find('all', $options); Using joins allows you to have
a maximum flexibility in how CakePHP handles associations and fetch
the data, however in most cases you can use other tools to achieve the
same results such as correctly defining associations, binding models
on the fly and using the Containable behavior. This feature should be
used with care because it could lead, in a few cases, into bad formed
SQL queries if combined with any of the former techniques described
for associating models.
Also your code is wrong somewhere.
Column not found: 1054 Unknown column 'contain' in 'where clause'
This means that $Model->contain() is somehow called. I don't see such a call in your code pasted here so it must be somewhere else. If a model method can not be found this error usually happens with the field name as column.
I want to share with everyone that the solution to working with HABTM searches with the plugin lies here: Using the CakeDC search plugin with associated models
#burzum, the documentation is far from ok man. Do you notice the use of 'type' => 'checkbox' and that it is not mentioned anywhere that it is a type?
Not to mention the total lack of grammar and the lots of typos and missing prepositions. I lost 2 days only to get a grasp of what the author had in mind and bind the words in there. No comment on that.
I am glad that after 5 days on the uphill work I made it. Thanks anyway for being helpful.
I have an images table and a servers table. images has a server_id field which is a foreign key to the id field in the servers table. The servers table also has a field called name, which is what I want to retrieve.
Here's my controller action code:
$images = $this->Image->find('all', array(
'conditions' => array('Image.user_id' => $this->Auth->user('id')),
'order' => array('Image.uploaded DESC')
));
$this->set('images', $images);
It gets data like this:
Array
(
[0] => Array
(
[Image] => Array
(
[id] => 103
[orig_name] => Untitled-5.jpg
[hash] => MnfWKk
[filename] => MnfWKk.jpg
[uploaded] => 2012-07-12 00:09:08
[views] => 0
[album_id] =>
[user_id] => 15
[server_id] => 1
)
)
)
Instead of server_id, I want to get the name field from the servers table. How can I adapt my find() method to get this? I know it's an SQL join, but I have no idea how to tell Cake to do one in order to get the servers name.
Thanks.
TLDR:
Set up the correct CakePHP associations, and use CakePHP's Containable. (with recursive -1).
Longer Description:
It's best practice to keep your find code in the model itself, so that's what I'll show, but feel free (if you must) to move it back into the controller.
Doing it this way allows you to call the same getImages() function from any controller, and just pass different parameters based on what you want returned. The benefit to coding like this is, you always know if you're looking for code related to queries/database, that you should be looking in the model. It's VERY beneficial when the next person who looks at your code doesn't have to go searching.
Because of the association set up between Image and Server, you can then "contain" the Server info when you query images. But - you can't use "contain" until you specify that you want your model to $actAs = array('Containable');. [ CakePHP Book: Containable ]
Lastly, in your AppModel, it's good practice to set $recursive = -1;. That makes it default to -1 for all models. If for some reason you're against doing that, just make sure to set recursive to -1 any time you use containable. And - once you learn to use containable, you'll never look back - it's awesome. There are a lot more things you can
Code:
//AppModel *******
//...
$recursive = -1;
//...
//Images controller *******
//...
public function whatever() {
$opts = array();
$opts['user'] = $this->Auth->user('id');
$images = $this->Image->getImages($opts);
$this->set(compact('images'));
}
//...
//Image model *******
//...
public $actsAs = array('Containable');
public belongsTo = array('Server');
public function getImages($opts = array()) {
$params = array('condtions'=>array());
//specific user
if(!empty($opts['user'])) {
array_push($params['conditions'], array('Image.user_id'=>$opts['user']);
}
//order
$params['order'] = 'Image.uploaded DESC';
if(!empty($opts['order'])) {
$params['opts'] = $opts['order'];
}
//contain
$params['contain'] = array('Server');
//returns the data to the controller
return $this->find('all', $params);
}
A few other notes
You should also set the association in your Server model.
The code example I gave is written fairly verbosely (is that a word?). Feel free to condense as you see fit
You can also extend the model's getImages() method to accept a lot more parameters like find, limit...etc. Customize this all you want - it's not THE way to do it - just similar to what I usually use.
Per your question, if you only need one field, you can specify in the "contain" what fields you want - see the book for details.
It might seem confusing now, but it's SOO worth learning how to do this stuff right - it will make your life easier.
Cake have a lot of model relationships to achieve this. Check out this page, I think you'll be using the belongsTo relationship
Easy and alternate way for begginers
$images = $this->Image->find('all', array(
'conditions' => array('Image.user_id' => $this->Auth->user('id')),
'joins' => array(
array(
'table' => 'servers',
'alias' => 'Server',
'type' => 'inner', //join of your choice left, right, or inner
'foreignKey' => true,
'conditions' => array('Image.server_id=Server.id')
),
),
'order' => array('Image.uploaded DESC')
));
This is very good in performance
I've been quite some time trying to use the Containable Behavior in CakePHP but I can't get to make it work as I expected.
My application is different, but to simplify I'll put this example. Let's say I have a forum with threads and activities, and the activities can be rated. The general relations would be:
Forum: hasMany [Thread]
Thread: belongsTo [Forum], hasMany [Activity]
Activity: belongsTo [Thread], hasMany [Rating]
Rating: belongsTo [Activity]
What I want to achieve is, using the find method, get all the ratings performed on a certain forum. What I suppose should be done is the following:
$this->Rating->find('count', array(
'contain' => array(
'Activity' => array(
'Thread'
)
),
'conditions' => array(
'Thread.forum_id' => 1
)
));
But the result query is:
SELECT COUNT(*) AS `count` FROM `ratings` AS `Rating` LEFT JOIN `activities` AS `Activity` ON (`Rating`.`activity_id` = `Activity`.`id`) WHERE `Thread`.`forum_id` = 1;
I've accomplished this using the 'joins' option, but it's more complex and I have to use this kinda action in many situations.
All the files related with the example can be found here: http://dl.dropbox.com/u/3285746/StackOverflow-ContainableBehavior.rar
Thanks
Update 23/11/2011
After investigating the framework and thanks to the answers of Moz Morris and api55 I found the source of the problem.
The basic problem was that, as I understood CakePHP, I thought it was querying using joins each time. The thing it that it doesn't do that, the real operation it would perform to obtain the result I was looking for would be something like this:
SELECT * FROM Rating JOIN Activity...
SELECT * FROM Activity JOIN Thread...
SELECT * FROM Activity JOIN Thread...
...
Meaning that it would do a query to get all the activities and then, for each activity, perform a query to get the Threads... My approach was failing not because of the Containable Behaviour being used wrong, but because the 'conditions' option was applied to all queries and, on the first one, it crashed because of the absence of the Thread table. After finding this out, there are two possible solutions:
As api55 said, using the conditions inside the 'contain' array it would apply them only to the queries using the Thread table. But doing this the problem persists, because we have way too many queries.
As Moz Morris said, binding the Thread model to Rating would also work, and it would perform a single query, which is what we want. The problem is that I see that as a patch that skips the relations betweem models and doesn't follow CakePHP philosophy.
I marked api55 solution as the correct because It solves the concrete problem I had, but both give a solution to the problem.
First of all, have you put the actAs containable variable in the appModel?? without it this beahaviour won't work at all (i see it is not working correctly since it didn't join with Thread table)
I would do it from the top, i mean from forum, so you choose your forum (im not sure you want forum or thread) and get all its rating, if theres no rating you will end up with the rating key empty.
something like this
appModel
public $actsAs = array('Containable');
rating controller
$this->Rating->Activity->Thread->Forum->find('count', array(
'contain' => array(
'Thread' => array(
'Activity' => array(
'Rating' => array (
'fields' => array ( 'Rating.*' )
)
)
)
),
'conditions' => array(
'Forum.id' => 1
)
));
Then if you need only a value in rating table just use Set:extract to get an array of this value.
As you did it IT SHOULD work anyways but i sugest not to use forum_id there, but in conditions inside contain like this
'contain' => array(
'Activity' => array(
'Thread' => array(
'conditions' => array('Thread.forum_id' => 1)
)
)
),
Also, never forget the actsAs variable in the model using the containable behaviuor (or in app model)
Whist I like api55's solution, I think the results are a little messy - depends on what you intend to do with the data I guess.
I assume that when you said using the 'joins' method you were talking about using this method:
$this->Rating->bindModel(array(
'belongsTo' => array(
'Thread' => array(
'foreignKey' => false,
'conditions' => 'Thread.id = Activity.thread_id',
),
'Forum' => array(
'foreignKey' => false,
'conditions' => 'Forum.id = Thread.forum_id'
)
)
));
$ratings = $this->Rating->find('all', array(
'conditions' => array(
'Forum.id' => 1 // insert forum id here
)
));
This just seems a little cleaner to me, and you don't have to worry about using the containable behaviour in your AppModel. Worth considering.