Doctrine2: Cara terbaik untuk menangani banyak-ke-banyak dengan kolom tambahan dalam tabel referensi

282

Saya bertanya-tanya apa yang terbaik, paling bersih dan cara paling sederhana untuk bekerja dengan banyak-ke-banyak hubungan di Doctrine2.

Mari kita asumsikan bahwa kita punya album seperti Master of Puppets oleh Metallica dengan beberapa lagu. Tapi tolong perhatikan fakta bahwa satu lagu mungkin muncul di lebih dari satu album, seperti yang dilakukan Battery oleh Metallica - tiga album menampilkan lagu ini.

Jadi yang saya butuhkan adalah hubungan banyak-ke-banyak antara album dan trek, menggunakan tabel ketiga dengan beberapa kolom tambahan (seperti posisi trek di album tertentu). Sebenarnya saya harus menggunakan, seperti yang ditunjukkan oleh doktrin Doktrin, hubungan ganda satu-ke-banyak untuk mencapai fungsi itu.

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

Contoh data:

             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 |
+----+----------+----------+----------+------------+

Sekarang saya dapat menampilkan daftar album dan trek yang terkait dengannya:

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

Hasilnya adalah apa yang saya harapkan, yaitu: daftar album dengan trek mereka dalam urutan yang sesuai dan yang dipromosikan ditandai sebagai dipromosikan.

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) 

Jadi apa yang salah?

Kode ini menunjukkan apa yang salah:

foreach ($album->getTracklist() as $track) {
    echo $track->getTrack()->getTitle();
}

Album::getTracklist()mengembalikan array AlbumTrackReferenceobjek, bukan Trackobjek. Saya tidak dapat membuat metode proksi karena bagaimana jika keduanya, Albumdan Trackakan memiliki getTitle()metode? Saya bisa melakukan beberapa pemrosesan tambahan dalam Album::getTracklist()metode tetapi apa cara paling sederhana untuk melakukan itu? Apakah saya dipaksa menulis sesuatu seperti itu?

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 menyarankan untuk menggunakan metode proxy:

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

Itu akan menjadi ide yang bagus tapi saya menggunakan "objek referensi" dari kedua sisi: $album->getTracklist()[12]->getTitle()dan $track->getAlbums()[1]->getTitle(), jadi getTitle()metode harus mengembalikan data yang berbeda berdasarkan konteks doa.

Saya harus melakukan sesuatu seperti:

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

Dan itu bukan cara yang sangat bersih.

Crozin
sumber
2
Bagaimana Anda menangani Referensi AlbumTrack? Misalnya $ album-> addTrack () atau $ album-> removeTrack ()?
Daniel
Saya tidak mengerti komentar Anda tentang konteks. Menurut pendapat saya, data tidak tergantung pada konteks. About $album->getTracklist()[12]is AlbumTrackRefobject, jadi $album->getTracklist()[12]->getTitle()akan selalu mengembalikan judul trek (jika Anda menggunakan metode proxy). Sementara $track->getAlbums()[1]ini Albumobjek, sehingga $track->getAlbums()[1]->getTitle()akan kembali selalu judul album.
Vinícius Fagundes
Gagasan lain adalah menggunakan AlbumTrackReferencedua metode proxy, getTrackTitle()dan getAlbumTitle.
Vinícius Fagundes

Jawaban:

158

Saya telah membuka pertanyaan serupa di milis pengguna Doctrine dan mendapatkan jawaban yang sangat sederhana;

pertimbangkan relasi many to many sebagai entitas itu sendiri, dan kemudian Anda sadar bahwa Anda memiliki 3 objek, yang dihubungkan di antara mereka dengan relasi satu ke banyak dan banyak-ke-satu.

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

Setelah relasi memiliki data, itu bukan lagi relasi!

