Доктрина2:Лучший способ обработки связи «многие-ко-многим» с дополнительными столбцами в справочной таблице

StackOverflow https://stackoverflow.com/questions/3542243

Вопрос

Мне интересно, какой лучший, самый чистый и простой способ работать с отношениями «многие ко многим» в Doctrine2.

Давайте предположим, что у нас есть альбом типа Кукловод от Metallica с несколькими треками.Но обратите внимание на тот факт, что один трек может появиться более чем в одном альбоме, например Батарея от Metallica да - этот трек присутствует в трёх альбомах.

Итак, мне нужна связь «многие ко многим» между альбомами и треками, используя третью таблицу с некоторыми дополнительными столбцами (например, положение трека в указанном альбоме).На самом деле мне приходится использовать, как предполагает документация Doctrine, двойное отношение один-ко-многим для достижения этой функциональности.

/** @Entity() */
class Album {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @Column() */
    protected $title;

    /** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="album") */
    protected $tracklist;

    public function __construct() {
        $this->tracklist = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getTitle() {
        return $this->title;
    }

    public function getTracklist() {
        return $this->tracklist->toArray();
    }
}

/** @Entity() */
class Track {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @Column() */
    protected $title;

    /** @Column(type="time") */
    protected $duration;

    /** @OneToMany(targetEntity="AlbumTrackReference", mappedBy="track") */
    protected $albumsFeaturingThisTrack; // btw: any idea how to name this relation? :)

    public function getTitle() {
        return $this->title;
    }

    public function getDuration() {
        return $this->duration;
    }
}

/** @Entity() */
class AlbumTrackReference {
    /** @Id @Column(type="integer") */
    protected $id;

    /** @ManyToOne(targetEntity="Album", inversedBy="tracklist") */
    protected $album;

    /** @ManyToOne(targetEntity="Track", inversedBy="albumsFeaturingThisTrack") */
    protected $track;

    /** @Column(type="integer") */
    protected $position;

    /** @Column(type="boolean") */
    protected $isPromoted;

    public function getPosition() {
        return $this->position;
    }

    public function isPromoted() {
        return $this->isPromoted;
    }

    public function getAlbum() {
        return $this->album;
    }

    public function getTrack() {
        return $this->track;
    }
}

Образец данных:

             Album
+----+--------------------------+
| id | title                    |
+----+--------------------------+
|  1 | Master of Puppets        |
|  2 | The Metallica Collection |
+----+--------------------------+

               Track
+----+----------------------+----------+
| id | title                | duration |
+----+----------------------+----------+
|  1 | Battery              | 00:05:13 |
|  2 | Nothing Else Matters | 00:06:29 |
|  3 | Damage Inc.          | 00:05:33 |
+----+----------------------+----------+

              AlbumTrackReference
+----+----------+----------+----------+------------+
| id | album_id | track_id | position | isPromoted |
+----+----------+----------+----------+------------+
|  1 |        1 |        2 |        2 |          1 |
|  2 |        1 |        3 |        1 |          0 |
|  3 |        1 |        1 |        3 |          0 |
|  4 |        2 |        2 |        1 |          0 |
+----+----------+----------+----------+------------+

Теперь я могу отобразить список альбомов и связанных с ними треков:

$dql = '
    SELECT   a, tl, t
    FROM     Entity\Album a
    JOIN     a.tracklist tl
    JOIN     tl.track t
    ORDER BY tl.position ASC
';

$albums = $em->createQuery($dql)->getResult();

foreach ($albums as $album) {
    echo $album->getTitle() . PHP_EOL;

    foreach ($album->getTracklist() as $track) {
        echo sprintf("\t#%d - %-20s (%s) %s\n", 
            $track->getPosition(),
            $track->getTrack()->getTitle(),
            $track->getTrack()->getDuration()->format('H:i:s'),
            $track->isPromoted() ? ' - PROMOTED!' : ''
        );
    }   
}

Результаты такие, как я ожидал, а именно:список альбомов с треками в соответствующем порядке и продвигаемыми, отмеченными как продвигаемые.

The Metallica Collection
    #1 - Nothing Else Matters (00:06:29) 
Master of Puppets
    #1 - Damage Inc.          (00:05:33) 
    #2 - Nothing Else Matters (00:06:29)  - PROMOTED!
    #3 - Battery              (00:05:13) 

Так что же не так?

Этот код демонстрирует, что не так:

foreach ($album->getTracklist() as $track) {
    echo $track->getTrack()->getTitle();
}

Album::getTracklist() возвращает массив AlbumTrackReference объекты вместо Track объекты.Я не могу создавать прокси-методы, потому что, если оба, Album и Track имел бы getTitle() метод?Я мог бы выполнить дополнительную обработку внутри Album::getTracklist() метод, но какой самый простой способ сделать это?Обязан ли я писать что-то подобное?

public function getTracklist() {
    $tracklist = array();

    foreach ($this->tracklist as $key => $trackReference) {
        $tracklist[$key] = $trackReference->getTrack();

        $tracklist[$key]->setPosition($trackReference->getPosition());
        $tracklist[$key]->setPromoted($trackReference->isPromoted());
    }

    return $tracklist;
}

// And some extra getters/setters in Track class

РЕДАКТИРОВАТЬ

@beberlei предложил использовать прокси-методы:

class AlbumTrackReference {
    public function getTitle() {
        return $this->getTrack()->getTitle()
    }
}

Это была бы хорошая идея, но я использую этот «эталонный объект» с обеих сторон: $album->getTracklist()[12]->getTitle() и $track->getAlbums()[1]->getTitle(), так getTitle() метод должен возвращать разные данные в зависимости от контекста вызова.

Мне нужно было бы сделать что-то вроде:

