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
그리고 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();
}
그리고 그것은 매우 깨끗한 방법이 아닙니다.
Doctrine 사용자 메일 링리스트에서 비슷한 질문을했고 정말 간단한 답변을 얻었습니다.
다 대다 관계를 실체 자체로 생각하면, 일대 다 관계와 다 대일 관계로 연결된 3 개의 객체가 있다는 것을 알게됩니다.
관계에 데이터가 있으면 더 이상 관계가 아닙니다!
$ 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
와 귀하 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
인터페이스 뒤에 당신이 숨긴 때문에 모든 :)
도움이 되었기를 바랍니다!
첫째, 나는 주로 그의 제안에 대해 beberlei에 동의합니다. 그러나 함정에 빠지게 될 수도 있습니다. 귀하의 도메인은 타이틀이 트랙의 자연스러운 열쇠라고 생각하는 것 같습니다. 이는 시나리오의 99 %에 해당 될 것입니다. 그러나 어떤 경우 배터리 에 인형의 마스터 의 버전보다 다른 버전 (리마스터 다른 길이, 라이브, 음향, 리믹스, 등)입니다 메탈리카 컬렉션 .
이 경우를 처리 (또는 무시)하려는 방법에 따라 beberlei의 제안 된 경로로 이동하거나 Album :: getTracklist ()에서 제안 된 추가 논리를 사용할 수 있습니다. 개인적으로 API를 깨끗하게 유지하기 위해 추가 논리가 정당하다고 생각하지만 둘 다 장점이 있습니다.
내 유스 케이스를 수용하려면 트랙에 다른 트랙 (예 : $ similarTracks)을 참조하는 자체 참조 OneToMany를 포함시킬 수 있습니다. 이 경우 트랙 배터리에 대한 두 개의 엔티티가 있습니다 . 하나는 메탈리카 컬렉션 과 하나는 마스터 오브 퍼펫 입니다. 그런 다음 각 유사한 트랙 엔터티는 서로에 대한 참조를 포함합니다. 또한 현재 AlbumTrackReference 클래스를 제거하고 현재 "문제"를 제거합니다. 복잡성이 다른 지점으로 이동하고 있지만 이전에는 불가능했던 유스 케이스를 처리 할 수 있다는 데 동의합니다.
"가장 좋은 방법"을 요구하지만 최선의 방법은 없습니다. 여러 가지 방법이 있으며 이미 그 중 일부를 발견했습니다. 연관 클래스를 사용할 때 연관 관리를 관리 및 / 또는 캡슐화하는 방법은 전적으로 귀하와 귀하의 구체적인 영역에 달려 있으며, 아무도 "두려운 방법"을 보여줄 수 없습니다.
그 외에도, 교리와 관계형 데이터베이스를 방정식에서 제거하여 문제를 크게 단순화 할 수 있습니다. 귀하의 질문의 본질은 일반 OOP에서 연관 클래스를 처리하는 방법에 대한 질문으로 요약됩니다.
연결 클래스 (추가 사용자 정의 필드 포함) 주석에 정의 된 조인 테이블과 다 대다 주석에 정의 된 조인 테이블과의 충돌에서 왔습니다.
직접 다 대 다 관계를 갖는 두 엔티티의 맵핑 정의는 'joinTable'주석을 사용하여 결합 테이블을 자동으로 작성하는 것으로 나타났습니다. 그러나 조인 테이블은 이미 기본 엔터티 클래스의 주석으로 정의되었으며 추가 사용자 정의 필드로 조인 테이블을 확장하기 위해이 연관 엔터티 클래스의 자체 필드 정의를 사용하고 싶었습니다.
설명과 해결책은 위의 FMaz008에 의해 식별 된 것입니다. 내 상황에서, 그것은 ' Doctrine Annotation Question ' 포럼 의이 게시물 덕분이었습니다 . 이 글은 ManyToMany 단방향 관계 에 관한 교리 문서에 주목합니다 . 'association entity class'를 사용하는 방법에 대한 참고 사항을 확인하여 두 개의 주요 엔티티 클래스 사이의 다 대다 주석 맵핑을 기본 엔티티 클래스의 일대 다 주석과 두 개의 '다 대다'로 직접 대체하십시오. 연관 엔티티 클래스에서 -1 '주석. 이 포럼에서 추가 필드가있는 연관 모델에 제공되는 예제가 있습니다 .
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 프레임 워크를 참조하지만 Doctrine 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/
행운과 멋진 메탈리카 참조!
해결책은 교리 문서에 있습니다. FAQ에서 당신은 이것을 볼 수 있습니다 :
튜토리얼은 다음과 같습니다.
http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html
따라서 더 이상 수행하지 manyToMany
않지만 추가 엔티티를 작성 manyToOne
하여 두 엔티티에 추가 해야합니다.
ADD 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 : (Foreign Column Name)을 추가하여 양방향으로 만드십시오.
# 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 Table Inheritance를 사용 하여 원하는 것을 얻을 수 있습니다 .
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();
}
}
'Programing' 카테고리의 다른 글
MySQL이 외래 키 제약 조건을 추가 할 수 없음 (0) | 2020.03.25 |
---|---|
"x = x ++"다음에 x는 무엇입니까? (0) | 2020.03.25 |
대수 데이터 유형의 대수를 남용-왜 이것이 작동합니까? (0) | 2020.03.25 |
“git remote add…”와“git push origin master”는 무엇입니까? (0) | 2020.03.25 |
위도 및 경도 좌표를 사용하여 위치에서 시간대를 얻는 방법은 무엇입니까? (0) | 2020.03.25 |