Yii Relational Active Record Not Loading Duplicate Related Entities

This topic is a cross-post of this question at StackOverflow - stackoverflow.com/questions/19104742/yii-relational-active-record-not-loading-duplicate-related-entities (can’t add this as a link as I’m new to this forum and I’m not allowed to).

I’m using the version 1.1.13 of Yii. Got the following problem with relational active records.

Have three tables created in MySQL database: tbl_playlist, tbl_track and tbl_playlist_track. Tracks belong to playlists, MANY_MANY relation is defined in the Playlist model, tbl_playlist_track is the relational table implementing the relation.

Tables:




CREATE TABLE IF NOT EXISTS `tbl_playlist` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `name` varchar(256) NOT NULL,

  PRIMARY KEY (`id`)

) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;


CREATE TABLE IF NOT EXISTS `tbl_track` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `title` varchar(256) NOT NULL,

  `added_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`),

) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;


CREATE TABLE IF NOT EXISTS `tbl_playlist_track` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `playlist_id` int(11) NOT NULL,

  `track_id` int(11) NOT NULL,

  `position` int(11) NOT NULL,

  PRIMARY KEY (`id`),

  KEY `playlist_id` (`playlist_id`),

  KEY `track_id` (`track_id`)

) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;


ALTER TABLE `tbl_playlist_track`

  ADD CONSTRAINT `tbl_playlist_track_ibfk_1` FOREIGN KEY (`playlist_id`) REFERENCES `tbl_playlist` (`id`) ON DELETE CASCADE,

  ADD CONSTRAINT `tbl_playlist_track_ibfk_2` FOREIGN KEY (`track_id`) REFERENCES `tbl_track` (`id`) ON DELETE CASCADE;



Relation declared in the Playlist model:




public function relations()

{

    return array(

        'tracks' => array(self::MANY_MANY,

                          'Track',

                          'tbl_playlist_track(playlist_id, track_id)',

                          'order' => 'position ASC')

    );

}



The only additional field in the tbl_playlist_track is ‘position’, which is used for sorting tracks in a playlist.

The problem I face with this setup is that if I have multiple entries in tbl_playlist_track with the same values of FK’s playlist_id and track_id (i.e. if the same track is added to a playlist several times) then when I load a Playlist model using the ‘with’ operator for eager loading - it instantiates only Track models with unique track_id values.

For example, if I have for a playlist with id 1 tracks added to tbl_playlist_track with ids 3 3 5 1 6 1 and I run this code:




$playlist = Playlist::model()->with('tracks')->findByPk(1);


foreach($playlist->tracks as $track)

{

    print $track->id." ";

}



then the output of the script will look like this:




3 5 1 6



I’ve checked the SQL query generated by Yii and it’s correct - it joins tables and returns all rows. So it must be related to the way Yii instantiates related model entities.

I found a blog post (hollyii.blogspot.be/2011/08/avoiding-duplicate-records-when-using.html) which made me believe that, actually, the default behavior should be the opposite - duplicate Track instances should have been created.

Hi El G,

I think the relation is not in fact a MANY_MANY, but a combination of a HAS_MANY and a BELONGS_TO.




// Playlist.php

public function relations()

{

    return array(

        'playlist_tracks' => array(self::HAS_MANY, 'PlaylistTrack', 'playlist_id',

                          'order' => 'position ASC')

    );

}

// PlaylistTrack.php

public function relations()

{

    return array(

        'track' => array(self::BELONGS_TO, 'Track', 'track_id'),

    );

}


// usage


$playlist = Playlist::model()->with(array('playlist_tracks', 'playlist_tracks.track'))->findByPk(1);

foreach($playlist->playlist_tracks as $playlist_track)

{

    print $playlist_track->track->id." ";

}



[size=2]Hi softark,[/size]

Thank you very much, this solution indeed works and duplicates are displayed now.

However, I don’t quite understand what exactly makes my case different and why MANY_MANY relation cannot be used. Solution with two relations is a bit less elegant (also in terms of usage), unfortunately.

In fact, all MANY_MANY relations are actually HAS_MANY + BELONGS_TO relations.

And only when we have no interest in the bridge table, then we can safely apply MANY_MANY relation to it and forget about the bridge table.

You could apply a MANY_MANY relation to the given tables, but then you had to forget about ‘position’ field in the bridge table because a MANY_MANY relation didn’t add the bridge AR object to the main object.

Yes, I think you are right.

Well, I agree that it could have been possible to implement MANY_MANY to allow duplicate entries. But I second the design decision made by the developers. I believe that MANY_MANY should not contain duplicate entries, otherwise it would have been very confusing … remember that HAS_MANY never contains duplicate entries.

Even if the duplication could be allowed in MANY_MANY relation, how would you distinguish the 2 entries with the same content? All you could rely on would be the appearing order of them in the array. And how would the order be defined? And what would you do to change it? After all, you would need ‘position’ that will be defined in a HAS_MANY related object outside the MANY_MANY relation.

So, falling back to HAS_MANY + BELONGS_TO is the right choice in your scenario. It may not look very elegant, but it’s the simplest and the most reasonable solution, IMO.

Well… I think this can be a Yii bug/feature…

look at CActiveFinder code (around line 869):




...

if($childRecord instanceof CActiveRecord)

       $fpk=serialize($childRecord->getPrimaryKey());

else

       $fpk=0;

if(!isset($this->_related[$pk][$child->relation->name][$fpk]))

...



now - as you can see related records are stored in associative array, where key value is created as serialized primaryKey of target objects (not pivot table in case of MANY_MANY relation). This means that multiple occurances of same primaryKey will hide each other so that there can be only one :)

Anyways - this means that you cannot have links to the same target objects in MANY_MANY relation and only solution is to have HAS_MANY / BELONGS_TO relations workaround as suggested by softark…

MANY_MANY requires that the join table have an unique constraint on all foreign keys. Usually there is no id column and both FK columns make up a composite primary key, making those pairs unique. If you add other columns and you want to access them it’s not a MANY_MANY relation anymore. Sure, you can use it as such, but with limitations. It’s not a bug.

Thank you for your replies.

Yes, I understand now why both technically and logically it all happens. Thanks!