FMaz008
sumber
Apakah ada yang tahu bagaimana saya bisa mendapatkan alat baris perintah doktrin untuk menghasilkan entitas baru ini sebagai file skema yml? Perintah ini: app/console doctrine:mapping:import AppBundle ymlmasih menghasilkan manyToMany relasi untuk dua tabel asli dan mengabaikan tabel ketiga alih-alih menganggapnya sebagai entitas:/
Stphane
apa perbedaan antara yang foreach ($album->getTracklist() as $track) { echo $track->getTrack()->getTitle(); }disediakan oleh @Crozin dan consider the relationship as an entity? Saya pikir apa yang ingin dia tanyakan adalah bagaimana cara melompati entitas relasional dan mengambil judul trek dengan menggunakanforeach ($album->getTracklist() as $track) { echo $track->getTitle(); }
panda
6
"Begitu suatu relasi memiliki data, itu bukan lagi relasi" Ini benar-benar mencerahkan. Saya hanya tidak bisa memikirkan hubungan dari perspektif entitas!
Bawang
Bagaimana kalau hubungan itu sudah dibuat dan digunakan sebagai banyak ke banyak. Kami menyadari bahwa kami membutuhkan bidang tambahan dalam banyak untuk banyak sehingga kami menciptakan entitas yang berbeda. Masalahnya adalah, dengan data yang ada, dan tabel yang ada dengan nama yang sama, sepertinya tidak ingin berteman. Adakah yang pernah mencoba ini sebelumnya?
tylerism
Bagi mereka yang bertanya-tanya: menciptakan Entitas dengan banyak-ke-banyak yang dapat disatukan saat tabelnya bekerja, bagaimanapun, entitas yang memegang banyak-ke-banyak harus diadaptasi alih-alih satu-ke-banyak ke entitas baru. juga antarmuka ke luar (getter / setter untuk mantan banyak-ke-banyak) kemungkinan besar harus disesuaikan.
Jakumi
17

Dari $ album-> getTrackList () Anda akan mendapatkan entitas "AlbumTrackReference" kembali, jadi bagaimana dengan menambahkan metode dari Track dan proxy?

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

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

Dengan cara ini, perulangan Anda sangat disederhanakan, juga semua kode lain yang terkait dengan perulangan trek sebuah album, karena semua metode hanya diproksi di dalam 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 Anda harus mengganti nama AlbumTrackReference (misalnya "AlbumTrack"). Jelas bukan hanya referensi, tetapi mengandung logika tambahan. Karena mungkin ada juga Tracks yang tidak terhubung ke album tetapi hanya tersedia melalui promo-cd atau sesuatu ini memungkinkan pemisahan yang lebih bersih juga.

beberlei
sumber
1
Metode proxy tidak menyelesaikan masalah dalam 100% (periksa edit saya). Btw You should rename the AlbumT(...)- Poin bagus
Crozin
3
Mengapa Anda tidak memiliki dua metode? getAlbumTitle () dan getTrackTitle () pada objek AlbumTrackReference? Keduanya proksi ke masing-masing sub proyek.
beberlei
Tujuannya adalah objek API paling alami . $album->getTracklist()[1]->getTrackTitle()sama baiknya / buruknya $album->getTracklist()[1]->getTrack()->getTitle(). Namun sepertinya saya harus memiliki dua kelas yang berbeda: satu untuk referensi album-> trek dan satu lagi untuk referensi track-> album - dan itu terlalu sulit untuk diterapkan. Jadi mungkin itu solusi terbaik sejauh ini ...
Crozin
13

Tidak ada yang mengalahkan contoh yang bagus

Untuk orang yang mencari contoh pengkodean yang bersih dari asosiasi satu-ke-banyak / banyak-ke-satu antara 3 kelas yang berpartisipasi untuk menyimpan atribut tambahan dalam relasi, periksa situs ini:

contoh yang bagus dari asosiasi satu-ke-banyak / banyak-ke-satu antara 3 kelas yang berpartisipasi

Pikirkan tentang kunci utama Anda

Juga pikirkan kunci utama Anda. Anda sering dapat menggunakan kunci komposit untuk hubungan seperti ini. Ajaran asli mendukung ini. Anda dapat membuat entitas yang dirujuk menjadi id. Periksa dokumentasi pada kunci komposit di sini

Melayu
sumber
10

Saya pikir saya akan pergi dengan saran @ beberlei untuk menggunakan metode proxy. Apa yang dapat Anda lakukan untuk membuat proses ini lebih sederhana adalah dengan mendefinisikan dua antarmuka:

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

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

Kemudian, Anda Albumdan Anda Trackdapat menerapkannya, sementara AlbumTrackReferencemasih dapat menerapkan keduanya, sebagai berikut:

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

Dengan cara ini, dengan menghapus logika Anda yang secara langsung mereferensikan a Trackatau a Album, dan hanya menggantinya sehingga menggunakan a TrackInterfaceatau AlbumInterface, Anda bisa menggunakan Anda AlbumTrackReferencedalam keadaan apa pun. Yang Anda perlukan adalah membedakan metode antar antarmuka sedikit.