 getTracklist() {
     foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
 }

 // ....

 getAlbums() {
     foreach ($this->tracklist as $trackRef) { $trackRef->setContext($this); }
 }

 // ...

 AlbumTrackRef::getTitle() {
      return $this->{$this->context}->getTitle();
 }

И это не очень чистый способ.

Это было полезно?

Решение

Я открыл аналогичный вопрос в списке рассылки доктрины и получил действительно простой ответ;

Рассмотрим многие многочисленные соотношения в качестве самой объекта, а затем вы понимаете, что у вас есть 3 объекта, связанные между ними с однозначным и многим к одному соотношению.

http://groups.google.com/group/doctrine-user/browse_Thrad/thrad/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868.

Как только отношение имеет данные, это не более соотношение!

Другие советы

От $ Album-> GetTrackList () Вы будете получать объекты Alwas «AlbumTrackReference», а как насчет добавления методов от дорожки и прокси?

class AlbumTrackReference
{
    public function getTitle()
    {
        return $this->getTrack()->getTitle();
    }

    public function getDuration()
    {
        return $this->getTrack()->getDuration();
    }
}

Таким образом, ваш цикл значительно упрощает, Aswellow, как и все остальные, связанные с зацикливанием дорожек альбома, поскольку все методы просто проксированы внутри альбумтраккрефина:

foreach ($album->getTracklist() as $track) {
    echo sprintf("\t#%d - %-20s (%s) %s\n", 
        $track->getPosition(),
        $track->getTitle(),
        $track->getDuration()->format('H:i:s'),
        $track->isPromoted() ? ' - PROMOTED!' : ''
    );
}

Кстати, вы должны переименовать альбумперерабатывающуюся (например, «AlbumTrack»). Это явно не только ссылка, но и содержит дополнительную логику. Поскольку вероятно, также следы, которые не подключены к альбому, но просто доступны через промо-диск или что-то, что позволяет также для очистки отделения.

Ничто не сравнится с хорошим примером

Для людей, ищущих чистый пример кодирования однозначных ассоциаций в однознаком / много к одному между 3 классами участвующих для хранения дополнительных атрибутов в отношении этого сайта:

хороший пример ассоциаций из однозначных / много к одному между 3 классами участвующих

Подумайте о ваших основных ключах

Также подумайте о своем основном ключ. Вы часто можете использовать композитные ключевые ключи для таких отношений. Доктрина в родом поддерживает это. Вы можете сделать ваши ссылки на IDS.Проверьте документацию на композитных ключах здесь

Я думаю, что я бы пошел с предложением @ Beberlei с использованием методов прокси. Что вы можете сделать, чтобы сделать этот процесс проще, состоит в том, чтобы определить два интерфейса:

interface AlbumInterface {
    public function getAlbumTitle();
    public function getTracklist();
}

interface TrackInterface {
    public function getTrackTitle();
    public function getTrackDuration();
}

Затем, оба Album и ваш Track может реализовать их, пока AlbumTrackReference все еще может реализовать оба, как следствие:

class Album implements AlbumInterface {
    // implementation
}

class Track implements TrackInterface {
    // implementation
}

/** @Entity whatever */
class AlbumTrackReference implements AlbumInterface, TrackInterface
{
    public function getTrackTitle()
    {
        return $this->track->getTrackTitle();
    }

    public function getTrackDuration()
    {
        return $this->track->getTrackDuration();
    }

    public function getAlbumTitle()
    {
        return $this->album->getAlbumTitle();
    }

