Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

06 Oktober, 2007

Modell Relationships

TL;DR:

Die meisten Datenbankabfragen beziehen mehrere Tabellen mit dem JOIN-Befehl ein. Picora bietet hier die Möglichkeit von Relationships die auch von Ruby On Rails bekannt sind. Sind die Tabellen im Modell ein mal verknüpft, gestalten sich die Datenbankabfragen viel einfacher und übersichtlicher.

Picora unterstützt momentan SQLite und MySQL nativ. Für alle anderen Datenbanken muss die PDO-Abstraktionsschicht herhalten. Es empfiehlt sich also, MySQL oder SQLite einzusetzen, da diese am wenigsten Overhead mitbringen.

Ein einfaches Modell

Zur Demonstration der verschiedenen Beziehungen verwenden wir folgendes Schema. Ich habe mich dabei an der Chen-Notation orientiert aber die Beziehungs-Rauten zwecks Übersichtlichkeit weggelassen, man möge mir verzeihen. Wem das Datenbankschema auf den ersten Blick nichts sagt, sollte sich zuerst etwas mit Datenbanken im Allgemeinen beschäftigen. Auf ONLamp.com gibt es dazu einen guten MySQL-Crashkurs.

image

Wir haben einen Blogeintrag, welcher mehrere Kommentare besitzen kann. Ein Kommentar kann immer nur einem Blogeintrag gehören. Der Blogeintrag wird von einem Autor geschrieben, welcher aber selbst mehrere Einträge verfassen kann. Jedem Blogeintrag können mehrere Kategorien zugewiesen werden und jede Kategorie enthält mehrere Blogeinträge. Für die n:m-Beziehung benötigen wir auf Datenbankseite nicht eine Zwischentabelle, die die Zuordnungen festhält.

1:1 oder Has-One Beziehung

Beginnen wir mit der einfachen Beziehung zwischen BlogPost und Author. Die Tabelle blogpost enthält den Fremdschlüssel author_id bereit, auf der Gegenseite hat die Tabelle author den Primärschlüssel id. Um nun diese beiden Tabellen miteinander zu verschalten, legen wir die Models folgendermaßen an.

PicoraActiveRecord::addRelationship(
    'BlogPost', 'has_one', 'Author', 'id');
class BlogPost extends PicoraActiveRecord {
    const TABLE_NAME = 'blogposts';
}
class Author extends PicoraActiveRecord {
    const TABLE_NAME = 'authors';
}

Zuerst müssen wir dem BlogPost-Model mitteilen, das zu jedem Blogeintrag ein Autor gehört. Dies machen wir über das Schlüsselwort has_one mit der statischen Methode PicoraActiveRecord::addRelationship() außerhalb der Klasse. Man kann diese Verschaltung auch in der config.php vornehmen, ich ziehe es allerdings vor, dies so nah wie möglich am Model zu definieren.

Die Parameter von addRelationship() sind der Reihe nach:

Für den Relationstyp has_and_belongs_to_many gibt es noch weitere Parameter, die weiter unten beschrieben werden.

Um nun von einer BlogPost-Instanz auf den Autor zugreifen zu können, verwenden wir die bereitgestellten get-Methoden. Hier ein Beispiel:

$post = BlogPost::findByField('BlogPost', 'id', 1);
$authorName = $post->getAuthor()->name;

Von getAuthor() bekommen wir eine neue Author-Instanz zurück, aus dem wir dann über das Attribut name auf die gleichnamige Spalte zugreifen können.

1:n oder Has-Many Beziehung

Betrachten wir die andere Seite. Ein Autor verfasst mehrere Blogeinträge. Um dies dementsprechend zu verknoten, fügen wir folgende Zeile über die Author-Klasse:

PicoraActiveRecord::addRelationship(
    'Author', 'has_many', 'BlogPost', 'author_id');

Der vierte Parameter mit dem Wert author_id definiert den Fremdschlüssel in der blogpost-Tabelle, welcher für die Relation verwendet werden soll. Mit dieser Konstruktion können wir nun folgende Abfragen machen:

$author = BlogPost::findByField('Author', 'id', 1);
$numPosts = $author->getBlogPostCount();
$allPosts = $author->getBlogPostList();
$firstTitle = $allPosts[0]->title;

Zuerst holen wir uns wieder eine Instanz vom Author-Model. Dieses können wir nun mit der Methode getBlogPostCount() nach der Anzahl der geschriebenen Blogeinträge fragen. Als Antwort erhalten wir einen Integer-Wert. Mit der bereitgestellten Methode getBlogPostList() können wir eine Liste von allen Blogeinträgen von diesem Autor bolen. Offensichtlich erhalten wir hier ein Array von Post-Instanzen.

