Aug 02

Achtung:
Dieses Turorial bezieht sich auf eine veraltete Version von Doctrine und funktioniert vermutlich mit den neueren 1.x und späteren 2.x Releasen nicht mehr.

Nachdem im letzten Beitrag das Thema 1:n Beziehungen beleuchtet haben, wenden wir uns nun der letzten Beziehungsart zu, die man mit einer Datenbankrelation abbilden kann, nämlich der n:m Beziehung.
Als Basis dazu nehmen wir die Klassen und Datenbanktabellen, die wir in den letzten beiden Artikeln erstellt haben und tauchen nun wieder in die Anwendung ein. Wir möchten nun festhalten, dass die verschiedenen Variationen (Light, Zero) eines Produkts (Coca Cola) in verschiedenen Handelsunternehmen (POS = Point of Sale) verkauft werden.
Dies lässt sich ja auf Datenbankebene mit einer Verknüpfungstabelle darstellen, die die beiden Fremdschlüssel von den Jeweiligen Entitäten enthält und diese so mit einander verknüpft. Auf diese Weise können beliebige Kombinationen in beliebiger Anzahl dargestellt werden. Als Schutz vor Doppeleinträgen machen wir aus dem Datensatz in der Verknüpfungstabelle einen kombinierten Primärschlüssel, sodass hier Datenbankseitig bereits vor Doppeleinträgen geschützt wird. Doctrine reagiert beim Versuch einen Doppeleintrag anzulegen mit einer Exception, die von PDO geworfen wird.

Steigen wir aber nun in die Entwicklung der Doctrine Klassen ein.Legen wir nun zunächst die Klasse für unseren Point of Sale an. Das ist eine einfache Doctrine-Klasse, wie wir sie so kennen. Eine Besonderheit dabei ist der neue Relationstyp:

1
2
3
         $this->hasMany('Data_Db_ProductsVariations', array('local' => 'pos_id',
                                    'foreign' => 'products_variations_id',
                                    'refClass' => 'Data_Db_ProductsVariations2pos'));

Hier sehen wir die Besonderheit der Relation. Wir geben also nicht direkt den Schlüssel der fremden Tabelle an (in diesem Falle wäre es auch einfach nur id) sondern den Namen des Fremdschlüssels, wie er in der Verknüpfungstabelle heißt. Ebenso geben wir für die eigene Tabelle nicht die id an, sondern auch den Namen des Schlüssels, wie er in der Verknüpfungstabelle lautet. Zu guterletzt benennen wir die Verknüpfungsklasse noch über refClass. Im Ganzen sieht die Klasse dann so aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class
	Data_Db_Pos
extends
	Doctrine_Record
{
 
	public function setTableDefinition
	(
	)
	{
		$this->setTableName('pos');
 
        $this->hasColumn('id', 'integer', 4, array('notnull' => true,
                                                   'primary' => true,
                                                   'unsigned' > true,
                                                   'autoincrement' => true));
		$this->hasColumn('name' , 'string' , 255 , array ('notnull' => true));
		$this->hasColumn('description' , 'string' , 3000 , array ('notnull' => true));
		$this->hasColumn('date_created' , 'timestamp' , null);
	}
 
	public function setUp
	(
	)
	{
        $this->hasMany('Data_Db_ProductsVariations', array('local' => 'pos_id',
                                    'foreign' => 'products_variations_id',
                                    'refClass' => 'Data_Db_ProductsVariations2pos'));
	}
 
}

Um die Verknüpfung nicht zu einseitig zu gestalten, fügen wir in die Data_Db_ProductsVariations noch die Gegenverknüpfung ein:

1
2
3
        $this->hasMany('Data_Db_Pos', array('local' => 'products_variations_id',
                                    'foreign' => 'pos_id',
                                    'refClass' => 'Data_Db_ProductsVariations2pos'));

Nun haben wir Anwendungsseitig schonmal die halbe Miete erledigt, jetzt legen wir zunächst mal die Tabelle in der Datenbank an. Dazu verwenden wir folgenden SQL-Query:

