0 follower

Relationale ActiveRecords

Wir haben schon gesehen, wie wir ActiveRecord (AR) nutzen können, um Daten einer einzelnen Tabelle auszulesen. In diesem Abschnitt beschreiben wir, wie wir mit AR mehrere relationale Datentabellen zusammenführen, um verbundene Datensätze zurückzuerhalten.

Um mit relationalen AR zu arbeiten, müssen zwischen den zu verbindenden Tabellen eindeutige Fremdschlüsselbeziehungen definiert worden sein. AR stützt sich auf die Metadaten dieser Beziehungen, um zu ermitteln, wie die Tabellen verbunden werden sollen.

Hinweis: Ab Version 1.0.1 können Sie relationale AR auch verwenden, wenn Sie keine Fremdschlüssel-Constraints in ihrer Datenbank definiert haben.

Der Einfachheit halber, verwenden wir zur Veranschaulichung der Beispiel in diesem Abschnitt das folgende ER-Diagramm (engl.: entity relationship, Gegenstands-Beziehungs-Modell).

ER-Diagramm

ER-Diagramm

Info: DBMS unterscheiden sich in ihrer jeweiligen Unterstützung von Fremdschlüssel-Constraints.

SQLite unterstützt keine Fremschlüssel-Constraints, aber Sie können beim Erstellen von Tabellen trotzdem Constraints festlegen. AR kann sich diese Angaben zunutze machen, um relationale Abfragen in richtiger Weise zu unterstützen.

MySQL unterstützt Fremdschlüssel-Constraints mit der InnoDB-Engine, aber nicht mit MyISAM. Es wird deshalb empfohlen, dass Sie InnoDB für MySQL-Datenbanken benutzen. Falls Sie MyISAM verwenden, können Sie relationale Abfragen mit AR mit folgendem Trick durchführen:

CREATE TABLE Foo
(
  id INTEGER NOT NULL PRIMARY KEY
);
CREATE TABLE bar
(
  id INTEGER NOT NULL PRIMARY KEY,
  fooID INTEGER
     COMMENT 'CONSTRAINT FOREIGN KEY (fooID) REFERENCES Foo(id)'
);

Hier verwenden wir das Schlüsselwort COMMENT, um die Fremdschlüssel-Constraints zu beschreiben. AR kann diese Informationen auslesen, um die Beziehung zu erkennen.

1. Festlegen der Beziehungen

Bevor wir relationale Abfragen mit AR durchführen können, müssen wir AR bekannt geben, wie eine AR-Klasse mit einer anderen in Beziehung steht.

Die Beziehung zwischen zwei AR-Klassen steht in direktem Zusammenhang mit der Beziehung zwischen den Datenbanktabellen, die die AR-Klassen repräsentieren. Aus Sicht der Datenbank gibt es drei Beziehungstypen zwischen zwei Tabellen A und B: eins-zu-viele (engl.: one-to-many, 1:n, z.B. zwischen User und Post), eins-zu-eins (engl.: one-to-one, 1:1, z.B. zwischen User und Profile) und viele-zu-viele (engl.: many-to-many, n:m, z.B. zwischen Category und Post):

  • BELONGS_TO (gehört zu): Wenn die Beziehung zwischen den Tabellen A und B eins-zu-viele ist, dann gehört B zu A (z.B. Post gehört zu User).

  • HAS_MANY (hat viele): Wenn die Beziehung zwischen der Tabelle A und B eins-zu-viele ist, dann hat A viele B (z.B. User hat viele Post).

  • HAS_ONE (hat ein): Dies ist ein Spezialfall von HAS_MANY, wobei A höchstens ein B hat (z.B. User hat höchstens ein Profile).

  • MANY_MANY (viele viele): Dies entspricht der viele-zu-viele-Beziehung (n:m-Beziehung) bei Datenbanken. Eine Verbindungstabelle wird benötigt, um die viele-zu-viele Beziehung auf eins-zu-viele-Beziehungen herunterzubrechen, da die meisten DBMS viele-zu-viele-Beziehungen nicht direkt unterstützen. In unserem Schema dient PostCategory diesem Zweck. In AR-Terminologie können wir MANY_MANY als eine Kombination von BELONGS_TO und HAS_MANY erklären. Beispielsweise gehört Post zu vielen Category und Category hat viele Post.

