Sometimes the right place to store application data is in a join table. For example, movie viewers either like or don’t like the movies they watched.

CREATE TABLE viewer ( id INT NOT NULL PRIMARY KEY, name VARCHAR(45)) CREATE TABLE movie ( id INT NOT NULL PRIMARY KEY, title VARCHAR(45)) CREATE TABLE viewer_watched_movie ( viewer_id INT NOT NULL, movie_id INT NOT NULL, liked TINYINT(1), PRIMARY KEY (viewer_id, movie_id), CONSTRAINT fk_viewer_watched FOREIGN KEY (movie_id) REFERENCES movie (id) CONSTRAINT fk_movie_watched_by FOREIGN KEY (viewer_id) REFERENCES viewer (id))
So I need a Viewer model class and a Movie model class. Movie and Viewer are related n:m, i.e. many-to-many.
Property liked goes in the join table. This is correct relational DB design for data that belong to the relationship between the related entities—liked belongs to neither Movie nor Viewer. But how to access the liked attribute in a sensible way using relational Active Record?
From the context of a Viewer I can get the Movies he or she watched with this relation:
class Viewer extends CActiveRecord { ... public function relations() { return array( 'movies' => array(self::MANY_MANY, 'Movie', 'viewer_watched_movie(viewer_id, movie_id)'), ...
Using it like this:
$viewer = Viewer::model()->findByPk($id); foreach ($viewer->movies as $movie) echo $viewer . ' watched ' . $movie->title;
[This will perform poorly because it lazily loads Movies one at a time in the loop. I can pep it up thus: Viewer::model()->with('movies')->findByPk($id)]
How can I add the liked property to that echo in the loop? For example, is there a way to write relations and/or getters so I can write?:
$viewer = Viewer::model()->findByPk($id); foreach ($viewer->movies as $movie) echo $viewer . ($movie->liked ? ' liked ' : ' didn’t like ') . $movie->title;
If there is a way (in Yii 1.1.8, without extending or adding behaviors to CActiveRecord) then I haven't found it.
This trick is hard to figure out from the documentation, hence this Wiki article. It uses the through relation option introduced in Yii 1.1.7. With through I can relate one model to another via an intermediary, e.g. a join table.
class Viewer extends CActiveRecord { ... public function relations() { return array( 'watched' => array(self::HAS_MANY, 'ViewerWatchedMovie', 'viewer_id'), 'movies' => array(self::HAS_MANY, 'Movie', 'movie_id', 'through' => 'watched'), ...
So now I need a model for the join table called ViewerWatchedMovie. There is no longer an explicit MANY_MANY relation—chaining two HAS_MANY achieves n:m-ness.
Now I can write:
$viewer = Viewer::model()->findByPk($id); foreach ($viewer->watched as $i => $watched) echo $viewer . ($watched->liked ? ' liked ' : ' didn’t like ') . $viewer->movies[$i]->title;
This is an improvement. It's not pretty (typical relational AR, imnsho) but it works.
To make it eager: Viewer::model()->with('watched', 'movies')->findByPk($id).
I have it on good authority that the coherence of indexes of the two arrays $viewer->watched and $viewer->movies is assured. But this fact (feature?) is not documented so it makes me nervous. That's why I prefer a variation.
Add a relation to the ViewerWatchedMovie model:
class ViewerWatchedMovie extends CActiveRecord { ... public function relations() { return array( 'movie' => array(self::HAS_ONE, 'Movie', 'movie_id'), ...
And that allows:
$viewer = Viewer::model()->findByPk($id); foreach ($viewer->watched as $watched) echo $viewer . ($watched->liked ? ' liked ' : ' didn’t like ') . $watched->movie->title;
which gets rid of that scary indexing trick.
Eager version: Viewer::model()->with('watched.movie')->findByPk($id). You can add 'movies' to the with() if needed.
That's the best I can manage. I didn't get to $movie[$i]->liked. Clever magic getters allow this but I don't think it's worth it.
Total 7 comments
to get a extra column to mm table like sorting. Car has many files. And Files of Car has sorting.
Model Cars
Model Files
Model FilesHasReference
Table Structure
i can do then in one some bigger query
like SELECT * FROM
filesfilesINNER JOINfiles_has_referencefiles_filesON (files_files.id_foreign= MY_CARS_ID) AND (files.id=files_files.id_locale) LEFT OUTER JOINfiles_has_referencefiles_has_referenceON (files_has_reference.id_locale=files.id)hope it helps someone
I've followed your guide and can now successfully access the 'liked' property (sticking to the example). But the "movie" relation from "viewer_watched_movie" does not work in my case. I think that is because the primary key is in fact a composite key which is not beeing used when finding the correct related entry in the "movie" table. In my case EVERY instance of "viewer_watched_movie" has primaryKey "1" which is used to find related records. Therefore the movie that i get trough this relation is always the same movie (the one with id = 1). Is there a way to tell the relation to use an other column to compare with?
// EDIT: It now works. The foreign key in the relation was wrong. It should be "array('id' => 'movie_id')" instead of just "movie_id" and the "watched" relation in the Viewer model should have the additional option 'with' => 'movie' in order to avoid additional SQL Querys.
In http://www.yiiframework.com/doc/guide/1.1/en/database.arr we can read "In AR terminology, we can explain MANY_MANY as the combination of BELONGS_TO and HAS_MANY". So we can write:
an then we can write:
Hi,
I also really think there should be a native way in Yii to access the data in the association class in MANY_MANY relations...
Like adding the property from the association model to the related model. In this examle, I think we should be able to call :
@Asgotath: what you have done is change the two relationships into non-identifying relations. This is not correct for this example.
The entity represented by a row in
viewer_watched_movieis a relationship, and it cannot exist except by being owned by the entities it relates. Hence its relations should be identifying relations.Put it another way: unless there is a reason why a
viewer_watched_movierow should exist with its viewer and/or movie ids going nowhere, it should not have its own independent PK.Or, yet another way: if
ON DELETE SET NULLis appropriate then you have a non-identifying relation and the join table needs its own PK. But clearly this application needsON DELETE CASCADEfor both relations. That makes all the difference.If the entity in the join table is a relation and its every column does no more than elaborate on the nature of the relation then it is still a join table.
mmm I beleave the problem is not Yii, but the way you are designing you database schema, I was taught, that when you have a many many relationship then you have an intermediate table, just like you have. however, if that table needs to hold other data (in your case whether the viewer liked the movie or not), them that table should no longer have a composite key.
Instead the table becomes an entity itself, and is no longer just a relational table. and you know, an entity should have its own primary key (preferably numerical, unsigned, and auto-incremental), and those keys from viewer and movie, become normal foreign keys.
So your schema should look like this:
And yes, like you had to do in your example you now have to create a model to represent that table, and the reason is the stated adobe, it is now an entity itself.
Don't get hung up on the fact that the middle entity joins the two other entities, as it has properties in its own right. This make is an entity with two HAS_MANY relationships and should be modelled in this way. It's called an Association Class and there's lots of hits on Google for it but this one looked half decent:
etutorials.org Association Class
Leave a comment
Please login to leave your comment.