cakephp habtm join issue - cakephp

I have table structure like below:
events (boxing, sparring etc)
competitors (users who are participating into diff events)
events_competitors (every competitor's selected events will go here)
Now what i want is, as per my match schedules i want to find competitors from schedules tables, where each competitors will be matching with events they have selected at registration time.
I'm using a find query something like this:
$matchdivisions = $this->Competitor->find("all" , array(
'conditions' => array('Competitor.status' => 1,
'Competitor.payment_completed' => 1,
'Competitor.weightgroup_id' => $current_matchsc['Matchschedule']['weightgroup_id'],
'Competitor.rank_id' => $current_matchsc['Matchschedule']['rank_id'],
'Competitor.degree_id' => $current_matchsc['Matchschedule']['degree_id'],
'Competitor.gender' => $current_matchsc['Matchschedule']['gender'],
),
'joins' => array(
array('table' => 'event_competitors',
'alias' => 'EventCompetitor',
'type' => 'left',
'conditions'=> array('EventCompetitor.event_id = '.$current_matchsc['Event']['id']),
)
)
)
);
Here, I have used joins because I am not able to find relations from EventCompetitor matching with Competitors and Events.
Problem: The single matching record comes 10 times, because of join while it should be single time only.
Earliest reply would be appreciated.
Thanks in advance!
Now a next level once this worked for me:
I have two levels of conditions check, in EventsCompetitors I want to see, if they want to have a fight with black belt or if they are minor and they want to have fight with adults so in this case, I want to see these both flags and on that basis, again my conditions will be changed and a new results will be displayed for my additional displays.
Kindly let me know, if sometime anyone have idea on this kind of stuffs with CakePHP.
Again a lot thanks to stackoverflow for their excellent services !

If you are only interested in a single record and are always sure that the others are duplicates then you can use find('first', ...) instead.
If you are interested in multiple different records but you are getting multiples of each of those records you could add in a 'group' => array('') to aggregate the competitors on a specific field to only return them once ?

Related

CakePHP 2 Models hasMany as hasOne

I am trying to do this as MVC / CakePHP 2 as possible so if my approach is the incorrect, I would love to know (still learning). I feel like what I am doing should happen in the model and less so in the controller (to follow the fat model skinny controller principals).
I have a hasMany relationship between two tables:
trainings hasMany days.
If I want all the days in a training, this setup works as expected.
But I want (in every instance of training) the first day in a training. My thought process was to setup a hasOne relationship in the Training model as follows:
public $hasOne = array(
...
'FirstDay' => array(
'className' => 'Day',
'foreignKey' => 'training_id',
'fields' => 'FirstDay.training_date',
'order' => array('FirstDay.training_date ASC'),
)
);
In essence training hasOne days as FirstDay.
I assumed that with this setup, if I call a Training object I will get the associated FirstDay.
Instead I get multiple entries for Training -- one for each instance of days for a given training. The SQL that gets output is as follows:
SELECT `Training`.`id`, `Training`.`course_id`, `Course`.`name`, ... `FirstDay`.`training_date`
FROM `tst`.`trainings` AS `Training`
LEFT JOIN `tst`.`courses` AS `Course` ON (`Training`.`course_id` = `Course`.`id`)
...
shortened for your benefit
...
LEFT JOIN `tst`.`days` AS `FirstDay` ON (`FirstDay`.`training_id` = `Training`.`id`)
WHERE 1 = 1 ORDER BY `FirstDay`.`training_date` ASC LIMIT 20
I was assuming that the hasOne would put a limit 1 instead of 20 in the above clause. Since it did not, I tried adding a 'limit' => 1 but that didn't work and the documentation does not mention that as an option in a hasOne relationship. I also do not understand why WHERE 1 = 1 is there but I figure it does not matter since it is a true statement that does not limit anything -- just seems like unnecessary lifting.
The relationship type hasOne is implemented with a LEFT JOIN, and therefore can't support LIMIT as an option, as it would affect the whole query (limiting not only Day but Training too).
There are several approaches to your problem. The most simple is to define your association as hasMany, but setting 'limit'=>1.
public $hasMany = array(
'FirstDay' => array(
'className' => 'Day',
'foreignKey' => 'training_id',
'fields' => 'FirstDay.training_date',
'order' => array('FirstDay.training_date ASC'),
'limit' => 1
)
);
Optionally, you then get rid of the extra numerical index [0] by using Hash::map() after the find():
$this->Training->contain('FirstDay');
$trainings=$this->Training->find('all');
$trainings = Hash::map($trainings, "{n}", function($arr){
$arr['FirstDay']=$arr['FirstDay'][0];
return $arr;
});
For other possible options, see:
Last x blog entries - but only once per user
Method for defining simultaneous has-many and has-one associations between two models in CakePHP?

how to get books from one category excluding other category (through cross table)

For now I am using following code to get books from certain category:
$options['conditions']['Category.id'] = $category_id;
$options['joins'] = array(
array(
'table' => 'books_categories',
'alias' => 'BookCategory',
'type' => 'inner',
'conditions' => array('Book.id = BookCategory.id_book'),
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions' => array('BookCategory.kat = Category.id'),
),
);
$this->Book->find('all',$options);
This just finds all books from given category_id.
So there are 3 tables: categories,books and books_categories. books_categories has two fileds: book_id and category_id so basicly its just HABTM relation. The problem is that one book may belong to many categories and I want for example find all books from category 5 but excluding books from categories 5,6 and 7. How I can do this?
edit -------------------------------------------------------------------------------
Ok so I figured out how it should look in pure SQL - the conditions should be like this:
where
category_id = <given category>
and books.book_id not in
(
select book_id from book_categories
where category_id in (<given set of cat>)
)
order by books.inserted
this will get all books from one category but excluding books from a set of other categories.
Now I want to force Cake to generate similar SQL query.
I tried so far:
$options['conditions']['Category.id'] = $category_id;
$options['conditions']['AND'][] = 'Book.id NOT IN (SELECT id_book FROM book_categories WHERE kat IN (133,134))';
$options['order'] = array('Book.inserted' => 'desc');
$options['joins'] = array(
array(
'table' => 'book_categories',
'alias' => 'BookCategory',
'type' => 'inner',
'conditions' => array('Book.id = BookCategory.id_book'),
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions' => array('BookCategory.kat = Category.id'),
),
);
This generates this query (sory - table names are little bit different):
SELECT `Book`.`id`, `Book`.`OK`, `Book`.`price`, `Book`.`link`, `Book`.`title`,
`Book`.`author`, `Book`.`img`, `Book`.`inserted`, `Book`.`checked`, `Book`.`big_img`, `Book`.`lang`, `Book`.`asin`, `Book`.`description`, `Book`.`last_update`, `Book`.`review`, `Book`.`changed`
FROM `amazon`.`linki` AS `Book`
inner JOIN `amazon`.`cross_kategorie_full` AS `BookCategory` ON (`Book`.`id` = `BookCategory`.`id_book`)
inner JOIN `amazon`.`kategorie` AS `Category` ON (`BookCategory`.`kat` = `Category`.`id`)
WHERE `Category`.`id` = 4
AND `Book`.`OK` = 'OK'
AND ((`Book`.`big_img` NOT LIKE '%no-image%')
AND (`Book`.`id` NOT IN (SELECT id_book FROM cross_kategorie_full WHERE kat IN (133,134))))
ORDER BY `Book`.`inserted` desc LIMIT 20
But there is error: Maximum execution time of 30 seconds exceeded - so There is something that doesnt end (loop?) in this sql statement...
Update relative to updated question
For the sql to yield correct results (in an acceptable time) you'll need to join with Categories again giving it another alias. Since this leads to another question, I suggest you post it tagged with mysql and query-optimization.
End update
As it is, a HABTM relationship is a bit devious (since it really isn't a HABTM). If you have only one row per book-category match in books_categories you can't know to what other categories a certain book belongs to, so you can't really tell which ones you really want (i.e. don't belong in those other categories). It's CakePHP's data layer and models that solve this problem for you behind the scenes :)
The only solution I see is to use Set::extract to further query the results that you get and filter out Books that belong to Categories that you didn't want to include. (something like:
// returns all books not belonging to categories 3,4
$new_result = Set::extract('/Books/Category[id!=3][!=4]', $results);
On a side note, I find it very useful in cases like this, to query the DB and visualize the complexity of the SQL query that gets you the required results. Also, you should activate the CakePHP debug toolbar to see the SQL queries that are sent to the DB so you have a better idea of what's going on behind the scenes.
The CakePHP book, advises the following at the end of "Associations: Linking models together" Section (emphasis mine).
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.
I think there is a typo in there. You want to get all books from category 5 but not from category 5, 6 and 7?
Nevermind. Cake convensions state out that there should be always a primary key within the HABTM table, so you may add a "id" column. With this the next steps are much easier or should I rather say: "They are getting possible".
What you do next is, you create an association model called "BooksCategory".
Use the 'with' index explained here to link your Book and Category models with each other over that new model. Don't forget to use plugin syntax (PluginName.ModelName) in case the linking model belongs to a plugin.
You are now putting two belongsTo associations in the linking model for each of the models you are linking. This makes sure to fetch those while finding.
Next thing you do is:
$this->Book->BooksCategory->find(
'all',
array(
'conditions' => array(
'BooksCategory.category_id' => 5
)
)
);
Finished ;) No you get only the books from category 5.
Greetings
func0der

