RSS - Latest posts Doctrine Many To Many With Extra Fields

Home » 2009 » 05 » Doctrine Many To Many With Extra Fields

Recently I have started using Doctrine with Zend Framework. Most of the time it is great, but sometimes I get stuck on this or that issue. Most of my problems so far have been connected with the Many to Many relationship. Here are a few tips I learned the hard way.

Automatic relationship detection

When setting up a YAML file with your database schema you can start it with the handy declaration:

---
detect_relations: true

It does the One to Many relationship for you nicely, however the Many to Many ones did not work out of the box and required manual BaseModel tweaking. Take for example my Tag, Website, and WebsiteTag classes.

BaseTag:

abstract class BaseTag extends Doctrine_Record {

  public function setTableDefinition() {
    $this->setTableName('ts_tags');
    $this->hasColumn('name', 'string', 128, array('type' => 'string', 'length' => '128'));
    $this->hasColumn('is_category as isCategory', 'integer', 1, array('type' => 'integer', 'length' => '1'));
    $this->hasColumn('page_id', 'integer', 8, array('type' => 'integer', 'length' => 8));
    $this->hasColumn('ordering', 'integer', 5, array('type' => 'integer', 'length' => '5'));
  }

  public function setUp() {
    $this->hasOne('Page', array('local' => 'page_id',
                                              'foreign' => 'id'));

    $this->hasMany('Website as Websites', array('local' => 'tag_id',
                                                                     'foreign' => 'website_id',
                                                                     'refClass' => 'WebsiteTag'));

    $i18n0 = new Doctrine_Template_I18n(array('fields' => array(0 => 'name')));
    $this->actAs($i18n0);
  }
}

BaseWebsite:

abstract class BaseWebsite extends Doctrine_Record{
  public function setTableDefinition() {
    $this->setTableName('ts_websites');
    $this->hasColumn('page_id', 'integer', 8, array('type' => 'integer', 'length' => 8));
    $this->hasColumn('title', 'string', 128, array('type' => 'string', 'length' => '128'));
    $this->hasColumn('client', 'string', 128, array('type' => 'string', 'length' => '128'));
    $this->hasColumn('link', 'string', 255, array('type' => 'string', 'length' => '255'));
    $this->hasColumn('intro', 'string', null, array('type' => 'string'));
    $this->hasColumn('content', 'string', null, array('type' => 'string'));
    $this->hasColumn('published', 'timestamp', null, array('type' => 'timestamp'));
    $this->hasColumn('is_home as isHome', 'integer', 1, array('type' => 'integer', 'length' => '1'));
  }

  public function setUp() {
    $this->hasOne('Page', array('local' => 'page_id',
                                              'foreign' => 'id'));

    $this->hasMany('Tag as Tags', array('local' => 'website_id',
                                                          'foreign' => 'tag_id',
                                                          'refClass' => 'WebsiteTag'));

    $this->hasMany('Media', array('local' => 'website_id',
                                                 'foreign' => 'media_id',
                                                 'refClass' => 'WebsiteMedia'));

    $i18n0 = new Doctrine_Template_I18n(array('fields' => array(0 => 'intro', 1 => 'content', 2 => 'title')));
    $this->actAs($i18n0);
  }
}

BaseWebsiteTag:

abstract class BaseWebsiteTag extends Doctrine_Record {

