Joins using paginate in cakephp - php

I have a project that i have inherited and was poorly designed and i am just trying to bring some sanity into it. Here is the issue i am having. I have a table that stores projects, contacts and another table that stores writers attached to that project(although there are still more tables). I have managed to write the below script.
$conditions = $this->paginate = array('joins' => array(
'table' => 'wp3_order_writers',
'alias' => 'OrderWriter',
'type' => 'LEFT',
'conditions' => array(
'OrderWriter.order_id=Order.id'
)
),
'conditions' => array(
array('Order.status_id' => array(5,6,7,8),
'OR' => array('Order.assigned_to_writer' => $this->_user['Contact']['id'],
'OrderWriter.writer_id' => $this->_user['Contact']['id'],
)
),
),
'fields' => array(
'Order.id', 'Order.urgent', 'Order.writer_deadline', 'Order.subject_title', 'Client.name', 'Client.email', 'Service.*', 'Order.writer_fee', 'Order.count_of_words', 'Status.*', 'Order.pay_status', 'Subject.*'
)
);
$this->paginate['limit'] = 100000;
$orders = $this->paginate($this->Order);
When i check the sql script generated it contains error. Below is what is generated
SELECT `Order`.`id`, `Order`.`urgent`, `Order`.`writer_deadline`, `Order`.`subject_title`, `Client`.`name`, `Client`.`email`, `Service`.*,`Order`.`writer_fee`, `Order`.`count_of_words`, `Status`.*, `Order`.`pay_status`, `Subject`.* FROM `wp3_orders` AS `Order` wp3_order_writers OrderWriter LEFT Array LEFT JOIN `wp3_contacts` AS `Client` ON (`Order`.`client_id` = `Client`.`id`) LEFT JOIN `wp3_keys_values` AS `Service` ON (`Order`.`service_id` = `Service`.`id` AND `Service`.`key` = 'services') LEFT JOIN `wp3_keys_values` AS `PayMethod` ON (`Order`.`pay_method_id` = `PayMethod`.`id` AND `PayMethod`.`key` = 'pay_methods') LEFT JOIN `wp3_subjects` AS `Subject` ON (`Order`.`subject_id` = `Subject`.`id`) LEFT JOIN `wp3_keys_values` AS `Status` ON (`Order`.`status_id` = `Status`.`id` AND `Status`.`key` = 'statuses') LEFT JOIN `wp3_clients_sessions` AS `ClientsSession` ON (`ClientsSession`.`current_order_id` = `Order`.`id`) WHERE ((`Order`.`status_id` IN (5, 6, 7, 8)) AND (((`Order`.`assigned_to_writer` = 1087) OR (`OrderWriter`.`writer_id` = '1087')))) AND `Order`.`completed` = 1 LIMIT 100000
With this error "Warning (512): SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'wp3_order_writers OrderWriter LEFT Array LEFT JOIN wp3_contacts AS Client ON' at line 1 [CORE\cake\libs\model\datasources\dbo_source.php, line 684]"
Your help will be greatly appreciated

I would make it with Containable (assuming that you have models like Order, OrderWriter, Writer and defined relations inside)
Then your query will look like:
$this->paginate['Order'] = array('contain' => array('Writer'));
$this->paginate['conditions'] = array(...);
$this->paginate['fields'] = array(...);
$orders = $this->paginate($this->Order);
I am not sure if the Order section should be like above or:
$this->paginate['Order'] = array('contain' => array('OrderWriter'=>array('Writer')));
Also I would advise you don't use 100000 limit, since it's probably a way to skip the pagination, but then just use find() instead of paginate.
Although avoiding pagination will slow down the request, since it will make a query for each row of Orders, so having default limit (20) or some reasonable number should speed-up the site/app.

Related

Checking if selected rows are favourite by user in cakephp

