senäh

17senäh und so…

Zend Framework Artikelbild

Zend Framework
30. Jul 2012
Kommentare: 0

Joins im Zend Framework

Kategorien: Zend Framework | 30. Jul 2012 | Kommentare: 0

Serie: Datenbankzugriff mit dem Zend Framework

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:

  1. cats
  2. 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:

  1. join($table, $on, $cols)
  2. joinLeft($table, $on, $cols)
  3. 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.

  1. Jedes Mal aufs neue setIntegrityCheck(false) ausschreiben ist nicht besonders DRY-kompatibel.
  2. 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.

Autor: Enno

Ich bin Enno. PHP ist mein Ding, aber auch alles Neue rund um die Themen HTML5, CSS3 & Co finde ich interessant. Ich mag es Leuten zu helfen und mein Wissen weiterzugeben. Sollte dir mein Beitrag gefallen haben, lass doch nen Kommentar da oder benutze einen der Social Buttons, um deinen Dank auszudrücken ;)