Das Festlegen der Beziehungen geschieht in AR durch Überschreiben der relations()-Methode von CActiveRecord. Die Methode gibt ein Array der Beziehungsstruktur zurück. Jedes Arrayelement repräsentiert eine einzelne Beziehung im folgenden Format:

'VarName'=>array('RelationsTyp', 'KlassenName', 'FremdSchlüssel', ...Zusätzliche Optionen)

wobei VarName der Name der Beziehung ist. RelationsTyp spezifiziert den Typ der Beziehung und kann eine dieser vier Konstanten sein: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY und self::MANY_MANY. KlassenName ist der Name der AR-Klasse, die zu dieser in Beziehung steht. Und FremdSchlüssel gibt den/die an der Beziehung beteiligten Fremdschlüssel an. Am Ende können zusätzliche Optionen für jede Beziehung angegeben werden (was später beschrieben wird).

Der folgende Code zeigt, wie wir die Beziehung für die User- und Post-Klasse angeben.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
            'categories'=>array(self::MANY_MANY, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}
 
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Info: Ein Fremdschlüssel kann auch ein kombinierter Schlüssel sein, also aus zwei oder mehr Attributen bestehen. In diesem Fall sollten wir die Namen der Fremdschlüssel verketten und durch ein Komma oder Leerzeichen trennen. Beim MANY_MANY Beziehungstyp muss der Name der Verbindungstabelle beim FremdSchlüssel ebenfalls angegeben werden. Zum Beispiel ist der Fremdschlüssel für die categories-Beziehung in Post mit PostCategory(postID, categoryID) angegeben.

Das Definieren von Beziehungen in einer AR-Klasse fügt dieser implizit eine Eigenschaft für jede Beziehung hinzu. Nachdem eine relationale Abfrage ausgeführt wurde, ist die entsprechende Eigenschaft mit der/den verbundenen AR Instanz(en) befüllt. Steht $author bespielsweise für eine AR-Instanz User, so können wir mit $author->posts auf die verbundenen Post-Instanzen zugreifen.

2. Ausführen von relationalen Abfragen

Die einfachste Art, relationale Abfragen auszuführen, ist, eine Verbundeigenschaft einer AR-Instanz zu lesen. Falls auf die Eigenschaft noch nicht zugegriffen wurde, wird eine relationale Abfrage ausgeführt, die die beiden betroffenen Tabellen kombiniert und nach dem Primärschlüssel der aktuellen AR-Instanz filtert. Das Abfrageergebnis wird in der Eigenschaft als Instanz(en) der verbundenen AR-Klasse gespeichert. Dies ist unter dem Namen lazy loading-Methode (träges Nachladen) bekannt, d.h. die relationale Abfrage wird erst beim ersten Zugriff auf die Verbundobjekte durchgeführt. Das folgende Beispiel zeigt, wie man dieses Konzept einsetzen kann:

// Frage den Beitrag mit der ID 10 ab
$post=Post::model()->findByPk(10);
// Frage den Autor des Beitrags ab: Hier wird eine relationale Abfrage durchgeführt
$author=$post->author;

Info: Wenn es keine verbundene Instanz für die Beziehung gibt, kann die entsprechende Eigenschaft null oder ein leeres Array sein. Für die BELONGS_TO und HAS_ONE Beziehungen ist das Ergebnis null, für HAS_MANY und MANY_MANY ein leerer Array.