I have a Venue table with 20+ columns.
I also have a favorite_venues table with columns
id | userId | venueId
Here is my code to get venues
$this->Venue->virtualFields = array(
'bookedCount' => "SELECT count(*) FROM bookings WHERE venueId = Venue.id"
);
$result = $this->Venue->find('all', array('conditions'=>$conditions,
'order' => array('Venue.bookedCount DESC'),
'limit' => 20,
'offset' => $offset * 20
));
i want to add a condition if i send userId, it should check every venue if its added to favourite list or not and set
$venue['isFavorite'] = yes/no
I dont want a for loop and check every venue i get. is there any way i can incorporate in same Mysql query in cakephp. I am not sure how to put yes/no as virtual field
You can LEFT join in your favorite_venues table, and then for example use a CASE statement in a virtual field that checks whether there is a linked row.
Here's an untested example to illustrate what I mean. I don't know how the user ID is involved, so you got to figure that on your own.
$this->Venue->virtualFields = array(
'bookedCount' => "SELECT count(*) FROM bookings WHERE venueId = Venue.id",
'isFavorite' => 'CASE WHEN FavoriteVenues.id IS NOT NULL THEN "yes" ELSE "no" END'
);
$result = $this->Venue->find('all', array(
'conditions' => $conditions,
'order' => array('Venue.bookedCount DESC'),
'limit' => 20,
'offset' => $offset * 20,
'joins' => array(
array(
'table' => 'favorite_venues',
'alias' => 'FavoriteVenues',
'type' => 'LEFT',
'conditions' => array(
'FavoriteVenues.venueId = Venue.id',
)
)
),
// don't forget to group to prevent duplicate results
'group' => 'Venue.id'
));
See also
Cookbook > Models > Virtual Fields > Virtual fields set in controller with JOINS
Cookbook > Models > Associations: Linking Models Together > Joining Tables
MySQL 5.7 Reference Manual / SQL Statement Syntax / ... / CASE Syntax

CakePHP pagination - order by virtual field

Contributors have songs and songs have contributors. I want to be able to sort by the number of songs that a contributor has.
In my Controller:
public $paginate = array(
'fields' => array(
'Contributor.id',
'Contributor.name',
'COUNT(DISTINCT ContributorsSong.song_id) AS Contributor__TotalSongs',
),
'joins' => array(
array(
'alias' => 'ContributorsSong',
'table' => 'contributors_songs',
'type' => 'LEFT',
'conditions' => 'ContributorsSong.contributor_id = Contributor.id'
),
array(
'alias' => 'Song',
'table' => 'songs',
'type' => 'LEFT',
'conditions' => 'ContributorsSong.song_id = Song.id'
)
),
'group' => array('ContributorsSong.contributor_id')
);
And in my index method.
$this->Contributor->recursive = 0;
$this->Paginator->settings = $this->paginate;
$this->Contributor->virtualFields['TotalSongs'] = 0;
$items = $this->paginate();
echo '<pre>';print_r($items);echo '</pre>';
I'm trying to sort by the number of songs by using a virtual field, so when I go to
localhost/site/contributors/index/sort:TotalSongs/
I get this error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column '0' in 'order clause'
SQL Query:
SELECT Contributor.id,
Contributor.name,
COUNT(DISTINCT ContributorsSong.song_id) AS Contributor__TotalSongs
FROM db_songs2.contributors AS Contributor LEFT JOIN
db_songs2.contributors_songs AS ContributorsSong ON
(ContributorsSong.contributor_id = Contributor.id) LEFT JOIN
db_songs2.songs AS Song ON (ContributorsSong.song_id =
Song.id) WHERE 1 = 1 GROUP BY ContributorsSong.contributor_id
ORDER BY (0) desc LIMIT 18
I thought that TotalSongs would get turned into Contributor__TotalSongs in the query but it gets turned into 0. What is going on here? Thanks.
I replaced:
$this->Contributor->virtualFields['TotalSongs'] = 0;
with:
$this->Contributor->virtualFields['TotalSongs'] = 'COUNT(DISTINCT ContributorsSong.song_id)';
And deleted:
'COUNT(DISTINCT ContributorsSong.song_id) AS Contributor__TotalSongs',
And it works for ordering now, but I can't use it in the conditions. I think this is part of the Limitations of virtualFields? I'm trying to select only tables where TotalSongs > 0, but I think I can get this another way, by changing the join from LEFT to INNER.

How to get data from different associated tables using containable in Cakephp?