1
2
3
4
5
6
7
CREATE TABLE `pos` (
`id` BIGINT( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT ,
`name` VARCHAR( 255 ) COLLATE latin1_general_ci NOT NULL ,
`description` tinytext COLLATE latin1_general_ci NOT NULL ,
`date_created` datetime NOT NULL ,
PRIMARY KEY ( `id` )
) ENGINE = MYISAM DEFAULT CHARSET = latin1 COLLATE = latin1_general_ci;

Jetzt müssen wir als nächstes, bevor wir mit den Daten arbeiten können, die Verknüpfungstabelle realisieren. Dazu legen wir eine “normale” Doctrine-Klasse an, die komplett ohne Relationen auskommt, da die Verknüpfungen über die Entitäten selbst realisiert werden. Die Klasse und somit auch die Tabelle, besteht also auch nur aus zwei Spalten und sieht folgendermaßen aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class
	Data_Db_ProductsVariations2pos
extends
	Doctrine_Record
{
 
	public function setTableDefinition
	(
	)
	{
		$this->setTableName('products_variations2pos');
 
        $this->hasColumn('products_variations_id', 'integer', 4, array('notnull' => true,
                                                   'primary' => true,
                                                   'unsigned' > true));
        $this->hasColumn('pos_id', 'integer', 4, array('notnull' => true,
                                                   'primary' => true,
                                                   'unsigned' > true));
	}
}

Via Sql legen wir dann noch die Tabelle in der Datenbank an. Anschließend sind wir startklar und können Daten einfügen und auslesen:

1
2
3
4
5
CREATE TABLE `products_variations2pos` (
`products_variations_id` BIGINT UNSIGNED NOT NULL ,
`pos_id` BIGINT UNSIGNED NOT NULL ,
PRIMARY KEY ( `products_variations_id` , `pos_id` )
) ENGINE = MYISAM ;

Jetzt wollen wir erstmal ein Produkt auslesen, damit wir weiter arbeiten können, dies erledigen wir per Finder. Wir nehmen das Beispiel, dass wir Coca Cola mit der ID 1 eingefügt haben:

1
$Product = $Connection->getTable ( 'Data_Db_Products' )->find ( 1 );

Jetzt haben wir ein Produkt im Speicher mit dem wir weiter arbeiten können. Nun wollen wir zunächst zwei POS einfügen. Dazu dient uns folgender Codeschnipsel:

1
2
3
4
5
6
7
8
9
10
11
$Aldi = new Data_Db_Pos;
$Aldi["name"] = 'Aldi Sued';
$Aldi["description"] = 'Aldi im Sueden und Westen Deutschlands';
$Aldi[ 'date_created' ] = date('Y-m-d');
$Aldi->save();
 
$Lidl = new Data_Db_Pos;
$Lidl["name"] = 'Lidl';
$Lidl["description"] = 'Lidl Stiftung Deutschland';
$Lidl[ 'date_created' ] = date('Y-m-d');
$Lidl->save();

Jetzt greifen wir über das Produkt-Objekt zunächst auf die Variationen des Produkts zu. Hier gehen wir mal davon aus, dass wir wissen, an welcher Stelle die beiden Datensätze stehen und das sie vorhanden sind. Zum Einfügen eines POS zu einer Variation gibt es wieder zwei Möglichkeiten. Die eine über die lose Array-Zuweisung, welche (wie beim letzten Mal erläutert) Performance-Einbußen mit sich bringen kann:

1
2
3
4
$Light = $Product->Data_Db_ProductsVariations[0];
$Light->Data_Db_Pos[] = $Lidl;
$Light->Data_Db_Pos[] = $Aldi;
$Light->save();

Jetzt haben wir die Variante Light zu Lidl und Aldi hinzugefügt. Nehmen wir nun Variante B und fügen Zero zu Lidl hinzu:

1
2
3
4
5
$Zero = $Product->Data_Db_ProductsVariations[1];
$Pv2Pos = new Data_Db_ProductsVariations2pos;
$Pv2Pos [ 'products_variations_id' ] = $Zero [ 'id' ];
$Pv2Pos [ 'pos_id' ] = $Lidl [ 'id' ];
$Pv2Pos->save();

Dies sieht zwar nicht ganz so cool aus, ist aber im Endeffekt performanter. Da die wenigsten Leute sich beim Benutzen der Anwendung für den coolen Source interessieren, sondern eher für die Performance, würde ich Variante B vorschlagen :-) .

Jetzt haben wir genug Daten in der Datenbank, sodass wir sie nun mit einem Query auslesen können:

1
2
3
4
// Auslesen (Alle Produkte-Variationen von Lidl)
$Query = new Doctrine_Query;
$Query->from ( 'Data_Db_ProductsVariations pv, pv.Data_Db_Pos p' );
$Query->where ( 'p.id = ' . (int)$Lidl [ 'id' ] );

Wie man erkennen kann ist die Verknüpfungstabelle transparent. Doctrine weiß selbst durch die Definition welche Verknüpfungen verwendet werden müssen und macht solchen Query daraus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT
    p.id AS
    p__id, p.products_id AS
    p__products_id, p.name AS
    p__name, p.date_created AS
    p__date_created, p2.id AS
    p2__id, p2.name AS
    p2__name, p2.description AS
    p2__description, p2.date_created AS
    p2__date_created FROM
    products_variations p
    LEFT JOIN products_variations2pos p3 ON p.id = p3.products_variations_id
    LEFT JOIN pos p2 ON p2.id = p3.pos_id
    WHERE p2.id = 2

Wenn wir diesen Query nun ausführen und dann durchwandern, sehen wir, dass wir Erfolg hatten. Wir bekommen die beiden hinzugefügten Varianten, die wir zum POS Lidl hinzugefügt haben. Diese sind natürlich unsortiert, aber das kann man nachholen bzw. vorher durch ein orderBy realisieren, wenn man es möchte :-) :

1
2
3
4
5
6
7
8
9
10
$Result = $Query->execute();
 
foreach
(
	$Result as $ProductVariation
)
{
	echo $ProductVariation->Data_Db_Products["name"]." -> " . $ProductVariation["name"];
	echo "<br />";
}

Folgendes Ergebnis können wir betrachten:

Coca Cola -> Light
Coca Cola -> Zero

Über die Objektschnittstellen von Doctrine können wir nun auch vollständig auf die Daten des eigentlichen Produkts zugreifen. Doctrine läd diese Daten dann on-Demand nach.

Nun haben wir 1:1, 1:n und n:m Beziehungen beleuchtet, das gibt uns die Möglichkeit vieles zu realisieren. Im nächsten Beitrag werde ich auf ein weiteres tolles Feature von Doctrine eingehen, die Eventlistener. :-) .
Kommentare sind gern gesehen, Fragen natürlich auch.

Post to Twitter Post to Delicious Post to Digg Post to Facebook

written by Alexander \\ tags: , , , ,


One Response to “Enterprise: ORM in PHP mit Doctrine #3”

  1. 1. Alexander Stelter Says:

    Nachdem wir uns im letzten Beitrag nochmal mit dem Beziehungen der Datenbanktabellen und Klassen beschäftigt haben, wollen uns heute einem weiteren Kapitel widmen, den Event-Listenern.

    Sie sind ein einfaches Instrument, die auch bei vielen DBMS durch T

Leave a Reply

i3Theme sponsored by Top 10 Web Hosting and Hosting in Colombia