Die lazy loading-Methode ist sehr bequem einzusetzen, aber in einigen Szenarien nicht sehr effizient. Wenn wir beispielsweise mit der lazy loading-Methode auf die author-Information von N Posts zugreifen wollen, müssen N relationale Abfragen durchgeführt werden. Unter diesen Umständen sollten wir auf die so genannte eager loading-Methode (begieriges Laden) zurückgreifen.

Das eager loading-Konzept fragt die verbundenen AR Instanzen zusammen mit der AR Hauptinstanz ab. Das wird in AR mit der with()-Methode zusammen mit der find- oder findAll-Methode durchgeführt. Zum Beispiel:

$posts=Post::model()->with('author')->findAll();

Der obige Code liefert ein Array von Post-Instanzen. Anders als beim lazy loading-Ansatz ist die author-Eigenschaft in jeder Post-Instanz schon mit der verbundenen User-Instanz befüllt, bevor wir auf die Eigenschaft zugreifen. Anstatt für jeden Post eine relationale Abfrage durchzuführen, liefert der eager loading-Ansatz mit einer einzigen JOIN-Abfrage alle Beiträge zusammen mit ihren Autoren.

Wir können mehrere Namen von Beziehungen in der with()-Methode angeben und mit der eager loading-Methode alle in einem Zug zurückerhalten. Beispielsweise liefert der folgende Code Beiträge zusammen mit ihren Autoren und Kategorien zurück:

$posts=Post::model()->with('author','categories')->findAll();

Wir können auch verschachteltes eager loading ausführen. Anstatt einer Liste von Beziehungsnamen, können wir der with()-Methode eine hierarchische Darstellung wie folgt mitgeben:

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

Das obige Beispiel liefert alle Beiträge zusammen mit ihrem Autor und den Kategorien zurück. Zusätzlich wird auch das Profil sowie alle Beiträge des jeweiligen Autors zurückgegeben.

Hinweis: Die Verwendung der with()-Methode hat sich ab Version 1.0.2 geändert. Bitte lesen Sie die API-Dokumentation sorgfältig durch.

Die AR-Implementierung von Yii ist sehr effektiv. Beim eager loading einer Hierarchie von N HAS_MANY- oder MANY_MANY-Beziehungen sind N+1 SQL Abfragen erforderlich, um das Ergebnis zu erhalten. Das bedeutet, dass im letzten Beispiel 3 SQL Abragen für die posts- und categories-Eigenschaften ausgeführt werden müssen. Andere Frameworks verfolgen eine fundamentalere Herangehensweise, indem sie nur eine SQL Anweisung benutzen. Auf den ersten Blick ist dieses fundamentalere Verfahren effizienter, da weniger Anfragen vom DBMS analysiert und ausgeführt werden müssen. In Wahrheit ist es aber aus zwei Gründen tatsächlich unpraktisch. Erstens enthält das Ergebnis viele, sich wiederholende Datenspalten, für deren Übermittlung und Bearbeitung zusätzliche Zeit benötigt wird. Zweitens wächst mit der Anzahl der beteiligten Tabellen die Anzahl der Spalten exponentiell und das macht es unübersichtlicher, wenn mehrere Beziehungen daran beteiligt sind.

Seit Version 1.0.2 können sie auch erzwingen, dass die relationale Abfrage mit genau einer SQL Abfrage durchgeführt wird. Fügen Sie einfach einen together()-Aufruf nach with() an, z.B.

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->together()->findAll();

Die obige Abfrage wird über eine einzelne SQL-Abfrage ausgeführt. Ohne den Aufruf von together würden drei SQL Abfragen benötigt werden: eine für den Verbund der Tabellen Post, User und Profile, eine, die die Tabellen User und Post verbindet und eine weitere die Post, PostCategory und Category zusammenführt.

3. Optionen für relationale Abfragen

