In my GridView I want to display all records of my table 'reply'. The table has relations to the tables 'author' and 'task' and not every reply has a task.
The table 'task' has a relation with another table called 'concern'.
Here ist the relations() of my model Reply:
public function relations() {
return array(
'trak' => array(self::HAS_MANY, 'Task', 'reply_id', 'condition' => 'task.deleted<>1'),
'author' => array(self::BELONGS_TO, 'Author', 'author_id'),
);
}
The search() method of my model Reply has the following code:
public function search() {
$criteria = new CDbCriteria;
$criteria->with = array(
'author' => array('select' => 'id, name, role, office_id, abk', 'together' => false),
'author.office' => array('select' => 'id, name'),
'task' => array('select' => 'id, concern_id', 'together' => true),
'task.concern' => array('select' => 'id, classification_id', 'alias' => 'concern'),
);
$criteria->compare('t.id', $this->id);
$criteria->compare('t.create_time', $this->create_time);
$criteria->compare('t.create_date', $this->create_date, true);
$criteria->compare('t.office.id', $this->search_office);
$criteria->compare('t.author_id', $this->author_id);
$criteria->compare('t.rel', $this->rel, true);
$criteria->compare('t.author_id', $this->author_id);
$criteria->compare('t.lektor_id', $this->lektor_id);
$criteria->compare('t.issue_id', $this->issue_id);
$criteria->compare('t.reply_text', $this->reply_text, true);
$criteria->compare('t.deleted', $this->deleted);
if (EWMParam::getValue(EWMParam::MODUL_SCHLAGWORTE))
$criteria->compare('t.tags', $this->tags, true);
$criteria->compare('t.text_name', $this->text_name, true);
$criteria->compare('t.use_count', $this->use_count);
$criteria->compare('concern.classification_id', $this->classification_id);
$criteria->compare('t.update_time', $this->update_time);
$criteria->compare('t.update_user', $this->update_user);
$criteria->compare('t.global', $this->global);
if (EWMParam::getValue(EWMParam::MODUL_confirmationN))
$criteria->compare('t.confirmation', $this->confirmation);
$criteria->compare('t.confirmation_text', $this->confirmation_text, true);
$criteria->compare('t.use', $this->use, true);
$pagination = EWMPageSortFilterHelper::getPagination($this);
$sort = new CSort();
$sort->defaultOrder = 't.id DESC';
$sort->attributes = array(
'global' => 't.global',
'search_office' => 'office.name',
'id' => 't.id',
'text_name' => 't.text_name',
'confirmation' => 't.confirmation',
'author_id' => 'author.name',
'create_date' => 't.create_date',
'tags' => 't.tags',
'use' => 't.use',
'classification_id' => 'classification_id',
);
$sort->applyOrder($criteria);
return new CActiveDataProvider($this, array(
'criteria' => $criteria,
'pagination' => $pagination,
'sort' => $sort
));
}
In my GridVew only replys with a task are displayed and all filters work fine. But I want to display all replys with a task and all replys without a task. If I
delete in the search() method in the array for $criteria->with the elements 'task' and 'task.concern' all replys are displyed. But the filter for the row
'Classifcation' which comes from the relation 'task.concern' doesn't work. Logically I get the error "Column not found: 1054 Unknown column 'concern.classification_id' in 'where clause'".
Is it possible to display all replys and to filter those replys by the classification? Do you have an idea?
You should look into how Yii relations work.
One thing that comes into my mind is that when adding the relations in the with() property the SQL generated might include INNER JOINs.
That's why when having the relations included you're not getting any reply without tasks.
In order to fix that you should make sure that the SQL generated is using LEFT JOINs.
You can do that by using joinType property of a relation:
http://www.yiiframework.com/doc/api/1.1/CActiveRelation#joinType-detail
And you could specify LEFT JOIN there.
'task' => array('select' => 'id, concern_id', 'together' => true, 'joinType'=>'LEFT JOIN'),
'task.concern' => array('select' => 'id, classification_id', 'alias' => 'concern', 'together' => true, 'joinType'=>'LEFT JOIN'),
That might do the trick. But maybe you need some more tweaking if it is not working just as expected.
If you want to debug what is actually happening, you could put a bad field/table name inside the criteria that would result in a database error and then you can look in the SQL code that was executed and see how tables are joined.
Suggested links:
http://www.yiiframework.com/doc/guide/1.1/en/database.arr
http://www.yiiframework.com/wiki/527/relational-query-lazy-loading-and-eager-loading-with-and-together/
http://www.yiiframework.com/wiki/428/drills-search-by-a-has_many-relation
Related
I have three Models Quotation, QuotationItem and Job.
My main Objective is to do a left join for tables of Quotation and QuotationItem in the Job controller. I cannot achieve it because instead of QuotationItem ,the job table is being used in left join!
$this->Job->unbindModel(
array('belongsTo' => array('Quotation','QuotationItem')), true
);
$options = array(
'fields' => array(
'QuotationItem.id',
'QuotationItem.Quot_id',
'QuotationItem.item_sno',
'QuotationItem.job_desc',
'QuotationItem.selected_qty',
'QuotationItem.paper_id',
'QuotationItem.plate_id',
'QuotationItem.design_id',
'QuotationItem.ink_id',
'QuotationItem.misel_id',
'QuotationItem.plate_size',
'QuotationItem.paper_size',
'QuotationItem.paper_type',
'QuotationItem.paper_gsm',
'QuotationItem.plate_qty',
'QuotationItem.paper_qty',
'QuotationItem.ink_qty',
'QuotationItem.plate_color',
'QuotationItem.ink_color',
'QuotationItem.ink_code',
'QuotationItem.plate_price',
'QuotationItem.paper_price',
'QuotationItem.ink_price',
'QuotationItem.design_price',
'QuotationItem.plate_total',
'QuotationItem.paper_total',
'QuotationItem.ink_total',
'QuotationItem.design_total',
'QuotationItem.printing_cost',
'QuotationItem.prepress_cost',
'QuotationItem.design_cost',
'QuotationItem.press_cost',
'QuotationItem.folding_cost',
'QuotationItem.binding_cost',
'QuotationItem.block_cost',
'QuotationItem.lamination_cost',
'QuotationItem.uv_cost',
'QuotationItem.stamping_cost',
'QuotationItem.diecutting_cost',
'QuotationItem.sewing_cost',
'QuotationItem.perfectbind_cost',
'QuotationItem.saddlestitch_cost',
'QuotationItem.emboss_cost',
'QuotationItem.cutting_cost',
'QuotationItem.labor_charges',
'QuotationItem.others',
'QuotationItem.cost_total',
'QuotationItem.total_item_sum',
'QuotationItem.net_total',
'QuotationItem.created',
'QuotationItem.modified',
'QuotationItem.status',
'Quotation.Quot_id',
'Quotation.job_item',
'Quotation.customer_id',
'Quotation.customer_name',
'Quotation.customer_phone',
'Quotation.customer_email',
'Quotation.customer_address',
'Quotation.salesperson',
'Quotation.pay_terms',
'Quotation.contact_id',
'Quotation.status',
'Quotation.discount',
'Quotation.total',
'Quotation.created',
'Quotation.modified',
),
'joins' => array(
array(
'table' => 'quotation',
'alias' => 'Quotation',
'type' => 'left',
'conditions' => array('QuotationItem.Quot_id = Quotation.Quot_id'),
)
),
'conditions' => array(
'1',
));$data = $this->Job->find('all', $options);`
You need to run the find on Quotation, not on Job. Models are tied to a table, think of them as one-in-the-same. You can't find something from a different table that has nothing to do with the Model you're executing the find function on.
If you're getting sql 42000 error. Table/alias not unique when loading a model, it's either already been loaded or you're trying to alias something with a name that's already been used for something else - which you can't do.
The simplest way to ensure you have access to all 3 Models from the Job Controller if you haven't grasped model associations well yet is to declare public $uses = array('Job','Quotation','QuotationItem'); in your Job Controller.
Whatever way you go about getting access to Quotation aside, what you want to do is then:
$this->Quotation->find('all',array(
'fields' => '*',
'joins' => array(
array(
'table' => 'quotation',
'alias' => 'Quotation',
'type' => 'left',
'conditions' => array(
'QuotationItem.Quot_id = Quotation.Quot_id'
)
)
)
));
Thank you #Tim for making me understand about model and controller. I have found the answer to my Question.
I used the join in my QuotationItem model since the QuotationItem belongs to Quotation .
public $belongsTo = array(
'Quotation' => array(
'className' => 'Quotation',
'foreignKey' => 'Quot_id',
'type'=>'left',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
Then I declared public $uses = array('Job','Quotation','QuotationItem'); in my jobs controller
after that in my add function I used
$quotationItem=$this->QuotationItem->find('all');
$this->set(compact( 'quotationItem'));
Finally I got my joins working
I have a problem with nested model pagination using containable in Cake...
I've got three models: Category, CompanyCategory, Company and there association is like this
Category hasMany CompanyCategory
CompanyCategory belongsTo Category
CompanyCategory belongsTo Company
Company hasMany CompanyCategory
I get data using contain, as shown below:
$options = [
'conditions' => ['Category.slug' => $slug],
'contain' => [
'CompanyCategory.Company.CompanyAddress'
]
];
return $this->find('first', $options);
Everything works properly, until I wanted to paginate the nested model - Company. I use CategoriesController with show method, which renders the view with the selected category and associated companies (Company hasMany Categories and Category hasMany Companies).
I've tried something like this:
$this->Paginator->settings = [
'limit' => 1,
'order' => [
'id' => 'asc'
],
'contain' => array('CompanyCategory.Company')
];
$data = $this->Paginator->paginate('Category.CompanyCategory.Company', array('Category.slug LIKE' => $slug));
$this->set('category', $data);
But this didn't work for me :(
Any suggestions / help?
If you define Category has a hasAndBelongsToMany relation, it would be easy for you. Thus you created a new Model called CompanyCategory, i assume there is a reason..
You can try this-
$this->Category->bindModel(
array(
'hasAndBelongsToMany' => array(
'Company'
)
)
);
$this->Paginator->settings = array(
'conditions' => array(
'Category.slug' => $slug
),
'limit' => 1,
'order' => array(
'id' => 'asc'
),
'contain' => array(
'Company'
)
);
I assume your relation table name is categorys_companies.... If not then you need take different approach
Finnaly I resolve my problem using
$this->Paginator->settings = array(
'limit' => 2,
);
$this->Paginator->paginate('Category.CompanyCategory.Company');
I forgot to write that I use method from CategoriesController, not Companies.
But now another problem blocked me... I use TranslateBehaviour, and data which I get has no translations...
Any idea? I googled a lot, but couldn't find any satisfactory solution.
I'm trying to add two fields (Shipping Postcode and Billing Postcod) to Magento 1.7CE's backend Sales grid.
I'm doing this by overriding Mage_Adminhtml_Block_Sales_Order_Grid::setCollection(...) to join the table with sales/order_address.
public function setCollection($collection){
parent::setCollection($collection);
$collection->getSelect()->join(
array('address_shipping' => $collection->getTable("sales/order_address")),
'main_table.entity_id = address_shipping.parent_id AND address_shipping.address_type = "shipping"',
array('postcode')
);
$collection->getSelect()->join(
array('address_billing' => $collection->getTable("sales/order_address")),
'main_table.entity_id = address_billing.parent_id AND address_billing.address_type = "billing"',
array('postcode')
);
}
And Mage_Adminhtml_Block_Sales_Order_Grid::_prepareColumns(...) to add the columns to Sales Grid.
protected function _prepareColumns(){
$this->addColumn('shipping_postcode', array(
'header' => Mage::helper('sales')->__('Shipping Postcode'),
'index' => 'postcode',
));
$this->addColumn('billing_postcode', array(
'header' => Mage::helper('sales')->__('Billing Postcode'),
'index' => 'postcode',
));
return parent::_prepareColumns();
}
The issue is that both times I need to select postcode field from sales/order_address table which results in SQL error
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'postcode' in order clause is ambiguous
I have tried to use MySQL's AS alias to differentiate between two postcodes, by modifying setCollection(...) to pass array('shipping_postcode'=>'postcode') and array('billing_postcode'=>'postcode') as well as changing _prepareColumns(...) to say 'index' => 'shipping_postcode' and 'index' => 'billing_postcode'. This resulted in another SQL error
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'shipping_postcode' in 'where clause'
How can I achieve having both columns in the Sales Grid?
let try this first code
public function setCollection($collection){
parent::setCollection($collection);
$collection->getSelect()->join(
array('address_shipping' => $collection->getTable("sales/order_address")),
'main_table.entity_id = address_shipping.parent_id AND address_shipping.address_type = "shipping"',
array('address_shipping.postcode as shippingpostcode')
);
$collection->getSelect()->join(
array('address_billing' => $collection->getTable("sales/order_address")),
'main_table.entity_id = address_billing.parent_id AND address_billing.address_type = "billing"',
array('address_billing.postcode as billingpostcode')
);
}
Second _prepareColumns() is here,
protected function _prepareColumns(){
$this->addColumn('shippingpostcode', array(
'header' => Mage::helper('sales')->__('Shipping Postcode'),
'index' => 'shippingpostcode',
'filter_index' => 'address_shipping.postcode'
));
$this->addColumn('billingpostcode', array(
'header' => Mage::helper('sales')->__('Billing Postcode'),
'index' => 'billingpostcode',
'filter_index' => 'address_billing.postcode'
));
return parent::_prepareColumns();
}
if you want to know more about 'filter_index', comment in/out in this, then try sorting in your grid for post code column. You will see different result. If you remove filter_index, error in sorting.
Before return parent::_prepareCollection();
You should create a join:
$collection->getSelect()->joinLeft(array('billing'=>'sales_flat_order_address'),
'main_table.entity_id = billing.parent_id AND billing.address_type="billing"',array('billing.postcode AS bp'));
And in the method _prepareColumns paste:
$this->addColumn('bp', array(
'header' => Mage::helper('sales')->__('Billing Postcode'),
'index' => 'bp',
'width' => '60px',
'filter_index' => 'billing.postcode'
));
I need some help with CakePHP 2.2.3.
What I have
I have the following setup at the moment:
Post hasMany Attachment
It works fine and the page is generated with 2 queries:
SELECT *, `Post`.`id`
FROM `posts` AS `Post`
WHERE 1 = 1
ORDER BY `Post`.`created` DESC
SELECT
`Attachment`.`id`,
`Attachment`.`post_id`,
`Attachment`.`created`
FROM
`attachments` AS `Attachment`
WHERE
`Attachment`.`post_id` IN (1, 2, 3, ..., n)
What I want
I want to extend the relation to be as follows:
Post hasMany Attachment; every Attachment belongsTo Type
And I don't know hot to make CakePHP follow it.
Basically, what I need is:
SELECT *, `Post`.`id`
FROM `posts` AS `Post`
WHERE 1 = 1
ORDER BY `Post`.`created` DESC
SELECT
`Attachment`.`id`,
`Attachment`.`post_id`,
`Attachment`.`created`,
`Type`.`title`, `Type`.`icon`
FROM
`attachments` AS `Attachment`
LEFT JOIN
`types` AS `Type`
ON (`Attachment`.`type_id`=`Type`.`id`)
WHERE
`Attachment`.`post_id` IN (1, 2, 3, ..., n)
Note the LEFT JOIN types added.
So I get the corresponding type data in the second query. I know I could get the data in a loop or using a ->query() call, but I want this to be as much effective and flexible as possible.
The problem
I tried the Containable, Model Unbinding trick (and this one) but no success. I tried different combinations of the options, I believe I've even removed joins. Here's what my PostsController looks like now.
class PostsController extends AppController {
public function index() {
$this->Post->unbindModel(array('hasMany' => array('Attachment')));
$this->Post->Attachment->unbindModel(array('belongsTo' => array('Type')));
$this->Post->bindModel(array(
'hasMany' => array(
'Attachment' => array(
'className' => 'Attachment',
// when uncommented, throws the "Unknown column Post.id" SQLSTATE error
// 'conditions' => array('Post.id' => 'Attachment.post_id'),
'foreignKey' => false,
),
),
));
$this->Post->Attachment->bindModel(array(
'belongsTo' => array(
'Filetype' => array(
'className' => 'Filetype',
// 'conditions' => array('Type.id' => 'Attachment.type_id'),
'foreignKey' => false,
),
),
));
$all = $this->Post->find('all', array(
'joins' => array(
array(
'table' => 'users',
'prefix' => '',
'alias' => 'User',
'type' => 'INNER',
'conditions' => array(
'User.id = Post.user_id',
)
),
),
'contain' => array('Attachment', 'Type'),
'conditions' => array(),
'fields' => array('*'),
'order' => 'Post.created ASC'
));
var_dump($all);exit;
}
}
But it just runs an extra query per each iteration in a loop and gets all the attachments:
SELECT `Attachment`.`id`, ...
FROM `attachments` AS `Attachment`
WHERE 1 = 1
When I uncomment the condition for this association, it throws me the SQLSTATE "Column Post.id not found error" - I guess because there's no Post table joined here.
I need a hand in setting this up.
Please help! Thanks
UPDATE
I've changed the controller as follows. Please note there's no bindModel/unbindModel code, the relation is set in the models classes (is that correct in this case?).
class PostsController extends AppController {
public function index() {
$options = array(
'contain' => array(
'Post',
'Type'
),
'order' => 'Post.created DESC',
'conditions' => array(
// 'Post.title LIKE' => 'my post'
)
);
// The following throws "Fatal error: Call to a member function find() on a non-object"
// $posts = $this->Attachment->find('all', $options);
// So I had to use $this->Post->Attachment instead of $this->Attachment
$posts = $this->Post->Attachment->find('all', $options);
$this->set(compact('posts'));
}
}
This is the Attachment model:
class Attachment extends AppModel {
public $belongsTo = array(
'Type' => array(
'className' => 'Type',
'foreignKey' => 'type_id',
),
'Post' => array(
'className' => 'Post',
'foreignKey' => 'post_id',
),
);
}
The above code runs this query:
SELECT
`Attachment`.`id`, `Attachment`.`type_id`, `Attachment`.`post_id`, `Attachment`.`created`,
`Type`.`id`, `Type`.`title`,
`Post`.`id`, `Post`.`text`, `Post`.`created`
FROM
`attachments` AS `Attachment`
LEFT JOIN `types` AS `Type` ON (`Attachment`.`type_id` = `Type`.`id`)
LEFT JOIN `posts` AS `Post` ON (`Attachment`.`post_id` = `Post`.`id`)
WHERE
1 = 1
ORDER BY
`Post`.`created` ASC
Everything is about the attachments here. I mean the posts are joined to attachments, so if the post has no attachments, it's not returned. This is probably because the call is Attachment->find() so it's from the attachment's point of view. I guess it just should be:
// ...
FROM
`posts` AS `Post`
LEFT JOIN `attachments` AS `Attachment` ON (`Attachment`.`post_id` = `Post`.`id`)
LEFT JOIN `types` AS `Type` ON (`Attachment`.`type_id` = `Type`.`id`)
// ...
But it's not going to work, is it? You see there are posts, attachments and types, but they do have the different relation types. Originally, I've posted those two separate queries CakePHP runs - there must be reasons for that.
UPDATE2
I still believe that it's all about changing the second query to the Attachment model in the initial setup (please see the What I Want section). So I will get attachments types along with attachments themselves. I mean in that case LEFT JOINing the types table to attachments is not going to break any database relation logic, is it?
I just want to make sure there's no way to do that with one complex, but single find() call.
Whenever Cake sees a hasMany relationship, it will automatically create multiple queries to pull the data. While constructing those queries, it looks for relationships that can be LEFT joined to it (hasOne and belongsTo).
Since Cake can't do this for you, you will need to merge them yourself.
public function index() {
$posts = $this->Post->find('all');
// get all attachments for all found posts
$attachments = $this->Post->Attachment->find('all', array(
'contain' => array('Type'),
'conditions' => array('Post.id' => Set::extract('/Post/id', $posts)
));
// now join them to the posts array
foreach ($posts as $key => $data) {
$postId = $data['Post']['id'];
// append any data related to this post to the post's array
$posts[$key] += Set::extract("/Attachment[post_id=$postId]/..", $attachments);
}
$this->set(compact('posts'));
}
This is not the most efficient way to do it since you'll iterate through the $attachments array multiple times, but I'm sure you get the idea.
Try the finderQuery in hasMany.
Eg:
In the Post model,
public $hasMany = array(
'Attachment' => array(
'className' => 'Attachment',
'foreignKey' => 'post_id',
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '
SELECT
`Attachment`.`id`,
`Attachment`.`post_id`,
`Attachment`.`created`,
`Type`.`title`,
`Type`.`icon`
FROM
`attachments` AS `Attachment`
LEFT JOIN
`types` AS `Type`
ON (`Attachment`.`type_id`=`Type`.`id`)
WHERE
`Attachment`.`post_id` IN (1, 2, 3, ..., n)
',
'counterQuery' => ''
)
I have been fighting with this code:
function getNextActionFObyBalance($when) {
$theQuery = $this->find('first', array(
'fields' => array(
'Contract.id',
'Contract.start_balance'
),
'conditions' => array(
'AND' => array(
'Status.next_action_by' => 'frontoffice',
'Status.status_type' => 'active',
'Status.visibility' => 'frontoffice',
'OR' => array(
'Contract.next_action_on' => null,
'Contract.next_action_on <=' => $when
)
)),
'order' => 'Contract.start_balance DESC',
'recursive' => 0,
));
return $theQuery;
}
I have enabled logging on the MySQL server at this is what the server indicates that CakePHP is requesting:
SELECT `Contract`.`id`, `Contract`.`start_balance` FROM `contracts` AS `Contract` LEFT JOIN `statuses` AS `Status` ON (`Contract`.`status_id` = `Status`.`id`) LEFT JOIN `users` AS `User` ON (`Contract`.`user_id` = `User`.`id`) WHERE ((`Status`.`next_action_by` = 'frontoffice') AND (`Status`.`status_type` = 'active') AND (`Status`.`visibility` = 'frontoffice') AND (((`Contract`.`next_action_on` IS NULL) OR (`Contract`.`next_action_on` <= '2010-09-13 10:13:04')))) ORDER BY `Contract`.`start_balance` DESC LIMIT 1
if I use that in the phpmyadmin tool, I get exactly what I was expecting 1 record with two fields. BUT CakePHP just gives me an empty result set.
Can anyone enlighten me?
PS the code was working but I can figure out what changed!
The problem was with a stub to do some post processing afterFind. The problem is that I have completely forgotten to return $results;
I found the error by doing a step by step debugging down the find method in model.php. Found that the after find was called at some point and went to check my afterFind.
Took my about 4 hours for a simple error but I am learning!
Presumably this method is defined in models/contract.php?
The recursive = 0 statement looks a bit suspect to me. Are the models correctly related in their respective model files?
Have you tried loadModel in case the associations aren't working properly?
It would be useful to see the relationship definitions from the respective models.
--EDIT--
I've formatted the code from your comment here as I can't edit your OP
var $belongsTo = array(
'Status' => array(
'className' => 'Status',
'foreignKey' => 'status_id',
),
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
)
);
var $hasMany = array(
'Transaction' => array(
'className' => 'Transaction',
'foreignKey' => 'contract_id',
'dependent' => false,
)
);