I have 3 tables: orders, discounts and products in the way that product has many discounts (discount times). Discount has many orders. in other words, it looks like: Product > Discount > Order.
I want to get data from 3 tables as the raw mySQL query below:
SELECT discounts.product_id, products.product_name,
sum(products.product_price - discounts.product_discount) as total_Amount,
count(orders.order_id) as total_Number
FROM products
inner join discounts on products.product_id = discounts.product_id
inner join orders on discounts.discount_id = orders.discount_id
group by discounts.product_id,products.product_name
This is what I did:
$this->Order->virtualFields['benefit']='SUM(Product.product_price - Discount.product_discount)';
$this->Order->virtualFields['number']='COUNT(Order.order_id)';
$products = $this->Order->find('all',array('contain'=>array('Discount'=>array('Product'))),
array( 'limit'=>20,
'fields'=>array('benefit','number'),
'group'=>array('Discount.product_id','Product.product_name')));
Debugger::dump($products);
$this->set('products',$products);
But I got an error:
Database Error
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Product.product_price' in 'field list'
SQL Query: SELECT `Order`.`order_id`, `Order`.`user_id`
`Order`.`order_date`, `Order`.`payment`, `Order`.`discount_id`, `Order`.`total`,
(SUM(`Product`.`product_price` - `Discount`.`product_discount`)) AS `Order__benefit`,
(COUNT(`Order`.`order_id`)) AS `Order__number`, `Discount`.`discount_id`,
`Discount`.`product_id`, `Discount`.`product_discount`,
`Discount`.`start_time`, `Discount`.`end_time`
FROM `project`.`orders` AS `Order`
LEFT JOIN `project`.`discounts` AS `Discount`
ON (`Order`.`discount_id` = `Discount`.`discount_id`) WHERE 1 = 1
It seems that containable didnt work as it didnt not contain products table in the query.
EDIT: according to Dave's suggestion, I used JOIN:
$this->Order->recursive=-1;
$this->Order->virtualFields['benefit']='SUM(Product.product_price - Discount.product_discount)';
$this->Order->virtualFields['number']='COUNT(Order.order_id)';
$option['joins'] = array(
array('table'=>'discounts',
'alias'=>'Discount',
'type'=>'INNER',
'conditions'=>array(
'Order.discount_id = Discount.discount_id',
)
),
array('table'=>'products',
'alias'=>'Product',
'type'=>'INNER',
'conditions'=>array(
'Discount.product_id = Product.product_id'
)
)
);
$products = $this->Order->find('all',$option,
array( 'limit'=>20,
'fields'=>array('Discount.product_id','Product.product_name'),
'group'=>array('Discount.product_id','Product.product_name')));
Debugger::dump($products);
$this->set('products',$products);
However, what $products contains is only:
array(
(int) 0 => array(
'Order' => array(
'order_id' => '23567636',
'user_id' => '1',
'order_date' => '2013-11-16 16:03:00',
'payment' => 'mc',
'discount_id' => '2',
'total' => '599',
'benefit' => '7212',
'number' => '19'
)
)
)
but, what I want is:
How can I fix that? thanks in advance.
Containable is not the same as JOIN.
Containable does not join the queries into a single query, but for the most part creates completely separate queries, then combines the results for your viewing pleasure.
So - per your error, in the query that's being run on the orders table, there IS no Product.product_price field because those fields are available only in a completely separate query.
Try using JOINs instead.

CakePHP: adding DISTINCT to find causes an associated to get omitted

