I am trying to make a messages functionality similar to the facebook. Just the message thing and not facebook. A brief descriptions does like this.
1) There are a number of users ( user table) 2) One person can send a message to one or more than one person. 3) There can be multiple reply to the same message. 4) If its send to multiple people. All can reply and it is show to all of them.
Tables used
messages table
id
timestamp
sender_id
subject
message
due_date
urgent_flag
open_flag
reply_id
message_user (table)
id
timestamp
message_id
receiver_id
read_flag
The CakePHP relations are as follows :
Message Model
var $hasMany = array(
'MessageUser' => array(
'className' => 'MessageUser',
'foreignKey' => 'message_id',
)
);
var $belongsTo = array (
'User' => array (
'className' => 'User',
'foreignKey' => 'sender_id',
)
);
var $hasAndBelongsTo=array(
'Message' => array (
'className' => 'Message',
'foreignKey' => 'reply_id',
)
);
MessageUser Model
var $belongsTo = array (
'User' => array (
'className' => 'User',
'foreignKey' => 'receiver_id',
),
'Message' => array (
'className' => 'Message',
'foreignKey' => 'message_id'
)
);
Questions :
1) Is my approach correct ? Or the database schema needs to be revised. 2) If yes, How should I fetch the data for inbox ? This is a bit complex as I want to show the conversation for those messages which people has sent me.
For example, user 1 send message to user 2. User 2 adds 2 replies to the same. Then users 1's inbox should show only 1 message. and when I open it. it will show the previous msgs as well.. (this is similar to facebook)
One more problem which I see here is, How to delete messages ? Suppose user 1 deletes a message it should not show anything in his inbox. but user 2 can see the whole conversation which he had.
WRT your schema, I would only set the relationships in the direction that you need to access them. Thus, if you don't need to ever access the User given a Message, don't link messages back to users. It can create some recursion problems where your models will automatically fetch way too much data, because you have a circular loop (User hasMany Message, Message HasMany User, etc.) That's actually what the HABTM relationship is, so use that instead of hasMany (if you want it to be a bi-directional relationship).
My comment mentioned tree behavior. Simply put, it allows you to easily store heirarchical data into a relational database table. I won't repeat the manual, but check out http://book.cakephp.org/view/91/Tree and google around for examples. I'm pretty sure this is what you want to use for threaded replies.
One further word of caution -- if you're going to use this with a serious user base, run some load tests on this part of your database. I'm not sure how well TreeBehavior would work with millions of rows on a cheap server.
Related
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?
I am using CakePHP 2.4 and I need to have a model called Files to store file information for Users, Cases, and Trucks. Now a file can be associated with many other models (Users, Cases, Trucks, etc). In other words I want Users to have Files, Cases to have Files, and Trucks to have Files.
Do I need to create UserFiles model, CaseFiles model, and TruckFiles or can I have a table entry that flags in Files that it is a User record, Case record, or Truck record? I also want to do the same for Notes and Tasks, so there would either be UserNotes, CaseNotes, and TruckNotes or can I have Notes (id, foreign_id, associatedModel(would be Truck, User, or Case), etc..). So that when I create my Users, I have Files that tie to my User.id and have a condition that the associatedModel is User.
I think I will need to create the UserFiles, UserNotes, and UserTasks. But if there is a good design and better way to do this I am open to suggestions.
CakePHP already has a Class named File. See: http://api.cakephp.org/2.4/class-File.html
For this its better to use other name, for example Attach.
User hasMany Attach
Case hasMany Attach
Truck hasMany Attach
Instead of create three foreign keys on 'attaches' table (like user_id, case_id, and truck_id), in this case i prefer create a model_name and model_id fields as foreign key.
schema table attaches:
- id
- model_name
- model_id
- ...
model Attach:
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'model_id',
'conditions' => array('Attach.model_name' => 'User'),
),
'Case' => array(
'className' => 'Case',
'foreignKey' => 'model_id',
'conditions' => array('Attach.model_name' => 'Case'),
),
'Truck' => array(
'className' => 'Truck',
'foreignKey' => 'model_id',
'conditions' => array('Attach.model_name' => 'Truck'),
),
);
model User, Case, Truck:
public $hasMany = array(
'Attach' => array(
'className' => 'Attach',
'foreignKey' => 'model_id',
'dependent' => true,
'conditions' => array('Attach.model_name' => 'User'), // Change the condition on Case and Truck model
),
);
You would need to create these models
User
File
Case
Truck
Then, you define model associations as you need.
For example: User hasMany File, Case hasMany File, Truck hasMany File, File belongsTo User, Case and Truck
One way to think about it is if the relationship is one to many (hasMany), many to one (belongsTo), one to one (hasOne), or many to many (hasAndBelongsToMany). With the first three mentioned, you can store the foreign key in one of the models because at least one of the models being associated is associated with only one entry in the other table. In the many to many relationship, you need a join table to store the pairs of foreign keys representing the associations.
In this case, if you're talking about a file that can belong to many users, and users can have many files, then that is a many to many or hasAndBelongsToMany relationship, and then yes, you need a UsersFile join table. If however, a file can only belong to one user, or a user can only have one file, then you're talking about one of the other relationships, and you do not need the join table. The same can be said for files/cases and files/trucks.
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
I was hoping to create and save the associated data like what I did with belongsTo data - the associated data are created on the fly and foreign ID is also generated and saved on the fly by a single call to saveAll or saveAssocated which has transaction built-in.
But this seems not the case for data in hasMany relationship. Take User and Comment as example. Comment has user_id as the foreign key.
It seems that I cannot save User $data by using single saveAll($data) on User
Array(
'name' => 'Jack',
'email' => 'jack#abc.com',
'Comment' => array(
array(
'title' => 'I like this article.'
)
)
)
I read some docs. It seems that I need to mention the user_id as the foreign key for the Comment data to create correctly.
If that's the case, since I don't have user ID until it's created, it seems that I need to code to let SAVE happen twice.
I really think that I am missing something. There should be a CAKE way for doing this.
This is done automatically by Cake as long as you follow the conventions and format the data accordingly. For hasMany associations, the main model data, and the associated model data, need to be set on string keys on the same level, like
array
(
'User' => array(),
'Comment' => array()
)
Also note that
The saveAll function is just a wrapper around the saveMany and
saveAssociated methods. it will inspect the data and determine what
type of save it should perform. If data is formatted in a numerical
indexed array, saveMany will be called, otherwise saveAssociated is
used.
This function receives the same options as the former two, and is
generally a backwards compatible function. It is recommended using
either saveMany or saveAssociated depending on the case.
So either will do.
Long story short, the model data needs to be separated and indexed by string keys, it's essentially the same format as a find() call would return it. That way Cake will know that it needs to save associated data, and will automatically insert the foreign key for the Comment records.
array
(
'User' => array
(
'name' => 'Jack',
'email' => 'jack#abc.com'
),
'Comment' => array
(
array
(
'title' => 'I like this article.'
)
)
)
See also
Cookook > Models > Saving Your Data > Associations: Linking Models Together > hasMany
Cookook > Models > Saving Your Data > Model::saveMany()
Cookook > Models > Saving Your Data > Model::saveAssociated()
Cookook > Models > Saving Your Data > Model::saveAll()
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?