Doctrine2: Лучший способ справиться со многими ко многим с помощью дополнительных столбцов в справочной таблице

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

Давайте предположим, что мы у нас есть альбом типа Master of Puppets Metallica с несколькими треками. Но, пожалуйста, обратите внимание на тот факт, что один трек может появляться в более чем одном альбоме, как это делает Battery 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) 

Ну и что? s неправильно?

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

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

EDIT

@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();
 }

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

276
задан S.L. Barth - Reinstate Monica 29 July 2014 в 22:14
поделиться

2 ответа

Из $album->getTrackList() вы всегда получите объекты "AlbumTrackReference", так что насчет добавления методов из Track и прокси?

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

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

Так ваш цикл значительно упрощается, как и весь остальной код, связанный с циклом. треки альбома, так как все методы просто проксируются внутри AlbumTrakcReference:

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!' : ''
    );
}

Btw Вы должны переименовать AlbumTrackReference (например, "AlbumTrack"). Он явно не только отсылка, но и содержит дополнительную логику. Так как, вероятно, также есть треки, которые не связаны с альбомом, а просто доступны через промо-диск или что-то еще, это также позволяет более четкое разделение.

17
ответ дан 23 November 2019 в 02:08
поделиться

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

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

6
ответ дан 23 November 2019 в 02:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: