Мне интересно, что является лучшим, самым чистым и простым способом работы с отношениями «многие ко многим» в Doctrine2.
Давайте предположим, что у нас есть альбом типа Master of Puppets от Metallica с несколькими треками. Но, пожалуйста, обратите внимание на тот факт, что один трек может появиться в более чем одном альбоме, как в Battery by 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();
}
И это не очень чистый способ.
$album->getTracklist()[12]
isAlbumTrackRef
object, поэтому$album->getTracklist()[12]->getTitle()
всегда будет возвращать заголовок трека (если вы используете прокси-метод). Пока$track->getAlbums()[1]
являетсяAlbum
объектом, поэтому$track->getAlbums()[1]->getTitle()
всегда будет возвращать название альбома.AlbumTrackReference
двух прокси-методовgetTrackTitle()
иgetAlbumTitle
.Ответы:
Я открыл похожий вопрос в списке рассылки пользователей Doctrine и получил очень простой ответ;
рассмотрите отношение многие ко многим как саму сущность, и тогда вы поймете, что у вас есть 3 объекта, связанных между собой отношениями «один ко многим» и «многие к одному».
http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Как только отношение имеет данные, оно больше не является отношением!
источник
app/console doctrine:mapping:import AppBundle yml
все еще генерирует отношение manyToMany для исходных двух таблиц и просто игнорирует третью таблицу вместо того, чтобы рассматривать ее как сущность:/
foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }
@Crozin отconsider the relationship as an entity
? Я думаю, что он хочет спросить, как пропустить реляционную сущность и получить название трека с помощьюforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
Из $ album-> getTrackList () вы всегда получите обратно объекты «AlbumTrackReference», так что насчет добавления методов из Track и прокси?
Таким образом, ваш цикл значительно упрощается, равно как и весь другой код, связанный с циклическим воспроизведением дорожек альбома, поскольку все методы просто проксируются внутри AlbumTrakcReference:
Кстати, вы должны переименовать AlbumTrackReference (например, «AlbumTrack»). Это явно не только ссылка, но и дополнительная логика. Так как, вероятно, есть также треки, которые не связаны с альбомом, а просто доступны через промо-диск или что-то еще, это также позволяет добиться более чистого разделения.
источник
Btw You should rename the AlbumT(...)
- хорошая точка$album->getTracklist()[1]->getTrackTitle()
так же хорошо / плохо, как$album->getTracklist()[1]->getTrack()->getTitle()
. Однако кажется, что мне нужно было бы иметь два разных класса: один для ссылок на альбом -> трек и другой для ссылок на трек -> альбомы - и это слишком сложно реализовать. Так что, вероятно, это лучшее решение на сегодняшний день ...Ничто не сравнится с хорошим примером
Для людей, которые ищут чистый пример кодирования связей один-ко-многим / многие-к-одному между 3 участвующими классами для хранения дополнительных атрибутов в отношении, просмотрите этот сайт:
хороший пример связи один-ко-многим / много-к-одному между 3 участвующими классами
Подумай о своих первичных ключах
Также подумайте о вашем первичном ключе. Вы можете часто использовать составные ключи для таких отношений. Доктрина изначально поддерживает это. Вы можете превратить ваши ссылочные объекты в идентификаторы. Проверьте документацию по составным ключам здесь
источник
Я думаю, что согласился бы с предложением @ beberlei использовать прокси-методы. Чтобы упростить этот процесс, вы можете определить два интерфейса:
Затем и ваш,
Album
и вашTrack
могут реализовать их, в то время как ониAlbumTrackReference
могут реализовать оба, как показано ниже:Таким образом, удаляя свою логику, которая непосредственно ссылается на a
Track
или anAlbum
, и просто заменяя ее так, чтобы она использовалаTrackInterface
илиAlbumInterface
, вы можете использовать вашуAlbumTrackReference
в любом возможном случае. Что вам нужно, это немного различать методы между интерфейсами.Это не будет дифференцировать ни DQL, ни логику репозитория, но ваши сервисы будут просто игнорировать тот факт, что вы передаете
Album
илиAlbumTrackReference
, или, илиTrack
или,AlbumTrackReference
потому что вы спрятали все за интерфейсом :)Надеюсь это поможет!
источник
Во-первых, я в основном согласен с Беберлей в его предложениях. Тем не менее, вы можете загнать себя в ловушку. Похоже, что ваш домен рассматривает заголовок как естественный ключ для трека, что, вероятно, имеет место в 99% сценариев, с которыми вы сталкиваетесь. Однако, что если Battery on Master of the Puppets - это другая версия (другая длина, живая, акустическая, ремикс, ремастеринг и т. Д.), Чем версия из коллекции Metallica .
В зависимости от того, как вы хотите обработать (или игнорировать) этот случай, вы можете либо пойти по предложенному Беберли маршруту, либо просто пойти с предложенной дополнительной логикой в Album :: getTracklist (). Лично я считаю, что дополнительная логика оправдана для поддержания чистоты вашего API, но у обоих есть свои достоинства.
Если вы хотите учесть мой вариант использования, вы можете использовать дорожки, которые ссылаются на OneToMany, ссылающуюся на другие дорожки, возможно, $ SimilarTracks. В этом случае будет два объекта для трека Battery , один для The Metallica Collection и один для Master of the Puppets . Тогда каждая похожая сущность Track будет содержать ссылку друг на друга. Кроме того, это избавит от текущего класса AlbumTrackReference и устранит вашу текущую «проблему». Я согласен с тем, что он просто переносит сложность в другую точку, но он способен обрабатывать сценарий использования, которого раньше не мог.
источник
Вы просите «лучший путь», но лучшего пути нет. Есть много способов, и вы уже обнаружили некоторые из них. То, как вы хотите управлять и / или инкапсулировать управление ассоциациями, когда использование классов ассоциаций полностью зависит от вас и вашего конкретного домена, боюсь, никто не может показать вам «лучший способ».
Кроме того, этот вопрос можно было бы значительно упростить, удалив доктрину и реляционные базы данных из уравнения. Суть вашего вопроса сводится к вопросу о том, как обращаться с классами ассоциаций в простом ООП.
источник
Я получал конфликт с таблицей соединений, определенной в аннотации класса ассоциации (с дополнительными настраиваемыми полями), и таблицей соединений, определенной в аннотации «многие ко многим».
Определения сопоставления в двух объектах с прямым отношением «многие ко многим», по-видимому, привели к автоматическому созданию таблицы объединения с использованием аннотации «joinTable». Однако таблица соединений уже была определена аннотацией в базовом классе сущностей, и я хотел, чтобы она использовала собственные определения полей этого класса сущностей ассоциации, чтобы расширить таблицу соединений дополнительными настраиваемыми полями.
Объяснение и решение - то, что определено FMaz008 выше. В моей ситуации это было благодаря этому посту на форуме ' Doctrine Annotation Question '. Этот пост привлекает внимание к документации Doctrine, касающейся однонаправленных отношений ManyToMany . Посмотрите на примечание, касающееся подхода использования «класса сущностей ассоциации», таким образом заменяя отображение аннотации «многие ко многим» непосредственно между двумя основными классами сущностей аннотацией «один ко многим» в основных классах сущностей и двумя «многими к» -он 'аннотации в классе ассоциативного объекта. В этом форуме есть пример ассоциации моделей с дополнительными полями :
источник
Это действительно полезный пример. В документации нет доктрины 2.
Большое спасибо.
Для прокси функции могут быть выполнены:
и
источник
То, что вы имеете в виду, это метаданные, данные о данных. У меня была такая же проблема для проекта, над которым я сейчас работаю, и мне пришлось потратить некоторое время, пытаясь понять это. Здесь слишком много информации для размещения, но ниже приведены две ссылки, которые могут оказаться полезными. Они ссылаются на платформу Symfony, но основаны на доктрине ORM.
http://melikedev.com/2010/04/06/symfony-saving-metadata-during-form-save-sort-ids/
http://melikedev.com/2009/12/09/symfony-w-doctrine-saving-many-to-many-mm-relationships/
Удачи и хороших ссылок Metallica!
источник
Решение находится в документации Доктрины. В FAQ вы можете увидеть это:
http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table
И учебник здесь:
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
Таким образом, вы больше не делаете,
manyToMany
но вы должны создать дополнительную сущность и поместитьmanyToOne
в нее две сущности.ДОБАВИТЬ для комментария @ f00bar:
все просто, нужно просто сделать что-то вроде этого:
Таким образом, вы создаете объект ArticleTag
Я надеюсь, что это помогает
источник
:(
Может ли кто-нибудь поделиться примером третьего варианта использования в формате yml? Я был бы очень ценен:#
Однонаправленный. Просто добавьте inversedBy: (имя внешнего столбца), чтобы сделать его двунаправленным.
Я надеюсь, что это помогает. Увидимся.
источник
Вы можете достичь желаемого с помощью наследования таблиц классов, где вы измените AlbumTrackReference на AlbumTrack:
И
getTrackList()
будет содержатьAlbumTrack
объекты, которые вы затем сможете использовать как хотите:Вам нужно будет тщательно проверить это, чтобы убедиться, что вы не страдаете от производительности.
Ваша текущая настройка проста, эффективна и проста для понимания, даже если некоторая семантика не совсем подходит вам.
источник
Получая все треки альбома в классе альбома, вы создадите еще один запрос для еще одной записи. Это из-за метода прокси. Есть еще один пример моего кода (см. Последнее сообщение в теме): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
Есть ли другой способ решить это? Разве одно соединение не является лучшим решением?
источник
Вот решение, описанное в документации Doctrine2
источник