[php] Doctrine2 : 참조 테이블의 추가 열을 사용하여 다대 다를 처리하는 가장 좋은 방법

Doctrine2에서 다 대다 관계로 작업하는 가장 좋고, 깨끗하고, 가장 간단한 방법이 무엇인지 궁금합니다.

Metallica의 Master of Puppets 와 같은 앨범 에 여러 트랙 이 있다고 가정 해 봅시다 . 그러나 Battery by Metallica 처럼 하나의 트랙이 하나 이상의 앨범에 나타날 수 있다는 사실에 유의하십시오 .이 트랙에는 3 개의 앨범이 있습니다.

그래서 필요한 것은 앨범과 트랙 사이의 다 대다 관계이며, 지정된 열의 트랙 위치와 같은 몇 가지 추가 열이있는 세 번째 테이블을 사용합니다. 실제로 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그리고 TrackgetTitle()방법을? 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();
 }

그리고 그것은 매우 깨끗한 방법이 아닙니다.



답변

Doctrine 사용자 메일 링리스트에서 비슷한 질문을했고 정말 간단한 답변을 얻었습니다.

다 대다 관계를 실체 자체로 생각하면, 일대 다 관계와 다 대일 관계로 연결된 3 개의 객체가 있다는 것을 알게됩니다.

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

관계에 데이터가 있으면 더 이상 관계가 아닙니다!


답변

$ 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”). 분명히 참조 일뿐 아니라 추가 논리가 포함되어 있습니다. 앨범에 연결되지 않았지만 프로모션 CD 또는 기타 항목을 통해 사용 가능한 트랙도있을 수 있으므로 더 명확하게 분리 할 수 ​​있습니다.


답변

좋은 예는 없습니다

관계에 추가 속성을 저장하기 위해 3 개의 참여 클래스 간의 일대 다 / 다 대일 연관의 깨끗한 코딩 예를 찾고있는 사람들은이 사이트를 확인하십시오.

3 개의 참여 클래스 사이의 일대 다 / 다 대일 연관의 좋은 예

기본 키에 대해 생각하십시오

기본 키에 대해서도 생각하십시오. 이와 같은 관계에 종종 복합 키를 사용할 수 있습니다. 교리는 기본적으로 이것을 지원합니다. 참조 된 엔터티를 ID로 만들 수 있습니다.
복합 키에 대한 설명서를 여기에서 확인하십시오.


답변

프록시 메소드 사용에 대한 @beberlei의 제안과 함께 할 것이라고 생각합니다. 이 프로세스를 단순화하기 위해 할 수있는 것은 두 가지 인터페이스를 정의하는 것입니다.

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

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

그런 다음 귀하 Album와 귀하 TrackAlbumTrackReference모두 구현할 수 있으며, 다음과 같이 여전히 구현할 수 있습니다.

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인터페이스 뒤에 당신이 숨긴 때문에 모든 🙂

도움이 되었기를 바랍니다!


답변

첫째, 나는 주로 그의 제안에 대해 beberlei에 동의합니다. 그러나 함정에 빠지게 될 수도 있습니다. 귀하의 도메인은 타이틀이 트랙의 자연스러운 열쇠라고 생각하는 것 같습니다. 이는 시나리오의 99 %에 해당 될 것입니다. 그러나 어떤 경우 배터리인형의 마스터 의 버전보다 다른 버전 (리마스터 다른 길이, 라이브, 음향, 리믹스, 등)입니다 메탈리카 컬렉션 .

이 경우를 처리 (또는 무시)하려는 방법에 따라 beberlei의 제안 된 경로로 이동하거나 Album :: getTracklist ()에서 제안 된 추가 논리를 사용할 수 있습니다. 개인적으로 API를 깨끗하게 유지하기 위해 추가 논리가 정당하다고 생각하지만 둘 다 장점이 있습니다.

내 유스 케이스를 수용하려면 트랙에 다른 트랙 (예 : $ similarTracks)을 참조하는 자체 참조 OneToMany를 포함시킬 수 있습니다. 이 경우 트랙 배터리에 대한 두 개의 엔티티가 있습니다 . 하나는 메탈리카 컬렉션 과 하나는 마스터 오브 퍼펫 입니다. 그런 다음 각 유사한 트랙 엔터티는 서로에 대한 참조를 포함합니다. 또한 현재 AlbumTrackReference 클래스를 제거하고 현재 “문제”를 제거합니다. 복잡성을 다른 지점으로 옮기는 데 동의하지만 이전에는 불가능했던 유스 케이스를 처리 할 수 ​​있습니다.


답변

“가장 좋은 방법”을 요구하지만 최선의 방법은 없습니다. 여러 가지 방법이 있으며 이미 그 중 일부를 발견했습니다. 연관 클래스를 사용할 때 연관 관리를 관리 및 / 또는 캡슐화하는 방법은 전적으로 귀하와 귀하의 구체적인 영역에 달려 있습니다.

그 외에도, 교리와 관계형 데이터베이스를 방정식에서 제거함으로써 문제를 크게 단순화 할 수 있습니다. 귀하의 질문의 본질은 일반 OOP에서 연관 클래스를 처리하는 방법에 대한 질문으로 요약됩니다.


답변

연결 클래스 (추가 사용자 정의 필드 포함) 주석에 정의 된 조인 테이블과 다 대다 주석에 정의 된 조인 테이블과의 충돌에서 왔습니다.

직접 다 대 다 관계를 갖는 두 엔티티의 맵핑 정의는 ‘joinTable’주석을 사용하여 결합 테이블을 자동으로 작성하는 것으로 나타났습니다. 그러나 조인 테이블은 이미 기본 엔터티 클래스의 주석에 의해 정의되었으며 추가 사용자 정의 필드로 조인 테이블을 확장하기 위해이 연관 엔터티 클래스의 자체 필드 정의를 사용하고 싶었습니다.

설명과 해결책은 위의 FMaz008에 의해 식별 된 것입니다. 내 상황에서, 그것은 ‘ Doctrine Annotation Question ‘ 포럼 의이 게시물 덕분이었습니다 . 이 글은 ManyToMany 단방향 관계 에 관한 교리 문서에 주목합니다 . ‘association entity class’를 사용하는 방법에 대한 참고 사항을 확인하여 두 개의 주요 엔티티 클래스 사이의 다 대다 주석 맵핑을 기본 엔티티 클래스의 일대 다 주석과 두 개의 ‘다 대다’로 직접 대체하십시오. 연관 엔티티 클래스에서 -1 ‘주석. 이 포럼 게시물 에 추가 필드가있는 Association 모델 의 예제가 있습니다 .

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;

}