I've got the following find() function
$this->User->find('all',array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'fields' => array('User.id','UserRole.id')
));
And the following associations are defined
// UserRole.php
class UserRole extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
// User.php
class User extends AppModel {
public $hasMany = array(
'UserRole' => array(
'className' => 'UserRole',
'foreignKey' => 'user_id',
'dependent' => false,
)
);
}
And In App model recursive is set to -1
public $recursive = -1;
The database tables are the following:
CREATE TABLE IF NOT EXISTS `user_roles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`event_id` int(10) unsigned NOT NULL,
`role_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`first_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`last_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
But I get the vollowing error
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'UserRole.id' in 'field list'
SQL Query: SELECT `User`.`id`, `UserRole`.`id` FROM `atlanta`.`users` AS `User` WHERE `User`.`id` = 5
I get that cake can't find the UserRole.id, but what did I do wrong?
Either you use some behavior like Containable and your find() will look like this
$this->User->find('all',array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'contain' => array(
'UserRole' => array(
'fields'=>array('id')
)
),
'fields' => array('id')
));
OR you set recursive to 1
$this->User->find('all',array(
'conditions' => array('User.id' => $this->Auth->user('id')),
'fields' => array('User.id'), // UPDATE: unfortunately you cant specify associated models fields without behavior
'recursive' => 1
));
The thing is that when recursive is set to -1 or 0 it does not fetch associated models unless some behavior takes care of that for e.g. Containable or Linkable.
Related
I set up more than one models, which i want to associate with a master-model like this:
class CommonType extends AppModel {
public $useDbConfig = 'common';
public $hasOne = array(
'CommonTypeDescription' => array(
'className' => 'CommonTypeDescription',
'foreignKey' => 'type_description_id',
'dependent' => true
),
'CommonTypeExperience' => array(
'className' => 'CommonTypeExperience',
'foreignKey' => 'type_expirience_id',
'dependent' => true
),
'CommonTypeProperty' => array(
'className' => 'CommonTypeProperty',
'foreignKey' => 'type_property_id',
'dependent' => true
),
);
public $belongsTo = array(
'CommonTypeCategory' => array(
'className' => 'CommonTypeCategory',
'foreignKey' => 'common_type_id',
));
}
In Heidi SQL (for example) i set the foreignkeys for the tables type_experiences, type_properties and type_descriptions as above, so that foreignkeys in my code and in my tables have the same name.
But now, i don't want to name the primary key as the foreign key, the primary key of the (e.g.) type_descriptions table should named "id". This is the model of type_description:
class CommonTypeDescription extends AppModel {
public $name = 'CommonTypeDescription';
public $useDbConfig = 'common';
public $useTable = 'type_descriptions';
public $primaryKey = 'id';
}
and here is the database creation code:
CREATE TABLE `type_descriptions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`common_type_id` INT(11) NULL DEFAULT NULL,
`short_description` VARCHAR(50) NULL DEFAULT NULL,
`long_description` VARCHAR(50) NULL DEFAULT NULL,
`notes` VARCHAR(50) NULL DEFAULT NULL,
`modified` DATETIME NULL DEFAULT NULL,
`created` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `type_description_id` FOREIGN KEY (`common_type_id`) REFERENCES `common_types` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION) ...
But when i try to get the data from my model CommonType ($this->find('all')) i get an error like "Unknown column 'CommonTypeDescription.type_description_id' in 'on clause'"
First, i thought that i have to declare the primary key of the description model as "id", but it seams, that the foreign key definition overwrites the naming convention from cake of the primary key from the associated model.
Many thanks for your help & Greetings,
Guido
ps. i've changed the Model-Names of this post (if there is a type-mistake). So when i rename the primary keys of the associated tables, it all works fine, but i want them to name only to "id".
How about update your CommonType Model to this:
'CommonTypeDescription' => array(
'className' => 'CommonTypeDescription',
'foreignKey' => 'common_type_id', //or try type_description_id if there's an error
'dependent' => true
)
Then on your CommonTypeDescription model add this relationship, I think you forgot adding belongs to relationship to CommonType Model:
public $belongsTo = array(
'CommonType' => array(
'className' => 'CommonType ',
'foreignKey' => 'common_type_id',
));
I wrote a behavior, Cascadable, that merges records across 2 databases. It works sort of like CSS, where the more local style overrides the global. The behavior uses $this->setDataSource() to switch between the databases. This was working up until the 2.4.8 release when this commit was made to the Model.php file.
Here's sort of an extraction from the behavior:
$Model->setDataSource( 'alt' );
$altData = $Model->find($findType, array(
'contain' => false,
'conditions' => array(
$Model->alias.'.id' => $ids
),
'callbacks' => 'before'
));
If the callbacks option is set to false, I get Missing Database Table errors about tables that are only present in the default (parent) datasource.
I'm not sure what I'm doing wrong here, but after 2.4.8 upgrade it always uses the wrong (parent) database instead of the child. I can revert Model.php back to the 2.4.7 version to fix the issue. I realize this is difficult to replicate because it only breaks in my very specific database structure.
MySQL Table Schema example:
Parent Database Table
CREATE TABLE `products` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`price` float(8,2) DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
KEY `active` (`active`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 PACK_KEYS=0;
INSERT INTO `parent_db`.`products` (`id`, `name`, `price`, `active`) VALUES (NULL, 'Big Box of Goodies', '99.99', '0');
Child Database Table
CREATE TABLE `products` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`price` float(8,2) DEFAULT NULL,
`active` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `active` (`active`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 PACK_KEYS=0;
INSERT INTO `child_db`.`products` (`id`, `name`, `price`, `active`) VALUES (NULL, 'Small Box of Surprises', NULL, '1');
Query Results
From Parent DB
array(
(int) 0 => array(
'Product' => array(
'id' => '1149',
'name' => 'Big Box of Goodies',
'price' => '99.99',
'active' => false
)
)
)
From Child DB with working Cascadable behavior (child record is merged into parent record, ignoring null values)
array(
(int) 0 => array(
'Product' => array(
'id' => '1149',
'name' => 'Small Box of Surprises',
'price' => '99.99',
'active' => true
)
)
)
Workaround:
Setting the schemaName manually after using setDataSource fixes the issue for me.
$Model->setDataSource( 'alt' );
App::uses('ConnectionManager', 'Model');
$dataSources = ConnectionManager::enumConnectionObjects();
$Model->schemaName = $dataSources['alt']['database'];
I have the following tables: causes, users, transactions
Cause hasMany Transaction and User hasMany Transaction
Transaction belongsTo Cause and Transaction belongsTo User
Both my causes and users tables have transaction_count fields and my belongsTo has counterCache => true on both associations.
Both of my hasMany clauses have dependent => true and my foreignKeys are set up correctly in that if I delete a Cause, all of it's associated Transactions are deleted as well.
It is also working, in that when I create a Transaction, the transaction_count field in both my users and causes tables updates correctly. Here is the breakdown:
1.) Create a cause
2.) Create a transaction
RESULT: causes[ 15 ][ 'transaction_count' ] increases by 1 correctly
RESULT: users[ 1 ][ 'transaction_count' ] increases by 1 correctly
3.) Manually delete a transaction ( $transaction->delete( $id ) )
RESULT: causes[ 15 ][ 'transaction_count' ] decreases by 1 correctly
RESULT: users[ 1 ][ 'transaction_count' ] decreases by 1 correctly
4.) FAILURE:
If I create the Cause and Transaction and then delete the Cause using $this->Cause->delete( $id ), even though all of the transactions are deleted in the database (along with the Cause), the transaction_count in my users table is not updated to reflect the decrease.
I imagine this is because $this->Transaction->delete() is not getting called explicitly, but it is instead getting deleted due to its association with Cause?
Is there a workaround for this?
Tables
CREATE TABLE `causes` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`transaction_count` int(10) unsigned NOT NULL DEFAULT '0'
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `transactions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cause_id` int(10) unsigned NOT NULL DEFAULT '0',
`user_id` int(10) unsigned NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`transaction_count` int(10) unsigned NOT NULL DEFAULT '0'
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
User.php
App::uses('AppModel', 'Model');
class User extends AppModel {
public $hasMany = array(
'Transaction' => array(
'dependent' => true
)
);
}
Cause.php
App::uses('AppModel', 'Model');
class Cause extends AppModel {
public $hasMany = array(
'Transaction' => array(
'dependent' => true
)
);
}
Transaction.php
App::uses('AppModel', 'Model');
class Transaction extends AppModel {
public $belongsTo = array(
'User' => array(
'counterCache' => true
),
'Cause' => array(
'counterCache' => true
)
);
}
Controller
$this->Cause->query('TRUNCATE TABLE users');
$user['User'] = array(
'name' => 'aaa'
);
$this->Cause->Transaction->User->create($user);
$this->Cause->Transaction->User->save(null, false);
$userId = $this->Cause->Transaction->User->getLastInsertID();
$this->Cause->query('TRUNCATE TABLE causes');
$cause['Cause'] = array(
'name' => 'aaa'
);
$this->Cause->create($cause);
$this->Cause->save(null, false);
$causeId = $this->Cause->getLastInsertID();
$this->Cause->query('TRUNCATE TABLE transactions');
$transaction['Transaction'] = array(
'name' => 'aaa',
'user_id' => $userId,
'cause_id' => $causeId
);
$this->Cause->Transaction->create($transaction);
$this->Cause->Transaction->save(null, false);
$this->Cause->Transaction->create($transaction);
$this->Cause->Transaction->save(null, false);
$transaction['Transaction'] = array(
'name' => 'aaa',
'user_id' => $userId,
'cause_id' => 99
);
$this->Cause->Transaction->create($transaction);
$this->Cause->Transaction->save(null, false);
$this->Cause->delete($causeId);
$cause = $this->Cause->find('first', array(
'conditions' => array(
'Cause.id' => $causeId
)
));
$transactions = $this->Cause->Transaction->find('all', array(
'conditions' => array(
'Transaction.cause_id' => $causeId
)
));
$user = $this->Cause->Transaction->User->find('first', array(
'conditions' => array(
'User.id' => $userId
)
));
pr($cause);
pr($transactions);
pr($user);
if (is_a($this->Cause, 'Cause')) {
echo 'Cause';
}
if (is_a($this->Cause->Transaction, 'Transaction')) {
echo 'Transaction';
}
if (is_a($this->Cause->Transaction->User, 'User')) {
echo 'User';
}
Result
Array
(
)
Array
(
)
Array
(
[User] => Array
(
[id] => 1
[name] => aaa
[transaction_count] => 1
)
)
CauseTransactionUser
It works ok.
Double check for typos in models and check if cake is using your models and not the ones that are created on the fly if some model is missing.
I have 4 models, Application , Accommodation, Invoice and InvoiceItem
Invoice, Application and Accommodation all hasMany InvoiceItem
InvoiceItem belongsTo Application, Accommodation and Invoice
The purpose of all this is so that the line items of an Invoice (which come from the InvoiceItem model) can be associated with either an Accommodation or an Application via the InvoiceItem.foreign_id depending on whether InvoiceItem.class is set as Accommodation or Application
From what I understand, this is known as a polymorphic association.
Unfortunately, when I do a $this->Invoice->find('all'); with recursive set to 3 (or set to -1 with the appropriate fields specified in containable) I get this MySQL error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'InvoiceItem.class' in 'where clause'
SQL Query: SELECT `Application`.`id`, `Application`.`name`,
`Application`.`created`, `Application`.`updated` FROM `applications`
AS `Application` WHERE `Application`.`id` = 3786 AND
`InvoiceItem`.`class` = 'Application'
I don't understand why Cake would try to add InvoiceItem.class as a condition without creating a join for the InvoiceItem model.
Here are my models (note: I've cut down the number of fields in each for readability -- the Application and Accommodation models share very few similar fields in reality):
Accommodation Model
CREATE TABLE `accommodations` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
)
<?php
class Accommodation extends AppModel {
var $name = 'Accommodation';
var $hasMany = array(
'InvoiceItem' => array(
'className' => 'InvoiceItem',
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Accommodation'),
)
);
}
?>
Application Model
CREATE TABLE `applications` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
)
<?php
class Application extends AppModel {
var $name = 'Application';
var $hasMany = array(
'InvoiceItem' => array(
'className' => 'InvoiceItem',
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Application'),
)
);
}
?>
InvoiceItem model
CREATE TABLE IF NOT EXISTS `invoice_items` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`class` varchar(30) COLLATE utf8_unicode_ci NOT NULL,
`foreign_id` int(10) unsigned NOT NULL,
`name` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)
<?php
class InvoiceItem extends AppModel {
var $name = 'InvoiceItem';
var $belongsTo = array(
'Accommodation' => array(
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Accommodation')
),
'Application' => array(
'foreignKey' => 'foreign_id',
'conditions' => array('InvoiceItem.class' => 'Application')
),
'Invoice' => array(
'className' => 'Invoice',
'foreignKey' => 'invoice_id',
),
);
}
?>
Invoice model
CREATE TABLE IF NOT EXISTS `invoices` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)
<?php
class Invoice extends AppModel {
var $name = 'Invoice';
var $hasMany = array(
'InvoiceItem' => array(
'className' => 'InvoiceItem'
'foreignKey' => 'invoice_id'
)
);
}
?>
I'm using CakePHP 2.4.0
class InvoiceItem extends AppModel,and bind no belongsTo.
class InvoiceItem extends AppModel {
var $name = 'InvoiceItem';
var $belongsTo = null
}
you can assign a suitable belongsTo to InvoiceItem at Runtime depend on content
$this->InvoiceItem->bindModel(array( 'belongsTo' => array(...) ) )
I have 3 models, Page , Course and Content
Page and Course contain meta data and Content contains HTML content.
Page and Course both hasMany Content
Content belongsTo Page and Course
To avoid having page_id and course_id fields in Content (because I want this to scale to more than just 2 models) I am looking at using Polymorphic Associations. I started by using the Polymorphic Behavior in the Bakery but it is generating waaay too many SQL queries for my liking and it's also throwing an "Illegal Offset" error which I don't know how to fix (it was written in 2008 and nobody seems to have referred to it recently so perhaps the error is due to it not having been designed for Cake 2?)
Anyway, I've found that I can almost do everything I need by hardcoding the associations in the models as such:
Page Model
CREATE TABLE `pages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
)
<?php
class Page extends AppModel {
var $name = 'Page';
var $hasMany = array(
'Content' => array(
'className' => 'Content',
'foreignKey' => 'foreign_id',
'conditions' => array('Content.class' => 'Page'),
)
);
}
?>
Course Model
CREATE TABLE `courses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
)
<?php
class Course extends AppModel {
var $name = 'Course';
var $hasMany = array(
'Content' => array(
'className' => 'Content',
'foreignKey' => 'foreign_id',
'conditions' => array('Content.class' => 'Course'),
)
);
}
?>
Content model
CREATE TABLE IF NOT EXISTS `contents` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`class` varchar(30) COLLATE utf8_unicode_ci NOT NULL,
`foreign_id` int(11) unsigned NOT NULL,
`title` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`content` text COLLATE utf8_unicode_ci NOT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)
<?php
class Content extends AppModel {
var $name = 'Content';
var $belongsTo = array(
'Page' => array(
'foreignKey' => 'foreign_id',
'conditions' => array('Content.class' => 'Page')
),
'Course' => array(
'foreignKey' => 'foreign_id',
'conditions' => array('Content.class' => 'Course')
)
);
}
?>
The good thing is that $this->Content->find('first') only generates a single SQL query instead of 3 (as was the case with the Polymorphic Behavior) but the problem is that the dataset returned includes both of the belongsTo models, whereas it should only really return the one that exists. Here's how the returned data looks:
array(
'Content' => array(
'id' => '1',
'class' => 'Course',
'foreign_id' => '1',
'title' => 'something about this course',
'content' => 'The content here',
'created' => null,
'modified' => null
),
'Page' => array(
'id' => null,
'title' => null,
'slug' => null,
'created' => null,
'updated' => null
),
'Course' => array(
'id' => '1',
'title' => 'Course name',
'slug' => 'name-of-the-course',
'created' => '2012-10-11 00:00:00',
'updated' => '2012-10-11 00:00:00'
)
)
I only want it to return one of either Page or Course depending on which one is specified in Content.class
UPDATE: Combining the Page and Course models would seem like the obvious solution to this problem but the schemas I have shown above are just shown for the purpose of this question. The actual schemas are actually very different in terms of their fields and the each have a different number of associations with other models too.
UPDATE 2
Here is the query that results from running $this->Content->find('first'); :
SELECT `Content`.`id`, `Content`.`class`, `Content`.`foreign_id`, `Content`.`title`,
`Content`.`slug`, `Content`.`content`, `Content`.`created`, `Content`.`modified`,
`Page`.`id`, `Page`.`title`, `Page`.`slug`, `Page`.`created`, `Page`.`updated`,
`Course`.`id`, `Course`.`title`, `Course`.`slug`, `Course`.`created`,
`Course`.`updated` FROM `cakedb`.`contents` AS `Content`
LEFT JOIN `cakedb`.`pages` AS `Page` ON
(`Content`.`foreign_id` = `Page`.`id` AND `Content`.`class` = 'Page')
LEFT JOIN `cakedb`.`courses` AS `Course` ON (`Content`.`foreign_id` = `Course`.`id`
AND `Content`.`class` = 'Course') WHERE 1 = 1 LIMIT 1
Your query is okay. Just filter empty values after find:
public function afterFind($results, $primary = false) {
return Set::filter($results, true);
}
Take a look at this question.
You can also try to provide conditions to you find query that Page.id in not null or Course.id is not null.