We build a fairly complex set of $conditions, then we search using Paginator:
$this->Paginator->settings = array( 'conditions'=>$conditions,
'joins'=>$joins,
'limit'=>25,
//'fields' => array('DISTINCT (User.id)')
);
$resume_display_array = $this->Paginator->paginate('User');
You'll see the 'fields' is commented out. When we uncomment that field, it causes associated model Resume to get dropped from the results array (without it, each result returned includes a [Resume] key.
Why is this happening?
Here's the SQL dump WITH the DISTINCT option in use:
SELECT DISTINCT (`User`.`id`), `User`.`id`
FROM `sw`.`users` AS `User`
LEFT JOIN `sw`.`taggings_users` AS `TaggingUser`
ON (`User`.`id`= `TaggingUser`.`user_id`)
LEFT JOIN `sw`.`taggings` AS `Tagging`
ON (`TaggingUser`.`tagging_id`= `Tagging`.`id`)
LEFT JOIN `sw`.`resumes` AS `Resume`
ON (`Resume`.`user_id` = `User`.`id`)
WHERE `User`.`first_name` LIKE '%jordan%' AND NOT (`Tagging`.`tag_name` = ('done'))
LIMIT 25
Here's the SQL dump WITHOUT the DISTINCT option:
SELECT `User`.`id`, `User`.`username`, `User`.`password`, `User`.`role`, `User`.`created`, `User`.`modified`, `User`.`email`, `User`.`wordpress_user_id`, `User`.`first_name`, `User`.`last_name`, `User`.`group_id`, `Resume`.`id`, `Resume`.`user_id`, `Resume`.`wordpress_resume_id`, `Resume`.`wordpress_user_id`, `Resume`.`has_file`, `Resume`.`is_stamped`, `Resume`.`is_active`, `Resume`.`file_extension`, `Resume`.`created`, `Resume`.`modified`, `Resume`.`is_deleted`
FROM `sw`.`users` AS `User`
LEFT JOIN `sw`.`taggings_users` AS `TaggingUser`
ON (`User`.`id`= `TaggingUser`.`user_id`)
LEFT JOIN `sw`.`taggings` AS `Tagging`
ON (`TaggingUser`.`tagging_id`= `Tagging`.`id`)
LEFT JOIN `sw`.`resumes` AS `Resume`
ON (`Resume`.`user_id` = `User`.`id`)
WHERE `User`.`first_name` LIKE '%jordan%' AND NOT (`Tagging`.`tag_name` = ('done'))
LIMIT 25
Update
Here is a var_dump of $joins:
array(
(int) 0 => array(
'table' => 'taggings_users',
'type' => 'LEFT',
'alias' => 'TaggingUser',
'conditions' => array(
(int) 0 => 'User.id= TaggingUser.user_id'
)
),
(int) 1 => array(
'table' => 'taggings',
'type' => 'LEFT',
'alias' => 'Tagging',
'conditions' => array(
(int) 0 => 'TaggingUser.tagging_id= Tagging.id'
)
)
)
If you specify the fields option, you need to specify all fields that you want to retrieve.
By setting it ONLY to User.id, you're telling it that that is the ONLY field you want returned.
You could also try something like below as a catchall (not sure if that works, but if not, you get the idea - you're limiting your own results):
'fields' => array('*', 'DISTINCT (User.id)')

CakePHP not joining the correct tables in find query

OK, another cake question from me.
I have rather a complex table structure with quite a few joins. Here's the jist of it:
So, in summary:
RiskCategory $hasMany Client [Client $belongsTo RiskCategory]
Client $hasMany Account [Account $belongsTo Client]
Account $hasMany Holding [Holding $belongsTo Account]
In my RiskCategory model I want to run a query that basically returns the sum of the holdings in each riskCategory id, grouped by date. There's a few other conditions but here is what I've put together:
$findParams = array(
'recursive' => 4,
'fields' => array(
'Holding.holding_date',
'SUM(Holding.value) AS risk_category_value'
),
'group' => array('Holding.holding_date'),
'order' => 'Holding.holding_date ASC'
);
$findParams['conditions'] = array(
'Client.active' => true,
'Client.model' => true,
'Client.currency' => 'GBP',
'OR' => array(
'Holding.holding_date' => $this->end_date,
array(
'Holding.holding_date = LAST_DAY(Holding.holding_date)',
'MONTH(Holding.holding_date)' => array(3,6,9,12)
)
)
);
$valuations = $this->Client->Account->Holding->find( 'all', $findParams );
When I run the above cake is giving me an error saying various fields are not present in the query. The raw query created is as follows:
SELECT `Holding`.`holding_date`,
Sum(`Holding`.`value`) AS risk_category_value
FROM `ips_client_db`.`holdings` AS `Holding`
LEFT JOIN `ips_client_db`.`accounts` AS `Account`
ON ( `Holding`.`account_id` = `Account`.`id` )
LEFT JOIN `ips_client_db`.`sedols` AS `Sedol`
ON ( `Holding`.`sedols_id` = `Sedol`.`id` )
WHERE `client`.`active` = '1'
AND `client`.`model` = '1'
AND `client`.`currency` = 'GBP'
AND ( ( `Holding`.`holding_date` = '2013-10-01' )
OR (( ( `Holding`.`holding_date` = Last_day(
`Holding`.`holding_date`) )
AND ( Month(`Holding`.`holding_date`) IN ( 3, 6, 9, 12 ) ) )
) )
GROUP BY `Holding`.`holding_date`
ORDER BY `Holding`.`holding_date` ASC
It looks as though cake is not doing all the joins. It is only joining Account to Holding and then Holding to Sedol (which is another joined table in the database but is not needed for this query so I've omitted it from the diagram)
Why are the joins not being made properly and how to acheive this? I'd like to avoid writing a raw statement if possible.
EDIT: The joins should be as follows:
...
FROM risk_categories
LEFT JOIN ((clients
LEFT JOIN accounts
ON clients.id = accounts.client_id)
LEFT JOIN holdings
ON accounts.id = holdings.account_id)
ON risk_categories.id = clients.risk_category_id
...
CakePHP does not perform deep joins for associations that are more than 1 level deep. That is why you're getting an error for references to the Client table in the conditions.
For these types of problems it's easier to use the Containable behavior. I usually add this behavior to my AppModel as the default handler for associations.
Containable allows you to define the associations (only those already defined) with their fields and conditions as part of the find operation. This is done by adding a new contain key in the find parameters. Below I've taking a guess at what you might need.
$findParams = array(
'fields' => array(
'Holding.holding_date',
'SUM(Holding.value) AS risk_category_value'
),
'conditions'=>array(
'OR' => array(
'Holding.holding_date' => $this->end_date,
array(
'Holding.holding_date = LAST_DAY(Holding.holding_date)',
'MONTH(Holding.holding_date)' => array(3,6,9,12)
)
)
),
'group' => array('Holding.holding_date'),
'order' => 'Holding.holding_date ASC',
'contain'=>array(
'Account'=>array(
'Client'=>array(
'RiskCategory',
'conditions'=>array(
'Client.active' => true,
'Client.model' => true,
'Client.currency' => 'GBP',
)
)
)
)
);
$valuations = $this->Client->Account->Holding->find( 'all', $findParams );

Categories