Ini tidak akan membedakan DQL atau logika Repositori, tetapi layanan Anda hanya akan mengabaikan fakta bahwa Anda melewati suatu Albumatau AlbumTrackReference, atau a TrackatauAlbumTrackReference karena Anda telah menyembunyikan segala sesuatu di balik antarmuka :)

Semoga ini membantu!

Ocramius
sumber
7

Pertama, saya sebagian besar setuju dengan beberlei tentang sarannya. Namun, Anda mungkin mendesain diri sendiri menjadi jebakan. Domain Anda tampaknya mempertimbangkan judul sebagai kunci alami untuk sebuah trek, yang kemungkinan besar merupakan kasus 99% dari skenario yang Anda temui. Namun, bagaimana jika Battery pada Master of the Puppets adalah versi yang berbeda (panjang berbeda, live, akustik, remix, remaster, dll) daripada versi di The Metallica Collection .

Bergantung pada bagaimana Anda ingin menangani (atau mengabaikan) kasing itu, Anda bisa pergi dengan rute yang disarankan beberlei, atau hanya pergi dengan logika tambahan yang Anda usulkan di Album :: getTracklist (). Secara pribadi, saya pikir logika ekstra dibenarkan untuk menjaga API Anda bersih, tetapi keduanya memiliki kelebihan.

Jika Anda ingin mengakomodasi kasus penggunaan saya, Anda dapat memiliki Lagu yang berisi referensi sendiri OneToMany ke Lagu lain, mungkin $ similarTracks. Dalam hal ini, akan ada dua entitas untuk trek Battery , satu untuk The Metallica Collection dan satu untuk Master of the Puppets . Kemudian setiap entitas Track yang serupa akan berisi referensi satu sama lain. Juga, itu akan menghilangkan kelas AlbumTrackReference saat ini dan menghilangkan "masalah" Anda saat ini. Saya setuju bahwa itu hanya memindahkan kompleksitas ke titik yang berbeda, tetapi ia mampu menangani usecase yang sebelumnya tidak mampu.

jsuggs
sumber
6

Anda meminta "cara terbaik" tetapi tidak ada cara terbaik. Ada banyak cara dan Anda sudah menemukan beberapa di antaranya. Bagaimana Anda ingin mengelola dan / atau merangkum manajemen asosiasi ketika menggunakan kelas asosiasi sepenuhnya terserah Anda dan domain konkret Anda, tidak ada yang bisa menunjukkan "cara terbaik" kepada saya.

Terlepas dari itu, pertanyaannya dapat disederhanakan banyak dengan menghapus Doktrin dan basis data relasional dari persamaan. Inti dari pertanyaan Anda adalah pertanyaan tentang bagaimana menangani kelas asosiasi di OOP.

romanb
sumber
6

Saya mendapatkan dari konflik dengan tabel bergabung yang ditentukan dalam kelas asosiasi (dengan bidang khusus tambahan) anotasi dan tabel gabungan yang didefinisikan dalam anotasi banyak ke banyak.

Definisi pemetaan dalam dua entitas dengan hubungan banyak-ke-banyak langsung tampaknya menghasilkan pembuatan tabel bergabung otomatis menggunakan anotasi 'joinTable'. Namun tabel bergabung sudah ditentukan oleh anotasi di kelas entitas yang mendasarinya dan saya ingin menggunakan definisi bidang kelas entitas asosiasi ini sendiri untuk memperluas tabel bergabung dengan bidang kustom tambahan.

Penjelasan dan solusinya adalah yang diidentifikasi oleh FMaz008 di atas. Dalam situasi saya, itu berkat pos ini di forum ' Pertanyaan Anotasi Doktrin '. Posting ini menarik perhatian pada dokumentasi Doktrin mengenai hubungan ManyToMany Uni-directional . Lihatlah catatan mengenai pendekatan menggunakan 'kelas entitas asosiasi' sehingga menggantikan pemetaan anotasi banyak-ke-banyak secara langsung antara dua kelas entitas utama dengan anotasi satu-ke-banyak di kelas entitas utama dan dua 'banyak-ke-banyak -satu penjelasan di kelas entitas asosiatif. Ada contoh yang disediakan dalam forum ini yang mengaitkan model Asosiasi dengan bidang tambahan :

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;

}
Di toko
sumber
3

