العقيدة 2: أفضل طريقة للتعامل مع العديد من الأعمدة مع الأعمدة الإضافية في الجدول المرجعي

StackOverflow https://stackoverflow.com/questions/3542243

سؤال

أنا أتساءل ما هو الأفضل ، والأنظف والأكثر طريقة للعمل مع العديد من العلاقات إلى العودية في العقيدة 2.

لنفترض أن لدينا ألبوم مثل سيد الدمى بواسطة ميتاليكا مع عدة مسارات. ولكن يرجى ملاحظة حقيقة أن مسار واحد قد يظهر في المزيد من الألبوم ، مثل بطارية بواسطة ميتاليكا لا - ثلاثة ألبومات تتميز بهذا المسار.

لذا فإن ما أحتاج إليه هو العلاقة بين العديد من الألبومات والمسارات ، باستخدام الجدول الثالث مع بعض الأعمدة الإضافية (مثل موضع المسار في الألبوم المحدد). في الواقع ، يجب أن أستخدم ، كما تشير وثائق العقيدة ، وهي علاقة مزدوجة من شخص واحد لتحقيق هذه الوظيفة.

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

وهذه ليست طريقة نظيفة للغاية.

هل كانت مفيدة؟

المحلول

لقد فتحت سؤالًا مشابهًا في القائمة البريدية للمستخدم ، وحصلت على إجابة بسيطة حقًا ؛

ضع في اعتبارك العديد من العلاقات ككيان بحد ذاته ، ثم تدرك أن لديك 3 كائنات ، مرتبطة بينهما بعلاقة واحدة إلى حد وعدة إلى واحدة.

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

بمجرد وجود علاقة ، لم تعد علاقة!

نصائح أخرى

من ألبوم $-> getTrackList () سوف تحصل على كيانات "ألبوم TrackReference" ، فماذا عن إضافة طرق من المسار والوكالة؟

class AlbumTrackReference
{
    public function getTitle()
    {
        return $this->getTrack()->getTitle();
    }

    public function getDuration()
    {
        return $this->getTrack()->getDuration();
    }
}

وبهذه الطريقة ، تبسط حلقةك إلى حد كبير ، كما أن جميع التعليمات البرمجية الأخرى تتعلق بحلق مسارات الألبوم ، حيث أن جميع الأساليب يتم إنشاؤها داخل الألبوم فقط TrakCreference:

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!' : ''
    );
}

راجع للشغل يجب عليك إعادة تسمية الألبوم (على سبيل المثال "الألبوم"). من الواضح أنه ليس فقط مرجعًا ، ولكنه يحتوي على منطق إضافي. نظرًا لأن هناك أيضًا مسارات غير متصلة بألبوم ولكنها متوفرة فقط من خلال العرض الترويجي أو شيء يسمح بفصل أنظف أيضًا.

لا شيء يتفوق على مثال لطيف

بالنسبة للأشخاص الذين يبحثون عن مثال ترميز نظيف للجمعيات الفردية/العديدة إلى واحدة بين الفئات المشاركة الثلاثة لتخزين سمات إضافية في العلاقة ، تحقق من هذا الموقع:

مثال جميل على الجمعيات الفردية/العديدة بين الفصول المشاركة الثلاثة

فكر في مفاتيحك الأساسية

فكر أيضًا في مفتاحك الأساسي. يمكنك في كثير من الأحيان استخدام المفاتيح المركبة لعلاقات مثل هذا. العقيدة تدعم هذا أصليا. يمكنك جعل كياناتك المرجعية في معرفات.تحقق من الوثائق على المفاتيح المركبة هنا

أعتقد أنني سأذهب مع اقتراح @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 ٪ من السيناريوهات التي صادفتها. ومع ذلك ، ماذا لو بطارية على سيد الدمى هو إصدار مختلف (طول مختلف ، مباشر ، صوتي ، ريمكس ، إعادة صياغة ، إلخ) عن الإصدار على مجموعة ميتاليكا.