  public function setTableDefinition() {
    $this->setTableName('ts_website_has_tags');
    $this->hasColumn('tag_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true ));
    $this->hasColumn('website_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true));
  }

  public function setUp() {
  }
}

The highlighted portions of code needed to be added. You can actually define these manually in the YAML file, the catch is that I was expecting it to happen magically with the auto relationship setting.

Many to Many with extra fields

Another little issue that Doctrine has is when you try to retrieve records. It in a way skips the joining table and retrieves a collection of the final items. In most cases this is exactly what you might need. However for my Media I decided to keep the ordering in the joining table – WebsiteMedia.

$q = Doctrine_Query::create()
  ->from('Website w')
  ->joinLeft('w.Media');

If you simply proceed with the above you only get Media objects without the ordering.

BaseWebsiteMedia:

abstract class BaseWebsiteMedia extends Doctrine_Record {

    public function setTableDefinition() {
        $this->setTableName('ts_website_has_media');
        $this->hasColumn('website_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true));
        $this->hasColumn('media_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true));
        $this->hasColumn('caption', 'string', 255, array('type' => 'string', 'length' => '255'));
        $this->hasColumn('is_main as isMain', 'integer', 1, array('type' => 'integer', 'length' => '1'));
        $this->hasColumn('ordering', 'integer', 4, array('type' => 'integer', 'length' => '4'));
    }

    public function setUp() {
        $this->hasOne('Website', array('local' => 'website_id',
                                       'foreign' => 'id'));

        $this->hasOne('Media', array('local' => 'media_id',
                                     'foreign' => 'id'));

        $i18n0 = new Doctrine_Template_I18n(array('fields' => array(0 => 'caption')));
        $this->actAs($i18n0);
    }
}

If you do not have the additional One to Many relations declared already you should add them now. With the modified model now you can get the records like so:

$q = Doctrine_Query::create()
  ->from('Website w')
  ->leftJoin('w.WebsiteMedia wm')
    ->leftJoin('wm.Media m');

This gives you a collection of WebsiteMedia records each containing the actual Media item.

 

You can leave a response, or trackback from your own site.

Add Comment

  1. Andres says:

    Thank you soooooooooo much man (or girld or ROBOT) !!! I added the missing relation to the auto-generated aux table, and my dql query works perfect. :D :D:D

  2. Rob says:

    Very nice example. Would it be possible to have all Media items loaded for a Website even if the linking WebsiteMedia record doesn’t exist (should give null values on ordering etc.)? This might seem odd in the current situation, however try to visualize it using User -> User_Setting -> Setting tables. Where Setting has a default_value. :)

  3. VanSanblch says:

    And what fixture file do you use for ts_website_has_media table?

  4. Hi there,
    Thank for this article.

    I want to ask you just one question,
    You have a many-to-many relationships between Media and Website.
    The link-table have some property like “ordering”, “caption”…
    The question is : “How do you proceed to order your result when you use the leftJoin on WebsiteMedia”.
    Otherwise, How do you proceed to access on the properties of the link-table with DQL.

    Thank,
    Armetiz.

  5. Jo says:

    Not sure if that’s what you are asking, but as you are explicitly using WebsiteMedia (with the alias wm) you should be able to access the properties as say wm.ordering or wm.caption.

    Don’t have the code at hand to check but I migth do a proper post about it later :)

  6. Okay, I have not see the DQL correctly.

    Two point are confused to me.
    The first, I want to select only some properties. But the select () isn’t working when I’m using the link-table like this;

    The second point is that you use a relation “WebsiteMedia” that isn’t define in the WebsiteBase::setUp () function.

    An optional point is, that the
    orderBy (“wm.ordring ASC”); doesn’t change the result order. Why..

  7. Joanna says:

    $q = Doctrine_Query::create()
    ->from(‘Model_Db_Websites w’)
    ->leftJoin(‘w.WebsiteMedia wm’)
    ->leftJoin(‘wm.Media m’)
    ->orderBy(‘wm.ordering ASC’)
    ;
    That’s the query above.

    Inside WebsitesBase setUp():

    $this->hasMany(‘Model_Db_WebsiteMedia as WebsiteMedia’, array(
    ‘local’ => ‘id’,
    ‘foreign’ => ‘websites_id’));

    WebsiteMedia setUp() function:

    $this->hasOne(‘Model_Db_Media as Media’, array(
    ‘local’ => ‘media_id’,
    ‘foreign’ => ‘id’));

    $this->hasOne(‘Model_Db_Websites as Websites’, array(
    ‘local’ => ‘websites_id’,
    ‘foreign’ => ‘id’));

  8. Prasad Gupte says:

    Do you how to define the fixture when the mapping (m:m) table has an extra field?

  9. fem! says:

    @Prasad Gupte
    Text:
    Text1:
    title: ‘t
    Author

  10. fem! says:

    Sorry for previous comment.
    @Prasad Gupte
    Text:
    Text1:
    title: ‘text1′
    Text2:
    title: ‘text2′
    Author:
    Author1:
    name: ‘a1′
    Author2:
    name: ‘a2′
    TextAuthor:
    TextAuthor1:
    author_id: Author1
    text_id: Text1
    TextAuthor2:
    author_id: Author2
    text_id: Text1

  11. Daniel says:

    great post, thanks for sharing

  12. amado says:

    I spent like 3 hours setting up the relations.. then I read your page and it hit me: The relations must be PROPERLY set up in BOTH of your Many-TO-Many Classes.. Not just your ‘connecting’ class.