CakePHP Recursive Find Method

In my CakePHP app, I have the following model association:
Offer -> Order -> Coupon
Then, at OffersController.php, i need to write a find method to output
the number of coupons sold on each offer, assuming that some orders can have multiple coupons.
The problem is that I'm in the OffersController, so when I try to use find('count'), I just get the number of offer entries. I want to count the number of coupon entries.
I tried to do something like $this->Offer->Order->Coupon->find('count'), but it does not work.
I tried to use the ContainableHelper as well, but got into the same problem of counting the wrong entries.
How can I proceed?
You need the resulting SQL query to do some magic with COUNT and GROUP BY. Try to add a field like this (untested):
$this->Offer->Order->Coupon->find(
'all',
array(
// Generate a field that counts the coupons per Order
'fields' => array(
'COUNT(Coupon.id) AS coupon_count'
),
// Only look at Orders for the given Offer
'conditions' => array(
'Order.offer_id' => $id
),
// This is important: group by Order.
'group' => ('Coupon.order_id')
)
);
If your Order -> Coupon have a simple hasMany relationship, a more convenient approach would be to keep track of the existing Coupon count in every Order record using an extra coupon_count field and the counterCache function. CakePHP can automatically update that counter field for you.

CakePHP: Unsure how to handle a semi difficult relationship