اعتمادًا على الطريقة التي تريد التعامل بها (أو تجاهلها) في هذه الحالة ، يمكنك إما السير في طريق Beberlei المقترح ، أو مجرد الذهاب مع المنطق الإضافي المقترح في الألبوم :: getTrackList (). أنا شخصياً أعتقد أن المنطق الإضافي له ما يبرره للحفاظ على نظافة واجهة برمجة التطبيقات الخاصة بك ، لكن كلاهما لهما ميزة.

إذا كنت ترغب في استيعاب حالة الاستخدام الخاصة بي ، فيمكنك أن تحتوي على مسارات تحتوي على Onetomany ذاتيًا إلى مسارات أخرى ، وربما $ مماثلة. في هذه الحالة ، سيكون هناك كيانان للمسار بطارية, ، واحدة لأجل مجموعة ميتاليكا وواحد ل سيد الدمى. ثم سيحتوي كل كيان مسار مماثل على إشارة إلى بعضها البعض. أيضًا ، من شأنه أن يتخلص من فئة الألبوم الحالية ويلغي "المشكلة" الحالية. أوافق على أنه مجرد تحريك التعقيد إلى نقطة مختلفة ، لكنه قادر على التعامل مع usecase لم يكن قادرًا على ذلك.

أنت تسأل عن "أفضل طريقة" ولكن لا يوجد أفضل طريقة. هناك العديد من الطرق واكتشفت بعضها بالفعل. كيف تريد إدارة و/أو تغليف إدارة الجمعيات عند استخدام فصول الجمعيات ، متروك لك تمامًا ولطالتك الخرسانية ، لا يمكن لأي شخص أن يظهر لك "أفضل طريقة" أخشى.

بصرف النظر عن ذلك ، يمكن تبسيط السؤال كثيرًا عن طريق إزالة العقيدة وقواعد البيانات العلائقية من المعادلة. يتلخص جوهر سؤالك في سؤال حول كيفية التعامل مع فصول الجمعيات في Plain OOP.

كنت أتنقل من تعارض مع جدول الانضمام المحدد في فئة الارتباط (مع حقول مخصصة إضافية) وجدول انضمام محدد في شرح عديد من العدد.

يبدو أن تعريفات التعيين في كيانين يتمتعان بعلاقة مباشرة إلى العديد من العدد تؤدي إلى إنشاء تلقائي لجدول الانضمام باستخدام التعليق التوضيحي "القابل للتواصل". ومع ذلك ، تم تعريف جدول الانضمام بالفعل من خلال التعليق التوضيحي في فئة الكيان الأساسية الخاصة به وأردت أن يستخدم تعريفات المجال الخاصة بفئة الكيان الجمعية هذه لتوسيع جدول الانضمام مع حقول مخصصة إضافية.

التفسير والحل هو الذي تم تحديده بواسطة 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;

}

هذا مثال مفيد حقًا. إنه يفتقر إلى عقيدة التوثيق 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 ، ولكنهم يعتمدون على عقيدة 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/

حظا سعيدا ، ومراجع ميتاليكا لطيفة!

الحل في توثيق العقيدة. في الأسئلة الشائعة يمكنك رؤية هذا:

http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-any-to-dany-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

اتمني ان يكون مفيدا

أحادي الاتجاه. فقط أضف العكس: (اسم العمود الأجنبي) لجعله ثنائي الاتجاه.

# 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

اتمني ان يكون مفيدا. أرك لاحقًا.

قد تكون قادرًا على تحقيق ما تريد به فئة الجدول الميراث WHERE UNE DELLICKTRACKREFERENCE إلى الألبوم:

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

هل هناك أي طريقة أخرى لحل ذلك؟ أليس أحد الوصابين حلاً أفضل؟

هنا هو الحل كما هو موضح في عقيدة 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();
    }
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top