Doctrine2: лучший способ обрабатывать многие ко многим с дополнительными столбцами в справочной таблице
мне интересно, какой самый лучший, самый чистый и самый простой способ работы со многими-ко-многим отношениями в Доктрине2.
давайте предположим, что у нас есть альбом, как мастер марионеток от Metallica несколько треков. Но обратите внимание, что один трек может появиться в нескольких альбомах, например батарея от Metallica делает-три альбома с участием этого трека.
Ну и что я нужна связь "многие ко многим" между альбомами и треками, используя третью таблицу с некоторыми дополнительными столбцами (например, положение трека в указанном альбоме). На самом деле я должен использовать, как предполагает документация доктрины, двойное отношение "один ко многим" для достижения этой функциональности.
/** @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 DoctrineCommonCollectionsArrayCollection();
}
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 EntityAlbum 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) %sn",
$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
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();
}
и это не очень чисто.
14 ответов:
Я открыл аналогичный вопрос в списке рассылки пользователей доктрины и получил очень простой ответ;
рассмотрите отношение "многие ко многим" как саму сущность, и тогда вы поймете, что у вас есть 3 объекта, связанные между собой отношением "один ко многим" и "многие к одному".
Как только отношение имеет данные, это больше не a родственник !
из $album - >getTrackList () вы всегда будете получать объекты "AlbumTrackReference", так что о добавлении методов из трека и прокси?
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!' : '' ); }
Кстати, вы должны переименовать AlbumTrackReference (например, "AlbumTrack"). Это явно не только ссылка, но и содержит дополнительную логику. Поскольку есть, вероятно, также треки, которые не связаны с альбомом, но просто доступны через промо-cd или что-то еще, это также позволяет более чистое разделение.
ничто не сравнится с хорошим примером
для людей, которые ищут чистый пример кодирования ассоциаций один-ко-многим/многие - к-одному между 3 участвующими классами для хранения дополнительных атрибутов в отношении проверьте этот сайт:
хороший пример ассоциаций один-ко-многим / многие-к-одному между 3 участвующими классами
подумайте о своих первичных ключах
также подумайте о своем первичном ключе. Для таких отношений часто можно использовать составные ключи. Доктрина изначально поддерживает это. Вы можете сделать ваши ссылочные объекты в идентификаторы. проверьте документацию по составным ключам здесь
я думаю, что я бы пошел с предложением @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% сценариев, с которыми вы сталкиваетесь. Однако, что если батарея on Мастер кукол - это другая версия (другая длина, живая, акустическая, ремикс, ремастеринг и т. д.), Чем версия на Группа "Металлика" Коллекция.
в зависимости от того, как вы хотите обрабатывать (или игнорировать) этот случай, вы можете либо пойти по предлагаемому маршруту beberlei, либо просто пойти с предложенной дополнительной логикой в Album::getTracklist(). Лично я думаю, что дополнительная логика оправдана, чтобы сохранить ваш API чистым, но у обоих есть свои достоинства.
Если вы хотите разместить мой вариант использования, у вас могут быть треки, содержащие самостоятельную ссылку OneToMany на другие треки, возможно, $similarTracks. В этом случае, есть было бы два объекта для трека батарея, для Коллекция Metallica и Мастер кукол. Тогда каждый подобный объект трека будет содержать ссылку друг на друга. Кроме того, это позволит избавиться от текущего класса AlbumTrackReference и устранить вашу текущую "проблему". Я согласен, что он просто перемещает сложность в другую точку, но он способен обрабатывать usecase, который ранее не был в состоянии.
вы просите "лучший способ", но нет лучшего способа. Есть много способов, и вы уже обнаружили некоторые из них. Как вы хотите управлять и / или инкапсулировать управление ассоциациями при использовании классов ассоциаций полностью зависит от вас и вашего конкретного домена, никто не может показать вам "лучший способ", я боюсь.
кроме того, этот вопрос можно было бы значительно упростить, удалив из уравнения доктрину и реляционные базы данных. Суть вашего вопроса сводится к вопрос о том, как обращаться с классами ассоциаций в простом ООП.
Я получал от конфликта с таблицей соединений, определенной в аннотации класса ассоциации ( с дополнительными пользовательскими полями), и таблицей соединений, определенной в аннотации "многие ко многим".
определения сопоставления в двух объектах с прямым отношением "многие ко многим", по-видимому, привели к автоматическому созданию таблицы соединений с использованием аннотации "joinTable". Однако таблица соединений уже была определена аннотацией в ее базовом классе сущностей, и я хотел ее использовать собственные определения полей этого класса сущностей ассоциации, чтобы расширить объединяемую таблицу дополнительными пользовательскими полями.
объяснение и решение-это то, что определено 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, но основаны на доктрине ОЗР.
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/tutorials/composite-primary-keys.html
таким образом, вы больше не делаете
manyToMany
но вы должны создать дополнительную сущность и поставитьmanyToOne
to ваши две сущности.добавить для @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
Я надеюсь, что это помогает. Увидимся.
вы можете быть в состоянии достичь того, чего вы хотите с Наследование Таблицы Классов где вы меняете AlbumTrackReference на AlbumTrack:
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_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868
есть ли другой способ решить это? Разве одно соединение не является лучшим решением?
вот решение, как описано в Doctrine2 Documentation
<?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(); } }