CakePHP - SQL query Join - php

Ok, so i'm in a pickle trying to figure a way for how to do this :
I have a live search(using ajax) which allows the user to select a criteria from a dropdown list and then enter a value which will be matched to values inside the database. This is quite trivial stuff, and on direct fields of the main model, i don't have any issues.
I have a Donor Model/table which consists of attributes such as ID, Name, Surname etc, but more importantly it also has FK of other associated models such as blood_group_id, donor_type_id, which map back to the respective models (BloodGroup and DonorType).. These two are already set with the associations and I am beyond that part, as I am already retrieving Donor records with associated model data.
Here is the search method which will hopefully help you in understanding my problem better.
public function search() {
if($this->request->is('post')){
if(!empty($this->request->data)){
$criteria = $this->request->data['criteria'];
$query = $this->request->data['query'];
$conditions = array("Donor." .$criteria. " LIKE '". $query . "%'");
The above checks if its a post request and whether data was sent. The criteria and user input are used to construct the query..
This is where my problem arises.. (When the user select search By blood type, as a criteria from the drop down)the above expects the user to enter the id of the blood_group rather than A+ or A- for instance.. So if the input is 1(id of blood group A+), the results are returned as expected. But I want the user to be able to enter A+...
Here is the rest of the method :
$this->Paginator->settings = array(
'conditions' => $conditions,
'limit' => 2
);
$donors = $this->Paginator->paginate('Donor');
$this->set('donors', $donors);
$this->beforeRender();
$this->layout= 'ajax';
}
}
}
I have tried this approach, setting up the conditions using the Model's name such as
if($criteria == 'blood_group_id'){
$conditions = array("BloodGroup.id" . " LIKE '". $query . "%'");
}elseif($criteria == 'donor_type_id'){
$conditions = array("DonorType.id" . " LIKE '". $query . "%'");
}else{
$this->Paginator->settings = array(
'conditions' => $conditions,
'limit' => 2
);
}
But this returns all the records irrespective of the input.
I also tried changing the settings for the paginator with no luck
$settings = array(
'joins' => array(
'table' => 'blood_groups',
'alias' => 'BloodGroup',
'type' => 'LEFT',
'conditions' => array(
"BloodGroup.id" => "Donor.blood_group_id",
"AND" => $conditions
)
),
'limit'=> 2
);
Any help on how to accomplish what I explained above, would greatly be appreciated!

simply:
if($criteria == 'blood_group_id')
$conditions = array("BloodGroup.name LIKE" => $query.'%');
(assuming bood_types has a 'name' column)
Also let me suggest you to use the CakeDC search plugin (https://github.com/CakeDC/search).

Related

query and UPDATE in cakephp1.3

would like to update attached_blog_id field to set NULL. Thank you in advance.
foreach($this->Event->find('all', array('conditions' => array('Event.attached_blog_id' => $this->id()))) as $attached_blog)
$this->Event->query('UPDATE armo8427_events' . ' SET attached_blog_id = NULL' . ' WHERE id = ' . $attached_blog['Event']['id']);
Don't use query unnecessarily
The code in the question would be better written as:
$events = $this->Event->find(
'all',
array(
'conditions' => array(
'Event.attached_blog_id' => $this->id()
)
)
);
foreach($events as $event) {
$this->Event->id = $event['Event']['id'];
$this->Event->saveField('attached_blog_id', null);
}
With the code-logic in the question there is no need to use query at all
But be efficient
The logic in the question can be expressed as a single sql query, instead of 1+n:
UPDATE
events
SET
attached_blog_id = NULL
WHERE
attached_blog_id = $id;
I.e. if there were 100 linked blogs, using the foreach loop will issue 101 queries, wheras this is the same in one query irrespective of the number rof affected rows.
The most appropriate way to do that in CakePHP is to use updateAll:
$id = $this->id(); // From the question
$this->Event->updateAll(
array('Baker.attached_blog_id' => null), // the update
array('Baker.attached_blog_id' => $id) // conditions to match
);
the method query should be reserved for sql calls which are not possible to achieve using the provided model methods.

Cakephp Validate uniqueness of name field w.r.t other field of same table

I am a kind of noob in cakephp and While working with this validation rule`, I were unlucky to get the satisfactory response.
In my project, I have to valiidate the name w.r.t foreign key and many other attribute like is_active
Lets Say
Table t1 has attributes (id, name varchar, is_active Boolean)
table t2 has attribute (id, name varchar, t1_id (foreign key to table t1), is_active boolean)
Now I want to validate unique name of table t1 of is_active group
And validate uniqueness of t2.name w.r.t is_active=1 and t1_id=specific_value
I googled and found a link regarding this with no luck :(
http://www.dereuromark.de/2011/10/07/maximum-power-for-your-validation-rules/
Any help will really be appreciated.
First some suggestions that may be worth considering;
Soft delete
Apparently you're trying to implement a 'soft delete' in your website. Soft-deletes are sometimes wanted in case you want to delete something, but being able to 'undelete' it in a later stage.
However, by allowing both active and inactive items to share the same name, 'name' is no longer unique (based on your question, unique names are a requirement).
This will prevent you from undeleting an item, because at that point, two items with the same name exist in your database, both active.
Here are some discussions on the subject of 'soft deletes';
Are soft deletes a good idea?
http://richarddingwall.name/2009/11/20/the-trouble-with-soft-delete/
Revisions/History
If you're trying to implement a 'revision history', not a 'soft delete', it's best to store revisions in a separate table. There are already some plugins for CakePHP that may handle this for you. I don't have links to them, but you'll be able to Google for that.
Custom validation
Back to your question; you are able to check if a record is unique within the 'active' group by creating your own validation-rule inside the Model, for example:
public function isNameUniqueActive()
{
if (
array_key_exists('is_active', $this->data[$this->alias])
&& 1 != $this->data[$this->alias]['is_active']
) {
// Record to save is not 'active', so no need to check if it is unique
return true;
}
$conditions = array(
$this->alias . '.name' => $this->data[$this->alias]['name'],
$this->alias . '.is_active' => 1,
);
if ($this->id) {
// Updating an existing record: don't count the record *itself*
$conditions[] = array(
'NOT' => array($this->alias . '.' . $this->primaryKey => $this->id)
);
}
return (0 === $this->find('count', array('conditions' => $conditions)));
}
You'll be able to use this validation just like the built-in validation rules;
public $validate = array(
'name' => 'isNameUniqueActive',
);
update
To also check if a name is unique within a group (based on foreign-key t1_id), try this:
public function isNameUniqueWithinGroup()
{
if (
array_key_exists('is_active', $this->data[$this->alias])
&& 1 != $this->data[$this->alias]['is_active']
) {
// Record to save is not 'active', so no need to check if it is unique
return true;
}
if (!array_key_exists('t1_id', $this->data[$this->alias])) {
// No group specified - not valid?
return false;
}
$conditions = array(
$this->alias . '.name' => $this->data[$this->alias]['name'],
$this->alias . '.is_active' => 1,
$this->alias . '.t1_id' => $this->data[$this->alias]['t1_id'],
);
if ($this->id) {
// Updating an existing record: don't count the record *itself*
$conditions[] = array(
'NOT' => array($this->alias . '.' . $this->primaryKey => $this->id)
);
}
return (0 === $this->find('count', array('conditions' => $conditions)));
}

CakePHP: Load count of related Models with bindModel

I have two models in an 1:n relation and I just want to load the count of the related items.
First one is the table/model "Ad" (one) which is related to "AdEvent" (many). AdEvents has a foreign key "ad_id".
In the controller I can use it that way and it loads the related AdEvent-records.
$this->Ad->bindModel(array('hasMany' => array(
'AdEvent' => array(
'className' => 'AdEvent',
'foreignKey' => 'ad_id',
))));
Now I just need the count without the data and I tried with param "fields" and "group" a COUNT()-statement, but in that case the result is empty. I also changed the relation to "hasOne", but no effect.
Any idea how to use the Cake-magic to do that?
EDIT:
With simple SQL it would look like this (I simplyfied it, a.id instead of a.*):
SELECT a.id, COUNT(e.id) AS count_events
FROM cake.admanager_ads AS a
JOIN ad_events AS e ON e.ad_id = a.id
GROUP BY a.id
LIMIT 50;
You can always do a manual count of course. This is what I almost always end up doing because I almost always have the data loaded already for some other purpose.
$Ads = $this->Ad->find('all')
foreach ($Ads as $Ad) {
$NumAdEvents = array(
$Ad['Ad']['id'] => sizeof($Ad['AdEvents']),
)
}
debug($NumAdEvents);
die;
Or you can use a find('count'):
$id_of_ad = 1; //insert your ad id here, or you can search by some other field
$NumAdEventsAtOneAd = $this->AdEvent->find('count', array('conditions' => array(
'AdEvent.ad_id' => $id_of_ad,
)));
debug($NumAdEventsAtOneAd);
die;

CGridView filtering with aggregate (grouping) functions - eg MAX()

I have a CGridView that uses a MAX() mysql column to provide data for one of the columns. I have that working with sorting, but I can't figure out filtering. I assumed that I could just use CDbCriteria::compare() call to set it, but it's not working. Ideas?
My search function:
$criteria = new CDbCriteria;
$criteria->condition = 't.is_deleted = 0 and is_admin = 0';
// get last note date
$criteria->select = array('t.*', 'MAX(n.visit_date) AS last_note');
$criteria->join = 'LEFT JOIN notes n ON t.id = n.distributor_id';
$criteria->group = 't.id';
$criteria->order = 't.status';
$criteria->compare('t.type', $this->type);
$criteria->compare('t.name', $this->name, true);
$criteria->compare('t.city', $this->city, true);
$criteria->compare('t.state', $this->state);
$criteria->compare('last_note', $this->last_note);
return new CActiveDataProvider('Distributor', array(
'criteria' => $criteria,
'pagination' => array(
'pageSize' => 20
),
'sort' => array(
'defaultOrder' => 'name',
'attributes' => array(
'last_note' => array(
'asc' => 'last_note',
'desc' => 'last_note DESC'
),
)
),
));
In my view, I just have the name and value values set.
The error I get is CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'last_note' in 'where clause'
Edit: The only way I found to do this is (because you can't have aggregate functions in where clauses) is to check the $_GET array for the last_note value, and add a having clause. Note, this isn't parameterized or anything yet, I just wanted to rough it out to see if it would work:
if(isset($_GET['Distributor']['last_note']) && $_GET['Distributor']['last_note'] != '') {
$criteria->having = 'MAX(n.visit_date)=\'' . $this->last_note . "'";
}
I hate using the request variables in the model like this, but there isn't much else I could do.
You're on the right track. You filter aggregate functions like MAX() and SUM() using having. In a query like this, you should probably add a group, otherwise you'll end up with a lot of duplicated data in your result when a Distributor has multiple notes on the same day (assuming t.id is a primary key). Second, you should use named params instead of putting variables directly into SQL. So, instead of compare('last_note'... you'd end up with something like this:
$criteria->group = 't.id';
$criteria->having = 'MAX(n.visit_date) = :last_note';
$criteria->params = array(':last_note' => $this->last_note);
In order for filtering to work, you should have a class attribute to store the data:
public $last_note;
otherwise $this->last_note wont return anything and your $criteria->compare('last_note', $this->last_note); wont do anything either

Using the CakeDC search plugin with associated models

I'm using CakePHP 1.3.8, and I've installed the CakeDC Search plugin. I have a Tutorial model, which is in a HABTM relationship with a LearningGoal model.
I have a search action & view in the Tutorials controller with which I can successfully search fields in the Tutorial model. I'd also like to filter my tutorial search results using LearningGoal checkboxes on the same form. I've tried adding various parameters to Tutorial's $filterArgs and TutorialsController's $presetVars. I've also tried moving the relevant $filterArgs to the LearningGoal model. I have not yet been able to successfully trigger the entry for learning goals in $filterArgs.
I think I must be missing something obvious. Or maybe the Search plugin doesn't support what I'm trying to do. Does anyone know how to use this plugin to search on associated models?
So here's what I've figured out. You can combine what's below with the Search plugin directions to search on related models.
The $filterArgs piece in the Tutorial model must look like this:
var $filterArgs = array(
array('name' => 'LearningGoal', 'type' => 'subquery', 'method' => 'findByLearningGoals', 'field' => 'Tutorial.id'),
);
Here's the supporting function in the Tutorial model:
function findByLearningGoals($data = array()) {
$ids = explode('|', $data['LearningGoal']);
$ids = join(',', $ids);
$this->LearningGoalsTutorial->Behaviors->attach('Containable', array('autoFields' => false));
$this->LearningGoalsTutorial->Behaviors->attach('Search.Searchable');
$query = $this->LearningGoalsTutorial->getQuery('all',
array(
'conditions' => array('LearningGoalsTutorial.learning_goal_id IN (' . $ids . ')'),
'fields' => array('tutorial_id'),
)
);
return $query;
}
In TutorialsController, $presetVars should look like this:
public $presetVars = array(
array('field' => 'LearningGoal', 'type' => 'checkbox', 'model' => 'Tutorial'),
);
And in my search action in TutorialsController, I did this:
$this->LearningGoal = $this->Tutorial->LearningGoal;
The Prg component seems to need that.
I am using CakePHP version 2.X
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 Michael 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.

Categories