Wir haben bereits erwähnt, dass bei Beziehungsdeklarationen zusätzliche Optionen angegeben werden können. Diese Optionen, durch Name-Wert-Paare festgelegt, werden verwendet um die relationale Abfrage individuell anzupassen. Sie sind nachfolgend zusammengestellt.

  • select: Eine Liste der abzufragenden Spalten der betreffenden AR Klasse. Der Standardwert ist '*', d.h. alle Spalten. Die Spaltennamen sollten durch aliasToken eindeutig gekennzeichnet sein, wenn sie in einem Ausdruck auftreten (z.B. COUNT(??.name) AS nameCount).

  • condition: Die WHERE Klausel, standardmäßig leer. Beachten Sie, dass Spaltenreferenzen mit aliasToken eindeutig gekennzeichnet werden müssen (z.B. ??.id=10).

  • params: Die Parameter, die an die erzeugte SQL Abfrage gebunden werden sollen. Diese sollten als ein Array von Namen-Werte Paaren angegeben werden. Diese Option ist seit Version 1.0.3 verfügbar.

  • on: Die ON Klausel. Die hier angegebene Bedingung wird an die JOIN Abfrage mit einem AND Operator angehängt. Die Spaltennamen sollten durch aliasToken eindeutig gekennzeichnet sein (z.B. ??.id=10). Bei MANY_MANY-Beziehungen wird diese Option nicht berücksichtigt. Diese Option ist seit Version 1.0.2 verfügbar.

  • order: Die ORDER BY Klausel, standardmäßig leer. Beachten Sie, dass Spaltenreferenzen mit aliasToken eindeutig gekennzeichnet werden müssen (z.B. ??.age DESC).

  • with: Eine Liste kind-bezogener Objekte, die zusammen mit diesem Objekt geladen werden sollen. Beachten Sie, dass bei falscher Verwendung dieser Option eine endlose Beziehungsschleife entstehen kann.

  • joinType: Jointyp für diesen Beziehungstyp. Der Standardwert ist LEFT OUTER JOIN.

  • aliasToken: Der Platzhalter für die Spaltenpräfix. Er wird durch den entsprechenden Alias der Tabelle ersetzt, um die Spaltenreferenzen zu vereindeutigen. Der Standardwert ist '??.'

  • alias: Der Alias für die in dieser Beziehung verbundene Tabelle. Diese Option ist seit Version 1.0.1 verfügbar. Der Standardwert ist null, das bedeutet, der Tabellenalias wird automatisch erzeugt. Das ist etwas anderes als aliasToken. Im letzterem Falle handelt es sich nur um einen Platzhalter, der vom tatsächlichen Tabellenalias ersetzt wird.

  • group: Die GROUP BY Klausel, standardmäßig leer. Beachten Sie, dass Spaltenreferenzen mit aliasToken eindeutig gekennzeichnet werden müssen (z.B. ??.age).

  • having: Die HAVING Klausel, standarmäßig leer. Beachten Sie, dass Spaltenreferenzen mit aliasToken eindeutig gekennzeichnet werden müssen (z.B. ??.age). Diese Option ist seit Version 1.0.1. verfügbar.

  • index: Der Name der Spalte, deren Wert als Schlüssel für den Array mit relationalen Objekten verwendet werden soll. Wird diese Option nicht gesetzt, wird ein 0-basierter ganzzahliger Index verwendet. Diese Option kann nur für HAS_MANY- und MANY_MANY-Beziehungen gesetzt werden. Diese Option ist seit Version 1.0.7 verfügbar.

Zusätzlich sind für bestimmte Beziehungen folgende Optionen beim lazy loading verfügbar:

  • limit: Begrenzt die auszuwählenden Zeilen. Diese Option ist bei der Beziehung BELONGS_TO NICHT anwendbar.

  • offset: Versatz der auszuwählenden Zeilen. Diese Option ist bei der Beziehung BELONGS_TO NICHT anwendbar.

Nachfolgend modifizieren wir die posts-Beziehung beim User indem wir einige der obigen Optionen einbeziehen:

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID',
                            'order'=>'??.createTime DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Wenn wir jetzt auf $author->posts zugreifen, erhalten wir die posts des author absteigend sortiert nach ihrer creation time (Erstellzeitpunkt). Bei jedem Post wurden auch seine Kategorien geladen.

