Serie: Datenbankzugriff mit dem Zend Framework
- Teil 1: App-Namespace Inititalisierung
- Teil 2: Models anlegen und DB-Operationen
- Teil 3: Joins im Zend Framework
- Teil 4: Abstrakte Klasse zum Pimpen des Datenbankzugriffs
Selbstverständlich habt ihr die Erkenntnisse aus dem letzten Artikel dieser Serie ausreichend verinnerlicht und seid nun wissbegierig wie Hölle. Worum geht es im diesen Teil? Joins. Das Prinzip dahinter hatte ich in weiser Voraussicht ja bereits vorgestellt. Das ZF bietet kontinuierlicherweise auch hierfür einen objektorientierten Zugriff. Schauen wir uns das mal genauer an.
Die Ausgangssituation
Ich verwende wieder Katzen und Hüte. Der Kontinuität wegen. Wir haben also 2 Tabellen:
cats
hats
Entsprechend haben wir ZF-seitig 2 Models.
class Senaeh_Model_Cats extends Zend_Db_Table_Abstract
{
protected $_name = 'cats';
protected $_primary = 'cat_id';
}
class Senaeh_Model_Hats extends Zend_Db_Table_Abstract
{
protected $_name = 'hats';
protected $_primary = 'hat_id';
}
Wer nachbauen möchte, hier die SQL-Befehle (sind identisch zu denen aus dem Join-Artikel):
CREATE TABLE cats (
cat_id INT(10) NOT NULL AUTO_INCREMENT,
name VARCHAR(32) NOT NULL DEFAULT '',
PRIMARY KEY (cat_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE hats (
hat_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(32) NOT NULL DEFAULT '',
cat_id INT(11) DEFAULT NULL,
PRIMARY KEY (hat_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO cats (cat_id, name)
VALUES
(1,'Maru'),
(2,'Al Katzone'),
(3,'Ginger Cat'),
(4,'Mauzi');
INSERT INTO hats (hat_id, title, cat_id)
VALUES
(1,'Sombrero',2),
(2,'Papiertüte',1),
(3,'Partyhut',1),
(4,'Strohhut',3),
(5,'Amboss',NULL),
(6,'Toupet',NULL);
Verfügbare Join-Methoden
Im ZF existieren Methoden für alle der im (nun bereits zum dritten Mal zitierten) Join-Artikel vorgestellten Joins:
join($table, $on, $cols)
joinLeft($table, $on, $cols)
joinRight($table, $on, $cols)
Wie sich die cleveren Füchse unter euch vielleicht denken können, steht $table
für die Datenbanktabelle, die gejoined werden soll. Die Bedingung wird in $on
festgehalten und die zu holenden Spalten in $cols
(als string
oder array
).
Leider ist es nicht so einfach wie man denkt
Nehmen wir mal an, unser Cats
-Model bekommt eine Methode namens getWithHats()
, in dem wir – logischerweise – alle Katzen mit ihren Hüten haben möchten. Vermutlich würde man so etwas schreiben wollen:
public function getWithHats()
{
$query = $this->select()
->join('hats', 'cats.cat_id = hats.cat_id', 'title');
return $this->fetchAll($query);
}
Ich weiß nicht, wie es euch geht, aber mir erscheint das der einfachste und intuitivste Weg zu sein. Leider ist es so nicht umgesetzt. Unser Model erbt von Zend_Db_Table_Abstract
. Diese Klasse ist nur für die Verwaltung einer Tabelle gedacht. Von Haus kann diese Klasse also gar nicht gejoined werden.
Die Lösung: setIntegrityCheck(false)
Wir müssen unserem Model explizit erlauben zu joinen. Und das auch noch für jede Anfrage neu. Nicht schön, aber machbar. Und zwar mithilfe der Methode setIntegrityCheck(false)
. Warum? Haut mich nicht, denn so richtig gerafft habe ich es nicht wirklich. Ihr könnt aber gern hier nachlesen (sucht nach setIntegrityCheck) und es mir erklären 😀
Unsere veränderte Abfrage schaut so hier aus:
$query = $this->select()
->setIntegrityCheck(false)
->join('hats', 'cats.cat_id = hats.cat_id', 'title');
Jetzt funktioniert’s aber, oder? Nein, noch nicht. Folgende Abfrage würde generiert:
SELECT hats.title FROM hats
w00t?!
Das Problem: durch setIntegrityCheck(false)
“vergisst” unser Model quasi, dass es den Join auf die aktuelle Tabelle anwenden soll. Müssen wir also dazu schreiben. Zickiges ZF.
$query = $this->select()
->setIntegrityCheck(false)
->from($this, 'name')
->join('hats', 'cats.cat_id = hats.cat_id', 'title');
Nochmal schauen, ob die MySQL-Abfrage jetzt hinhaut:
SELECT cats.name, hats.title
FROM cats
INNER JOIN hats
ON cats.cat_id = hats.cat_id
Bravo. Lessons learned:
setIntegrityCheck(false)
ermöglicht Joins- wir müssen sowohl die gejointe als auch die zu joinende Tabelle definieren (u know? ;))
Optimieren
Es gibt ein paar Sachen, die mir persönlich daran nicht gefallen.
- Jedes Mal aufs neue
setIntegrityCheck(false)
ausschreiben ist nicht besonders DRY-kompatibel. - Auch die vollen Tabellennamen will ich nicht immer ausschreiben. Mich dürstet’s nach Aliasen.
Aliase
Problem Numero 2 ist schnell erledigt. Wir verwenden einfach eine Array-Syntax beim Angeben der Tabellen. Dadurch lässt sich beispielsweise die ON
-Bedingung übersichtlicher notieren.
$query = $this->select()
->setIntegrityCheck(false)
->from(array('c' => $this->getTableName()), 'name')
->join(array('h' => 'hats'), 'c.cat_id = h.cat_id', 'title');
Beachtet hier den notwendigen Aufruf der Methode getTableName()
. Das ZF ist leider nicht klug genug, um einfach nur $this
verarbeiten und den Tabellennamen automatisch erkennen zu können.
SQL dazu:
SELECT c.name, h.title
FROM cats AS c
INNER JOIN hats AS h
ON c.cat_id = h.cat_id
Methode zum automatischen “Aktivieren” von Joins
Wäre da noch das oben zuerst genannte Problem. Wir sollten uns eine Methode schaffen, die uns automatisch Abfrage-Objekte liefert, die sich joinen lassen. Am besten legen wir eine private Methode an.
private function _getSelectWithJoin()
{
return $query = $this->select()
->setIntegrityCheck(false);
}
Der Aufruf erfolgt dann beispielsweise so hier:
$query = $this->_getSelectWithJoin()
->from(array('c' => $this->getTableName()), 'name')
->join(array('h' => 'hats'), 'c.cat_id = h.cat_id', 'title');
Man könnte noch ein wenig Spielerei betreiben, um das Hinzufügen des FROM
-Abschnitts zu vereinfachen. Das Austoben überlass ich dabei euch 😉
Noch ein bisschen einfacher geht es mit der joinUsing
-Methode:
$query = $this->_getSelectWithJoin()
->from($this, 'name')
->joinUsing('hats', 'cat_id', 'title');
Die Abfrage bleibt dieselbe, die Notation ist etwas kürzer und dadurch übersichtlicher.
Fazit
Joins mit dem Zend-Framework funktionieren. Natürlich tun sie das 😀 (wenn auch manchmal etwas suboptimal und unnötig umständlich). Ich habe zwar nur den “normalen” INNER JOIN
verwendet, das Prinzip sollte aber klar geworden sein. Die Anwendung der anderen Joins ist – natürlich bis auf die Änderung des Methodennamens – identisch.
Was erwartet euch im nächsten Teil? Die Erstellung eines Abstrakten Models, von denen alle anderen erben. Darin werden wir unter anderem die _getSelectWithJoin()
-Methode auslagern. Und noch ein paar andere Spielereien. Seid gespannt 😉
Bei Fragen, Fehlern, Anregungen etc. freue ich mich über Kommentare.