    public function getTrackList()
    {
        return $this->album->getTrackList();
    }
}

Таким образом, удалив свою логику, которая напрямую ссылается на Track или ан Album, и просто замените его так, чтобы он использовал TrackInterface или AlbumInterface, вы можете использовать свой AlbumTrackReference в любом возможном случае. То, что вам нужно, это дифференцировать методы между интерфейсами немного.

Это не будет дифференцировать DQL, ни в логике репозитории, но ваши услуги будут просто игнорировать тот факт, что вы передаете Album или ан AlbumTrackReference, или а Track или ан AlbumTrackReference Потому что вы спрятали все за интерфейсом :)

Надеюсь это поможет!

Во-первых, я во многом согласен с Беберлеем в его предложениях.Однако вы можете заманить себя в ловушку.Похоже, ваш домен считает заголовок естественным ключом к треку, что, вероятно, справедливо для 99% сценариев, с которыми вы сталкиваетесь.Однако что, если Батарея на Мастер кукол это другая версия (другая длина, концертная, акустическая, ремикс, ремастеринг и т. д.), чем версия на Коллекция Металлика.

В зависимости от того, как вы хотите обработать (или проигнорировать) этот случай, вы можете либо пойти по предложенному Беберли маршруту, либо просто использовать предложенную вами дополнительную логику в Album::getTracklist().Лично я считаю, что дополнительная логика оправдана для поддержания чистоты вашего API, но обе имеют свои преимущества.

Если вы хотите учесть мой вариант использования, вы можете сделать так, чтобы треки содержали самоссылку OneToMany на другие треки, возможно, $similarTracks.В этом случае для трека будет два объекта. Батарея, один для Коллекция Металлика и один для Мастер кукол.Тогда каждый подобный объект Track будет содержать ссылку друг на друга.Кроме того, это позволит избавиться от текущего класса AlbumTrackReference и устранить вашу текущую «проблему».Я согласен, что это просто переносит сложность в другую точку, но он способен справиться с вариантом использования, который раньше был невозможен.

Вы просите «лучший путь», но нет лучших способов. Есть много способов, и вы уже обнаружили некоторые из них. Как вы хотите управлять управлением и / или инкапсулировать управление ассоциацией при использовании классов ассоциации полностью зависит от вас и вашим конкретным доменом, никто не может показать вам «лучший способ», который я боюсь.

Кроме того, вопрос может быть очень упрощен, удалив доктрина и реляционные базы данных из уравнения. Суть вашего вопроса сводится к вопросу о том, как разобраться с классами ассоциации в простых ООП.

Я получал от конфликта с таблицей Join, определенной в классе ассоциации (с дополнительными пользовательскими полями) аннотацией и таблицей присоединения, определенной в аннотации многих ко многим.

Определения отображения в двух объектах с прямым соотношением во многих отношениях казалось, что приведет к автоматическому созданию таблицы присоединения с использованием аннотации «Совная» ». Однако таблица Join была уже определена путем аннотации в своем базовом классе сущности, и я хотел, чтобы он использовал свои определения собственных определений класса класса Assitiation Assitiation, чтобы продлить таблицу соединения с дополнительными пользовательскими полями.

Объяснение и решение в том, что идентифицируется FMAZ008 выше. В моей ситуации было благодаря этому посту на форуме 'Вопрос аннотации доктрины'. Этот пост обращает внимание на доктрину документацию в отношении Многотоманистые однонаправленные отношения. Отказ Посмотрите на ноту, касающуюся подхода к использованию «класса акций ассоциации», которая заменяет сопоставление сопоставления со многими ко многими, непосредственно между двумя классами основных объектов с аннотацией в однозначном уровне в основных классах сущности и двумя «многие -Отовые аннотации в ассоциативном классе сущности. Есть пример, предусмотренный на этом посте форума Модели ассоциации с дополнительными полями:

public class Person {

  /** @OneToMany(targetEntity="AssignedItems", mappedBy="person") */
  private $assignedItems;

}

public class Items {

    /** @OneToMany(targetEntity="AssignedItems", mappedBy="item") */
    private $assignedPeople;
}

public class AssignedItems {

    /** @ManyToOne(targetEntity="Person")
    * @JoinColumn(name="person_id", referencedColumnName="id")
    */
private $person;

    /** @ManyToOne(targetEntity="Item")
    * @JoinColumn(name="item_id", referencedColumnName="id")
    */
private $item;

}

Это действительно полезный пример. Не хватает в доктрине 2 документации.

Большое спасибо.

Для функций прокси можно сделать:

class AlbumTrack extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {} 
}

class TrackAlbum extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {}
}

class AlbumTrackAbstract {
   private $id;
   ....
}

а также

/** @OneToMany(targetEntity="TrackAlbum", mappedBy="album") */
protected $tracklist;

/** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
protected $albumsFeaturingThisTrack;

То, что вы ссылаетесь на метаданные, данные о данных. У меня была такой же проблема для проекта, над которой я сейчас работаю, и пришлось провести некоторое время, пытаясь понять это. Это слишком много информации, чтобы опубликовать здесь, но ниже двух ссылок, которые вы можете найти полезными. Они ссылаются на структуру Symfony, но основаны на доктрине ORM.

http://melikedev.com/2010/04/06/symfony-saving-metadata-ding-form-save-sort-iDs/

http://melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationsss/

Удачи, и хорошие ссылки на металлику!

Решение в документации доктрины. В FAQ вы можете увидеть это:

http://docs.doctrine-project.org/ru/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table.

И учебник здесь:

http://docs.doctrine-project.org/ru/2.1/tutorials/composite-primary-keys.html.

Так что вы больше не делаете manyToMany Но вы должны создать дополнительную сущность и поставить manyToOne к вашим двум объектам.

ДОБАВИТЬ Для @ f00bar комментарий:

Это просто, вы просто должны сделать что-то вроде этого:

Article  1--N  ArticleTag  N--1  Tag

Итак, вы создаете сущность Articletag

ArticleTag:
  type: entity
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  manyToOne:
    article:
      targetEntity: Article
      inversedBy: articleTags
  fields: 
    # your extra fields here
  manyToOne:
    tag:
      targetEntity: Tag
      inversedBy: articleTags

Я надеюсь, что это помогает

Однонаправленный. Просто добавьте INVERSEDBY: (имя иностранного столбца), чтобы сделать его двунаправленным.

# config/yaml/ProductStore.dcm.yml
ProductStore:
  type: entity
  id:
    product:
      associationKey: true
    store:
      associationKey: true
  fields:
    status:
      type: integer(1)
    createdAt:
      type: datetime
    updatedAt:
      type: datetime
  manyToOne:
    product:
      targetEntity: Product
      joinColumn:
        name: product_id
        referencedColumnName: id
    store:
      targetEntity: Store
      joinColumn:
        name: store_id
        referencedColumnName: id

Я надеюсь, что это помогает. Увидимся.

Вы можете достичь того, с кем вы хотите Наследование таблицы класса Где вы меняете альбумперерабатывающую к альбому:

class AlbumTrack extends Track { /* ... */ }

А также getTrackList() будет содержать AlbumTrack объекты, которые вы могли бы использовать так, как вы хотите:

foreach($album->getTrackList() as $albumTrack)
{
    echo sprintf("\t#%d - %-20s (%s) %s\n", 
        $albumTrack->getPosition(),
        $albumTrack->getTitle(),
        $albumTrack->getDuration()->format('H:i:s'),
        $albumTrack->isPromoted() ? ' - PROMOTED!' : ''
    );
}

Вам нужно будет изучить это практически, чтобы убедиться, что вы не пострадаете от производительности.

Ваша текущая настройка проста, эффективна и легко понять, даже если некоторые семантики не совсем сидят с вами.

При получении всех треков альбома формируются в классе альбома, вы генерируете еще один запрос для еще одной записи. Это из-за прокси-метода. Есть еще один пример моего кода (см. Последнее сообщение в теме): http://groups.google.com/group/doctrine-user/browse_Thrad/thrad/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868.

Есть ли другой метод для решения этого? Не один присоединяйся к лучшему решению?

Вот решение, как описано в Документация доктрины2

<?php
use Doctrine\Common\Collections\ArrayCollection;

/** @Entity */
class Order
{
    /** @Id @Column(type="integer") @GeneratedValue */
    private $id;

    /** @ManyToOne(targetEntity="Customer") */
    private $customer;
    /** @OneToMany(targetEntity="OrderItem", mappedBy="order") */
    private $items;

    /** @Column(type="boolean") */
    private $payed = false;
    /** @Column(type="boolean") */
    private $shipped = false;
    /** @Column(type="datetime") */
    private $created;

    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
        $this->items = new ArrayCollection();
        $this->created = new \DateTime("now");
    }
}

/** @Entity */
class Product
{
    /** @Id @Column(type="integer") @GeneratedValue */
    private $id;

    /** @Column(type="string") */
    private $name;

    /** @Column(type="decimal") */
    private $currentPrice;

    public function getCurrentPrice()
    {
        return $this->currentPrice;
    }
}

/** @Entity */
class OrderItem
{
    /** @Id @ManyToOne(targetEntity="Order") */
    private $order;

    /** @Id @ManyToOne(targetEntity="Product") */
    private $product;

    /** @Column(type="integer") */
    private $amount = 1;

    /** @Column(type="decimal") */
    private $offeredPrice;

    public function __construct(Order $order, Product $product, $amount = 1)
    {
        $this->order = $order;
        $this->product = $product;
        $this->offeredPrice = $product->getCurrentPrice();
    }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top