[DDC-181] Order of many-to-many relationship Created: 28/Nov/09  Updated: 28/Feb/10  Resolved: 28/Feb/10

Status: Resolved
Project: Doctrine 2 - ORM
Component/s: ORM
Affects Version/s: 2.0-ALPHA3
Fix Version/s: None
Security Level: All

Type: New Feature Priority: Major
Reporter: Amir Abiri Assignee: Benjamin Eberlei
Resolution: Duplicate Votes: 0
Labels: None

Issue Links:
Duplicate
duplicates DDC-213 Persist order of collections Open
duplicates DDC-195 Ordering of associations Resolved

 Description   

I've come across the need for this in a scenario which is analogous to orders / products.

I've got an "order" which is linked to one or more "products". However the order (position) in which the products appear in the order is significant. Therefore I have a "position" field in the join table.

The requirement is:

  • To map the order of the array to values of the "position" field in the join table upon save.
  • To hydrate the array ordered by the position field when hydrating the object.


 Comments   
Comment by Roman S. Borschel [ 08/Dec/09 ]

I think is a more general improvement for persisting the order / the keys of any collection.

This is not trivial though. I dont think it can make it into 2.0 but I will schedule it for 2.1.

Comment by Amir Abiri [ 08/Dec/09 ]

How about a temporary solution? At the moment to work around this limitation I need to keep two collections in my object:

  1. A collection of the join table records, which I am forced to raise to an entity status. This collection persists.
  2. A collection of the actual entities on the other end of the many-to-many relationship.

Perhaps a possible temporary solution is to create a collection that basically does that but in an encapsulated way? If I had such a collection all I would really need is to re-order it with a map and a closure on @PostLoad. That would still mean that Doctrine saves me a hell of a lot.

I think properties of a join link are a very common need. I came across this limitation more than once with 1.x as well. Even with Propel before that.

Comment by Roman S. Borschel [ 08/Dec/09 ]

Your current solution is not bad at all if I understand it right. It is normal that when you have additional fields in the join table you need to map the join table as an association class. Call it a "many-to-many between A and B through C". Its how you would do it in plain OOP, too.

Given:

A manytomany B

Now you want to add a field to the association. You can not add it to class A or B. You need to create a class in-between (C, the association class).

That results in:

A onetomany C
C manytoone A
B onetomany C
C manytoone B

I agree, however, that the order is something special, since it is implicitly represented by the order of the elements in the collection. You dont need an association class in plain OOP for that and Doctrine should be able to handle that transparently, too. I agree there, its just not yet possible.

Right now the only way to order a collection-valued association is in a DQL query or in PHP, I guess you know that already.

Additionally, we want to add something like: @OrderBy("foo ASC, bar DESC") that you can apply on a collection-valued association so that the collection is always ordered, whether you use DQL or find()/findAll()/lazy loading/ ...

However, this does not yet cover the "persistence" of the order in the relational database which is the more complicated part.

I dont understand your proposed temporary solution. Can you show some (peudo)code that shows how you imagine that?

Thanks!

Comment by Amir Abiri [ 08/Dec/09 ]

I am not disputing what you say, I'm just saying that Doctrine can offer a collection class that encapsulates the repeating pattern of join properties.

Consider the following code snippet:
(Assume that each article has a different priority in each category it is in, where the priority can be only "Low", "Medium" or "High")

/**
 * @Entity
 * @Table(name="article")
 */
class Article
{
  // ...
}

/**
 * @Entity
 * @Table(name="article_category")
 */
class ArticleCategory
{
    const PRI_LOW  = 1;
    const PRI_MED  = 2;
    const PRI_HIGH = 3;

    /**
     * @OneToMany(targetEntity="Article",...)
     */
    private $article;

    /**
     * @OneToMany(targetEntity="Category",...)
     */
    private $category;

    /**
     * @Column(type="integer")
     */
    private $priority;
}

/**
 * @Entity
 * @Table(name="category")
 */
class Category
{
    /**
     * @ManyToMany(targetEntity="Article",...,joinEntity="ArticleCategory")
     */
    private $articles;



    public function getArticles()
    {
        return $this->articles->toArray(); // Returns an array of articles.
    }

    public function getArticleByIndex($idx)
    {
        return $this->articles[$idx];
    }

    public function getArticlePriority($idx)
    {
        return $this->articles->getJoinEntity($idx)->getPriority();
        // or alternatively:
        return $this->articles->joinEntities[$idx]->getPriority();
    }
}

Since Doctrine 2.0 is already being a bit invasive with the PersistentCollection class anyway, this does not present any additional invasiveness. You just get a subclass of PersistentCollection called PersistentManyToManyCollection if you specify the joinEntity in the @ManyToMany tag.

If you provide this functionality, the users can take care of such things like the order by themselves. They can also do it in a less invasive manner: they have full control over how to store the order property.

Comment by Roman S. Borschel [ 08/Dec/09 ]

Thats not how you map a many-to-many with an association class. There is no @ManyToMany at all.

See below:

class Article {
   private $id;
   /** @OneToMany(targetEntity="ArticleCategorization", ....) */
    private $categorizations;
}

class Category {
   private $id;
   /** @OneToMany(targetEntity="ArticleCategorization", ...) */
   private $categorizations;
}

class ArticleCategorization {
    private $id;
   /**
    * @ManyToOne(targetEntity="Article", ...)
    * @JoinColumn(...) 
    */
    private $article;
   /** 
    * @ManyToOne(targetEntity="Category", ...)
    * @JoinColumn(...)
    */
    private $category;
   /**
    * @Column(type="integer")
    */
    private $priority;
}

Now, order is a different thing. Just to persist the order of a collection should not require an association class (currently it does), I guess we agree on that.

I dont see how PersistentCollection is invasive, at least its the best we can do. You program to the Collection interface, you never need to care about PersistentCollection and should certainly not use methods that are not on the Collection interface. Collections of entities can not be arrays. PHP's arrays are not suitable as collections of entities. (We're still waiting for something like SplArray that implements sth like an SplCollection interface, the OO version of arrays .

Comment by Amir Abiri [ 08/Dec/09 ]

My point was that instead of breaking it down like that, it can be defined as a @ManyToMany with a twist.

I wasn't criticizing you about PersistentCollection, I was merely repeating what you guys wrote in the manual. I completely agree with your design decision there and I also blame PHP for not implementing the whole Array / Object fiasco properly. All I'm saying is that what I'm proposing would not introduce any new constraints to the entities.

Comment by Guilherme Blanco [ 09/Dec/09 ]

Both expose same enhancement.

Comment by Benjamin Eberlei [ 28/Feb/10 ]

This is a duplicate of DDC-213

Generated at Wed Nov 26 05:24:22 UTC 2014 using JIRA 6.2.3#6260-sha1:63ef1d6dac3f4f4d7db4c1effd405ba38ccdc558.