Doctrine2: найкращий спосіб обробляти багато-багато-багато додаткових стовпців у довідковій таблиці


282

Мені цікаво, що найкращий, найчистіший і найпростіший спосіб співпраці з багатьма стосунками в Доктрині2.

Припустимо, що у нас вийшов альбом типу Master of Puppets від Metallica з кількома треками. Але зауважте, що один трек може з’явитися в більшій кількості одного альбому, як Battery by 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 \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

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

І це не дуже чистий спосіб.


2
Як ви поводитеся з AlphaTrackReference? Наприклад, $ album-> addTrack () або $ album-> removeTrack ()?
Даніель

Я не зрозумів, що ти коментуєш контекст. На мою думку, дані не залежать від контексту. About $album->getTracklist()[12]є AlbumTrackRefоб'єктом, тому $album->getTracklist()[12]->getTitle()завжди повертається заголовок доріжки (якщо ви використовуєте метод proxy). Хоча $track->getAlbums()[1]це Albumоб’єкт, тому $track->getAlbums()[1]->getTitle()завжди буде повертати назву альбому.
Vinícius Fagundes

Інша ідея - використання AlbumTrackReferenceдвох методів проксі, getTrackTitle()і getAlbumTitle.
Vinícius Fagundes

Відповіді:


158

Я відкрив подібне запитання в списку розсилки користувачів Doctrine і отримав дійсно просту відповідь;

розгляньте співвідношення "багато до багатьох" як сутність, і тоді ви зрозумієте, що у вас є 3 об'єкти, пов'язані між ними відношенням один-багато-багато і багато-до-одного.

http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Після того як відношення має дані, це вже не відношення!


Хтось знає, як я можу отримати інструмент командного рядка доктрини для створення цього нового об'єкта у вигляді файлу схеми yml? Ця команда: app/console doctrine:mapping:import AppBundle ymlвсе-таки генеруйте багато відношень дляToMany для початкових двох таблиць і просто ігноруйте третю таблицю, а не вважайте її сутністю:/
Stphane

в чому різниця між foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }наданими @Crozin і consider the relationship as an entity? Я думаю, що він хоче запитати, як пропустити реляційну сутність та отримати назву треку за допомогоюforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
панда

6
"Після того, як відношення має дані, це вже не стосунки" Це було справді освічуючим. Я просто не міг думати про відношення з точки зору сутності!
Цибуля

А як бути, якщо стосунки вже були створені та використовувались як багато-багато. Ми зрозуміли, що нам потрібні додаткові поля в наших багатьох до багатьох, тому ми створили іншу сутність. Проблема полягає в тому, що з наявними даними та існуючою таблицею з тим же ім’ям, схоже, не хочеться дружити. Хтось пробував це раніше?
тилеризм

Для тих, хто цікавиться: створення Суб'єкта за допомогою (вже існуючого) приєднаного між багатьма багатьма, як працює його таблиця, однак суб'єкти, що містять багато-багато-багато, повинні бути адаптовані до того, щоб один-багато-багато був новим. також інтерфейси із зовнішньої сторони (геттери / сетери для колишніх багатьох-до-багатьох) швидше за все повинні бути адаптовані.
Джакумі

17

Від $ 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!' : ''
    );
}

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


1
Проксі-методи не вирішують проблему на 100% (перевірити мою редагування). Btw You should rename the AlbumT(...)- хороший пункт
Крозін

3
Чому ви не маєте двох методів? getAlbumTitle () та getTrackTitle () на об’єкт AlbumTrackReference? Обидва проксі до відповідних субектив.
beberlei

Мета - API найбільш природного об'єкта. $album->getTracklist()[1]->getTrackTitle()так само добре / погано $album->getTracklist()[1]->getTrack()->getTitle(). Однак здається, що мені доведеться мати два різних класи: один для альбомів -> посилання на трек і інший для посилань на трек> альбоми - і це занадто важко для реалізації. Тож, напевно, це найкраще рішення досі ...
Крозін

13

Ніщо не б'є приємного прикладу

Для людей, які шукають чистий приклад кодування об'єднань один-багато-багато-один-один між 3 класами-учасниками, щоб зберігати додаткові атрибути у зв’язку, перевірте цей сайт:

приємний приклад асоціацій один на багато / багато на один між 3 класами-учасниками

Подумайте про свої первинні ключі

Також подумайте про свій основний ключ. Ви можете часто використовувати складові ключі для таких відносин. Вчення споконвічно це підтримує. Ви можете зробити свої посилання на ідентифікатори. Ознайомтеся з документацією на складені ключі тут


10

Я думаю, я б пішов із пропозицією @ 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();
    }
}

Таким чином, видаливши свою логіку, яка безпосередньо посилається на a Trackчи an Album, і просто замінивши її так, щоб вона використовувала a TrackInterfaceабо AlbumInterface, ви можете використовувати свою AlbumTrackReferenceв будь-якому можливому випадку. Що вам знадобиться, це трохи розмежувати методи між інтерфейсами.

Це не відрізнятиме DQL від логіки сховища, але ваші служби просто ігнорують той факт, що ви передаєте Albumабо AlbumTrackReference, або, або Trackабо AlbumTrackReferenceтому, що ви заховали все за інтерфейсом :)

