Quantcast
Channel: Live News for Yii Framework
Viewing all articles
Browse latest Browse all 3361

[Wiki] Accessing data in a join table with the related models

$
0
0

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.

Movie viewers

EER diagram

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?

Accessing join table properties

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' => 'movies' -> array(self::MANY_MANY, 'Movie', 
                'viewer_watched_movie(movie_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 as a time in the loop. I can pep it up with with(): Viewer::model->with('movies')->findByPk($id).]

The problem

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.

The nearest solution I've found

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' => 'moviesJoin'),
    ...

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->moviesJoin 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.


Viewing all articles
Browse latest Browse all 3361

Trending Articles