Belongs-To

Betrachten wir die Beziehung der Tabelle comments nach blogpost. Ein Kommentar gehört ausschließlich zu einem Blogeintrag. Diesen Zustand können wir mit belongs_to modellieren:

PicoraActiveRecord::addRelationship(
    'Comment', 'belongs_to', 'BlogPost', 'blogpost_id');

Auch hier können wir mit einer Comment-Instanz über die Methode getBlogPost() direkt auf die BlogPost-Instanz zugreifen, die dem Kommentar zugeordnet ist.

n:m oder Has-And-Belongs-To-Many Beziehung

Eine etwas kniffligere Situation haben wir zwischen Blogeinträgen und Kategorien. Ein Blogeintrag kann mehreren Kategorien zugeordnet sein. Eine Kategorie kann mehrere Blogeinträge enthalten. Wer etwas Erfahrung mit Datenbankdesign hat, wird schnell feststellen, das hier eine Zwischentabelle benötigt wird.

CREATE TABLE `categories_blogposts` (
  `id` int(6) NOT NULL auto_increment,
  `blogpost_id` int(4) NOT NULL,
  `category_id` int(3) NOT NULL,
  PRIMARY KEY  (`id`)
);

Die Zwischentabelle speichert die Beziehungen von Blogeinträgen und Kategorien. Um dies auf beiden Seiten in ActiveRecords umzusetzen, müssen wir der Methode addRelationship() ein paar Parameter mehr mitgeben.

PicoraActiveRecord::addRelationship(
    'BlogPost', 'has_and_belongs_to_many', 'Category',
    'categories_blogposts', 'blogpost_id', 'id');
PicoraActiveRecord::addRelationship(
    'Category', 'has_and_belongs_to_many', 'BlogPost',
    'categories_blogposts', 'category_id', 'id');

Die ersten drei Parameter sind die üblichen, wie wir sie von den anderen Beispielen her kennen. Da wir aber über eine Zwischentabelle gehen, müssen wir noch den Namen der Zwischentabelle, den Fremdschlüssel für die ausgehende Tabelle und den Primärschlüssel der Tabelle am anderen Ende angeben. In der Beziehung BlogPost -> Category heißt das also, das die Zwischentabelle categories_blogposts heißt, der Fremdschlüssel in category_blogposts blogpost_id heißt und der Primärschlüssel von Category id heißt.

Jetzt können wir abfragen machen:

$post = BlogPost::findByField('BlogPost', 'id', 1);
$categories = $post->getCategoryList();
$postInCategory = $categories[0]->getBlogPostList();

Wieder holen wir uns eine BlogPost-Instanz. Da diese nun die Kategorieliste kennt, können wir mit getCategoryList() eine Liste (Array) mit Category-Instanzen holen, in denen dieser Blogeintrag enthalten ist. Von der anderen Seite können wir uns eine Kategorie herausnehmen und diese nach seinem Inhalt fragen, sprich nach allen Blogeinträgen - die in der Kategorie enthalten sind - suchen.

Die Kehrseite

PicoraActiveRecords erleichtern die Datenbankabfragen enorm und folgen einem sauberen objektorientierten Ansatz. Wenn die Tabellen erst einmal richtig verschaltet sind, funktioniert alles einfacher als man anfangs denkt.

Doch haben ActiveRecords auch Nachteile: Der Programmierer kann nicht direkt in die SQL-Abfragen eingreifen. die PicoraActiveRecotd-Klasse verbindet alle Tabellen immer mit einem LEFT JOIN, optimierungen oder Veränderungen sind hier nicht drin. Wer lieber SQL von Hand schreibt, muss auf die Methode executeQuery() zurückgreifen und komplett auf das Objektdesign verzichten.

Viele Datenbanksysteme bieten ausserhalb der von Picora bereitgestellten Abfragebefehlen weitere tolle Features wie Views, Namespaces, usw. Diese Features müssen auf Grund der Kompatibilität ignoriert werden.

Was noch?

In diesem Artikel habe ich nur SELECT-Abfragen durchgeführt. INSERT-, UPDATE- oder DELETE-Anweisungen funktionieren natürlich auch mit PicoraActiveRecords. Die Klasse stellt hier einige Methoden bereit, um bspw. einem Blogeintrag eine neue Kategorie hunzuzufügen oder den Autor des Blogeintrages zu tauschen. Wie dies geht, werde ich in einem kommenden Artikel beschreiben.