Ini contoh yang sangat berguna. Tidak ada dalam doktrin dokumentasi 2.

Terima kasih banyak

Untuk fungsi proxy dapat dilakukan:

class AlbumTrack extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {} 
}

class TrackAlbum extends AlbumTrackAbstract {
   ... proxy method.
   function getTitle() {}
}

class AlbumTrackAbstract {
   private $id;
   ....
}

dan

/** @OneToMany(targetEntity="TrackAlbum", mappedBy="album") */
protected $tracklist;

/** @OneToMany(targetEntity="AlbumTrack", mappedBy="track") */
protected $albumsFeaturingThisTrack;
Anthony
sumber
3

Yang Anda maksud adalah metadata, data tentang data. Saya memiliki masalah yang sama untuk proyek yang sedang saya kerjakan dan harus meluangkan waktu untuk mencari tahu. Terlalu banyak informasi untuk dikirim di sini, tetapi di bawah ini ada dua tautan yang mungkin berguna bagi Anda. Mereka merujuk kerangka kerja Symfony, tetapi didasarkan pada ORM Doktrin.

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/

Semoga sukses, dan referensi Metallica yang bagus!

Mike Purcell
sumber
3

Solusinya ada dalam dokumentasi Doktrin. Di FAQ Anda dapat melihat ini:

http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table

Dan tutorialnya ada di sini:

http://docs.doctrine-project.org/en/2.1/tutorials/composite-primary-keys.html

Jadi Anda tidak lagi melakukan manyToManytetapi Anda harus membuat Entitas tambahan dan menempatkan manyToOneke dua entitas Anda.

ADD untuk @ f00bar komentar:

itu sederhana, Anda hanya perlu melakukan sesuatu seperti ini:

Article  1--N  ArticleTag  N--1  Tag

Jadi, Anda membuat entitas 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

Saya harap ini membantu

Mirza Selimovic
sumber
1
Tautan yang diperbarui: 1 - FAQ , 2 - Tutorial: Kunci Utama Komposit
m14t
Itulah yang saya cari, terima kasih! Sayangnya, tidak ada contoh yml untuk kasus penggunaan ketiga! :(Adakah yang bisa berbagi contoh kasus penggunaan ketiga menggunakan format yml? Saya akan benar-benar mencari tahu:#
Stphane
Saya telah menambahkan jawaban pada kasus Anda;)
Mirza Selimovic
Itu salah. Entitas tidak harus dengan id (id) AUTO. Itu salah, saya mencoba membuat contoh yang benar
Gatunox
saya akan memposting jawaban baru untuk mendapatkan jika diformat dengan benar
Gatunox
3

Searah. Cukup tambahkan inversedBy: (Nama Kolom Asing) untuk menjadikannya Dua Arah.

# 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

Saya harap ini membantu. Sampai jumpa.

Gatunox
sumber
2

Anda mungkin dapat mencapai apa yang Anda inginkan dengan Class Table Inheritance di mana Anda mengubah AlbumTrackReference ke AlbumTrack:

class AlbumTrack extends Track { /* ... */ }

Dan getTrackList()akan berisi AlbumTrackobjek yang bisa Anda gunakan seperti yang Anda inginkan:

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

Anda perlu memeriksa ini secara menyeluruh untuk memastikan Anda tidak menderita dari segi kinerja.

Pengaturan Anda saat ini sederhana, efisien, dan mudah dimengerti bahkan jika beberapa semantik tidak cocok dengan Anda.

rojoca
sumber
0

Saat mendapatkan semua trek album dalam kelas album, Anda akan menghasilkan satu permintaan lagi untuk satu catatan lagi. Itu karena metode proxy. Ada contoh lain dari kode saya (lihat posting terakhir dalam topik): http://groups.google.com/group/doctrine-user/browse_thread/thread/d1d87c96052e76f7/436b896e83c10868#436b896e83c10868

Apakah ada metode lain untuk menyelesaikannya? Bukankah satu orang bergabung dengan solusi yang lebih baik?

quba
sumber
1
Sementara ini secara teoritis dapat menjawab pertanyaan, akan lebih baik untuk memasukkan bagian-bagian penting dari jawaban di sini, dan menyediakan tautan untuk referensi.
Spontifixus
0

Berikut adalah solusinya seperti yang dijelaskan dalam Dokumentasi Doctrine2

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