Info: Wenn ein Spaltenname in zwei oder mehr Tabellen auftaucht, die verbunden werden sollen, muss er eindeutig gekennzeichnet werden. Dies geschieht durch voranstellen des Tabellennamens vor den Spaltennamen. Aus id wird beispielsweise Team.id. Bei relationalen AR-Abfragen haben wir diese Freiheit nicht, da AR den SQL-Ausdruck automatisch erzeugt und jeder Tabelle systematisch ein Alias gegeben wird. Um Konflikte mit Spaltennamen zu vermeiden, benutzen wir daher einen Platzhalter, um zu markieren, dass wir eine zu kennzeichnende Spalte verwenden. AR ersetzt den Platzhalter mit einem geeignet Tablellenalias und sorgt so fur eine ordnungsgemäße eindeutige Kennzeichnung.

4. Dynamische Optionen für relationale Abfragen

Seit Version 1.0.2 können wir dynamische Optionen für relationale Abfragen sowohl bei with(), als auch bei der with-Option nutzen. Die dynamischen Optionen überschreiben die in der relations()-Methode spezifisierten. Wollen wir beispielsweise im obigen User-Model den eager loading-Ansatz nutzen, um die Beiträge, die zu einem Autor gehören in aufsteigender Reihenfolge zu erhalten (die order-Option in der relations-Angabe verwendet absteigende Reihenfolge), können wir das folgendermaßen erreichen:

User::model()->with(array(
    'posts'=>array('order'=>'??.createTime ASC'),
    'profile',
))->findAll();

Seit Version 1.0.5 können dynamische Abfrageoptionen auch beim lazy loading-Ansatz verwendet werden. Dazu rufen wir die Methode mit dem namen des verbundenen Objekts auf und übergeben die Abfrageoptionen als Parameter. Der folgende Code liefert zum Beispiel die Beiträge eines Benutzers mit status 1:

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

5. Statistische Abfragen

Hinweis: Statistische Abfragen werden seit Version 1.0.4 unterstützt.

Neben den oben beschriebenen relationalen Abfragen unterstützt Yii auch sogenannte statistische Abfragen (auch: aggregierte Abfragen, engl.: aggregational query) Mit ihnen können Informationen zu verbundenen Objekten ausgelesen werden, wie z.B. die Anzahl von Kommentaren zu einem Beitrag, die durchschnittliche Bewertung eines Produkts, etc. Statistische Abfragen können nur für Objekte durchgeführt werden, die in einer HAS_MANY-Beziehung (z.B. ein Beitrag hat viele Kommentare) oder einer MANY_MANY-Beziehung (z.B. ein Beitrag gehört zu vielen Kategorien und eine Kategorie hat viele Beiträge) stehen.

Eine statistische Abfrage wird ähnlich wie eine oben beschriebene relatoinale Abfrage durchgeführt. Analog dazu müssen wir zunächst die statistische Abfrage in der relations()-Methode des CActiveRecord festlegen.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'commentCount'=>array(self::STAT, 'Comment', 'postID'),
            'categoryCount'=>array(self::STAT, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}

Hier legen wir zwei statistische Abfragen fest: commentCount errechnet die Anzahl der Kommentare zu einem Beitrag und categoryCount die Anzahl von Kategorien, denen ein Beitrag zugeordnet wurde. Beachten Sie, dass Post und Comment in einer HAS_MANY-Beziehung zueineander stehen, während Post und Category über eine MANY_MANY-Beziehung (über die Verbindungstabelle PostCategory) verknüpft sind.