I'm working on my first CakePHP app, an order/invoice system. The order-part are coming along nicely, but for the invoices I kinda need some help.
The database structure I'm using; A delivery note consists of multiple products, which in turn exist of multiple re-usable elements (like a number of trays and the product itself). Now because some customer order larger quantities, they get lower rates, which are time-based (from week 1 to 9 €1 for element y, and from week 10 to 8 €1.20 for the same element). Of course some customers just have to use the daily prices, which will be stored the same way, just witha nulled customer_id.
Now my problem; I have absolutely no idea how I should tackle the invoice view, or more specifically; what the best way is to get the data, or if I should just go and practice my SQL-writing skills.
I'm not seeing any major problems with your schema. The Containable behavior should make things easy here. Add it to your Invoice model:
var $actsAs = array('Containable');
Make your life easier by adding a DefaultPrice relationship for each element. In your Element model:
var $hasOne = array(
'DefaultPrice' => array(
'className' => 'Price',
'foreignKey' => 'element_id',
'conditions' => array('DefaultPrice.customer_id IS NULL')
)
);
Now to build your invoice, set up and pass a contain parameter to your find operation. (I assume you want to show a breakdown of element costs, right?)
$contain = array(
'DeliveryNote' => array(
'Product' => array(
'Element' => array(
'DefaultPrice',
'Price' => array(
'conditions' => array('Price.customer_id' => $customer_id)
)
)
)
)
);
$this->Invoice->find('first', array('conditions' => ..., 'contain' => $contain));
This should result in each Element record including a DefaultPrice, and if the customer is receiving special pricing, the Price record will also be included.
Note: You may want to consider including a default_price field in the Element field, and avoid having to do the additional join above. Every Element is going to have a default price, right?

Manipulating Order of JOINS in CakePHP

I have the following problem with CakePHP:
In my model, Deposit belongsTo Account, and Account belongsTo Customer.
When querying Deposits, I get the Account information, but not the Customer's, by default.
If I set Deposit->recursive to 2, I get the Customer information (and a whole lot more), but CakePHP esentially throws one SELECT per each deposit, which is quite bad in this case.
So, I did this:
'joins' => array(
array('table'=>'customers', 'alias'=>'Customer', 'type'=>'left', 'foreignKey' => false, 'conditions'=>array('Account.customer_id = Customer.id'))
)
which almost works...
What I get from that is esentially:
SELECT (...) FROM Deposits LEFT JOIN Customers LEFT JOIN Accounts
instead of
SELECT (...) FROM Deposits LEFT JOIN Accounts LEFT JOIN Customers
which of course doesn't work.
Is there anyway to specify that "my custom joins" should go after the "regular model joins"?
Or do I have to manually unbind Deposit from Account, and specify both joins manually?
Thanks!
Daniel
You need to unbind models that you want to be at the top and include the appropriate arrays in 'joins' array. See this question for details
You can use the Containable behavior to select just what you want, so from your Deposits controller:
$this->Deposit->find('all', array(
// conditions
'contain' => array('Account' => array('Customer'))
));
Just be sure to add the actsAs variable to the class. Here's more info. http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
I realise this is an old question but it is still relevant...
Cake does look through existing joins before adding in new joins to satisfy associations. After some digging in dbo_source.php (cake 1.3, also applies to 2.x) I have that by specifying your "joins" exactly how cake builds them you can effectively override the order of the joins.
So, in your case here:
'joins' => array(
array(
'table' => '`accounts`',
'alias' => 'Account',
'type' => 'Left',
'conditions' => '`Deposit`.`account_id` = `Account`.`id`'
),
array(
'table' => '`customers`',
'alias' => 'Customer',
'type' => 'Left',
'conditions' => '`Account`.`customer_id` = `Customer`.`id`'
)
)
Cake using in_array() to do this so note the `` around table and field names, capitalisation of LEFT and not using an array for the conditions as these are required to make the comparison work.
I too had this situation and I was almost stuck then. After doing some search I found that there are 2 ways to do it.
Edit the core DboSource.php file to change the joins order. (How to change the sequence of 'joins' in CakePHP?)
Or we need to explicitly specify our association in $joins array. Then use recursive -1 in query.
I hope there are the possible options now. If you have any other interesting way please post here!

Resources