Сподіваюсь, це допомагає!


7

По-перше, я здебільшого погоджуюся з beberlei щодо його пропозицій. Однак, можливо, ви проектуєте себе в пастку. Здається, ваш домен розглядає назву як природний ключ для треку, що, ймовірно, стосується 99% сценаріїв, на які ви потрапляєте. Однак що робити, якщо Battery on Master of Puppets - це інша версія (різної довжини, живої, акустичної, реміксної, ремастерованої тощо), ніж версія в колекції Metallica .

Залежно від того, як ви хочете обробити (або проігнорувати) цей випадок, ви можете або пройти запропонований маршрут beberlei, або просто піти із запропонованою вами додатковою логікою в альбомі :: getTracklist (). Особисто я вважаю, що додаткова логіка є виправданою, щоб ваш API чистий, але обидва мають свою заслугу.

Якщо ви хочете розмістити мій випадок використання, у вас можуть бути композиції, що містять самовідвідання OneToMany до інших треків, можливо, $ $ аналогічно. У цьому випадку для треку Battery буде дві сутності : одна для колекції Metallica та одна для Master of Puppets . Тоді кожен подібний об'єкт Track міститиме посилання один на одного. Крім того, це позбудеться поточного класу AlbumTrackReference і усуне поточну "проблему". Я погоджуюся, що це просто переміщення складності до іншої точки, але вона здатна обробляти корисну коробку, якої раніше не вміла.


6

Ви просите "найкращий шлях", але найкращого способу немає. Є багато способів, і ви вже відкрили деякі з них. Як ви хочете керувати та / або інкапсулювати управління асоціацією при використанні класів асоціацій, залежить лише від вас та вашого конкретного домену, я боюся, що ніхто не може показати вам "найкращий спосіб".

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


6

Я отримував конфлікт із таблицею приєднання, визначеною в анотації класу асоціацій (з додатковими спеціальними полями) та таблицею приєднання, визначеною в анотації «багато до багатьох».

Визначення відображення у двох об'єктах із прямим відношенням багато до багатьох виявилося результатом автоматичного створення таблиці приєднання за допомогою анотації 'joinTable'. Однак таблиця приєднання вже була визначена анотацією в її базовому класі сутності, і я хотів, щоб вона використовувала власні визначення цього класу об'єднаного об'єднання, щоб розширити таблицю приєднання додатковими спеціальними полями.

Пояснення та рішення - це те, що було визначено FMaz008 вище. У моїй ситуації саме завдяки цій публікації на форумі « Питання донорської анотації ». Цей пост привертає увагу документальної документації щодо багатонаправлених відносин ManyToMany . Подивіться на замітку щодо підходу використання "класу сутностей асоціації", таким чином заміняючи відображення анотацій "багато на багато" безпосередньо між двома основними класами сутності, анотацією "один на багато" в основних класах сутності та двома "багато-до" -онові примітки в класі асоціативної сутності. На цьому форумі є приклад, що надається на цьому форумі після моделей асоціацій із додатковими полями :

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;

}

3

Це дійсно корисний приклад. У ньому відсутня документація доктрини 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;

3

Ви маєте на увазі метадані, дані про дані. У мене був цей самий випуск для проекту, над яким я зараз працюю, і мені довелося витратити деякий час, намагаючись розібратися. Тут розміщується занадто багато інформації, але нижче наведено два посилання. Вони посилаються на рамки 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!


3

Рішення в документації доктрини. У 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:

це просто, вам потрібно просто зробити щось подібне:

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

Я сподіваюся, що це допомагає



Це саме те, що я шукав, дякую! На жаль, немає прикладу yml для випадку третього використання! :(Чи може хтось поділитись додатковою частиною третього випадку використання у форматі yml? Я б справді опинився:#
Stphane

я додав у відповідь вашу справу;)
Мірза Селімович

Це неправильно. Суб'єкт не повинен мати ідентифікатор (id) AUTO. Це неправильно, я намагаюся створити правильний приклад
Gatunox

Я опублікую нову відповідь, щоб отримати, якщо правильно відформатований
Gatunox

3

Односпрямований. Просто додайте перевернутийBy: (Ім'я іноземного стовпця), щоб зробити його двонаправленим.

# 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

Я сподіваюся, що це допомагає. Побачимося.


2

Можливо, вам вдасться досягти того, що ви хочете, завдяки спадкуванню таблиці таблиць, коли ви зміните 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!' : ''
    );
}

Вам потрібно буде уважно вивчити це, щоб переконатися, що ви не страждаєте від продуктивності.

Ваша поточна настройка проста, ефективна і її легко зрозуміти, навіть якщо частина семантики не зовсім сидить з вами.


0

Отримавши всі композиції альбомів всередині класу альбомів, ви генеруєте ще один запит для ще однієї записи. Це через метод проксі. Є ще один приклад мого коду (див. Останню публікацію в темі): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Чи є якийсь інший метод вирішення цього питання? Хіба жодне приєднання не є кращим рішенням?


1
Хоча це теоретично може відповісти на питання, бажано було б сюди включити істотні частини відповіді та надати посилання для довідки.
Spontifixus

0

Ось таке рішення, як описано в Документі2 Доктрини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();
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.