Mit obiger Deklaration können wir die Anzahl der Kommentare eines Beitrags über den Ausdruck $post->commentCount beziehen. Beim ersten Zugriff auf diese Eigenschaft wird implizit eine SQL-Abfrage durchgeführt, um das entsprechende Ergebnis zu bestimmen. Wie wir bereits wissen, handelt es sich hierbei um den lazy loading-Ansatz. Wenn wir die Anzahl der Kommentare für mehrere Beiträge bestimmen wollen, können wir auch die eager loading-Methode verwenden:

$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

Dieser Befehl führt drei SQL-Anweisungen aus um alle Beiträge zusammen mit der Anzahl ihrer Kommentare und Kategorien zurückzuliefern. Würden wir den lazy loading-Ansatz verwenden, würde das in 2*N+1 SQL-Abfragen resultieren, wenn es N Beiträge gibt.

Standardmäßig wird eine statistische Abfrage unter Verwendung von COUNT durchgeführt (was in obigem Beispiel für die Ermittlung der Anzahl der Kommentare und Kategorien der Fall ist). Wir können dies über zusätzliche Optionen bei der Deklaration in relations() anpassen. Hier die verfügbaren Optionen:

  • select: Der statistische Ausdruck. Vorgabewert ist COUNT(*), was der Anzahl der Kindobjekte entspricht.

  • defaultValue: Der Wert, der jenen Einträgen zugeordnet werden soll, für die die statistische Abfrage kein Ergenis liefert. Hat ein Beitrag z.B. keine Kommentare, würde sein commentCount diesen Wert erhalten. Vorgabewert ist 0.

  • condition: Die WHERE-Bedingung. Standardmäßig leer.

  • params: Die Parameter, die an die erzeugte SQL-Anweisung gebunden werden sollen. Sie sollten als Array aus Namen-/Wert-Paare angegeben werden.

  • order: Die ORDER BY-Anweisung. Standardmäßig leer.

  • group: Die GROUP BY-Anweisung. Standardmäßig leer.

  • having: Die HAVING-Anweisung. Standarmäßig leer.

6. Relationale Abfragen mit benannten Bereichen

Hinweis: Benannte Bereiche werden seit Version 1.0.5 unterstützt.

Auch relationale Abfragen können mit benannten Bereichen kombiniert werden. Dabei gibt es zwei Anwendungsfälle. Im ersten Fall werden benannte Bereiche auf das Hauptmodel, im zweiten auf die verbundenen Objekte angewendet.

Der folgende Code zeigt, wie benannte Bereiche mit dem Hauptmodel verwendet werden:

$posts=Post::model()->veroeffentlicht()->kuerzlich()->with('comments')->findAll();

Dies unterscheidet sich kaum vom Vorgehen bei nicht-relationalen Abfragen. Der einzige Unterschied besteht im zusätzlichen Aufruf von with() nach der Kette benannter Bereiche. Diese Abfrage würde also die kürzlich veröffentlichten Beiträge zusammen mit ihren Kommentaren zurückliefern.

Und der folgende Code zeigt die Anwendung benannter Bereiche auf verbundene Models:

$posts=Post::model()->with('comments:kuerzlich:freigegeben')->findAll();

Diese Abfrage liefert alle Beiträge zusammen mit ihren freigegebenen Kommentaren zurück. Beachten Sie, dass comments sich auf den Namen der Beziehung bezieht, während kuerzlich und freigegeben zwei benannte Bereiche sind, die in der Modelklasse Comment deklariert sind. Der Beziehungsname und die benannten Bereiche sollten durch Doppelpunkte getrennt werden.

Benannte Bereiche können auch in den in CActiveRecord::relations() festgelegten with-Optionen einer Beziehung angegeben werden. Würden wir im folgenden Beispiel auf $user->posts zugreifen, würden alle freigegebenen Kommentare des Beitrags zurückgeliefert werden.

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID',
                'with'=>'comments:freigegeben'),
        );
    }
}

Hinweis: Bei relationalen Abfragen können nur benannte Bereiche verwendet werden, die in CActiveRecord::scopes definiert wurden. Daher können diese hier auch nicht parametrisiert werden.