Home-Produkte-Testarea-Kontakt-Datenschutz-Aktualisiert: 05-Apr-2010
< Voriger Tag   Nächster Tag >

Montag, 05. April 2010

PHP: RuBisCO ORM -- 1:1 Beziehung

Das Speichern einzelner Objekte jeweils in eine Tabelle lässt sich sehr direkt umsetzen. Mit Objekten, die miteinander in Beziehung stehen, sieht das schon komplizierter aus. Mal in kleinen Schritten anhand konkreter Beispiele herantasten.

Für die 1:1-Beziehung benutze ich das To Do-List Beispiel. Jedem Task kann ein User zugeordnet werden, der sich um die Umsetzung der Aufgabe kümmert.

ID Task Assigned
1Task 1alice
2Task 2alice
3Task 3bob

SQL-Tabellen

Benutzt werden dazu folgende Tabellen:

CREATE TABLE IF NOT EXISTS users (
  id     INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  login  VARCHAR(256) NOT NULL
) ENGINE=INNODB;


CREATE TABLE IF NOT EXISTS tasks (
  id       INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  task     VARCHAR(256) NOT NULL,
  assigned INT, FOREIGN KEY (assigned) REFERENCES users(id)
                                       ON UPDATE RESTRICT
                                       ON DELETE SET NULL,
  INDEX (assigned)
) ENGINE=INNODB;

PHP-Objekte

Die auf folgende PHP-Objekte abgebildet werden:

class User {
    protected $_id    = null;
    protected $_login = null;
}


class Task {
    protected $_id       = null;
    protected $_task     = null;
    protected $_assigned = null;

    public $usersRef = null;
}

Für jede Tabelle gibt es also weiterhin jeweils eine Klasse. Und die 1:1-Verbindung der Objekte erfolgt über die Referenz in $task->usersRef.

Wie müssen jetzt die Methoden fürs Lesen, Einfügen, Aktualisieren, ... aussehen?

Read

Per JOIN werden die Daten aus den benötigten Tabellen mit einer SELECT-Anweisung gelesen und dann den einzelnen Objekten zugewiesen.

$sqlReadTask = 'SELECT tasks.id AS tasks_id, tasks.task AS tasks_task,
			tasks.assigned AS tasks_assigned, users.id AS users_id,
			users.login AS users_login
		FROM tasks JOIN users ON tasks.assigned = users.id
		WHERE tasks.id = :id';
fetch();

$this->id       = $result['tasks_id'];
$this->task     = $result['tasks_task'];
$this->assigned = $result['tasks_assigned'];

$this->usersRef        = new UserDB();
$this->usersRef->id    = $result['users_id'];
$this->usersRef->login = $result['users_login'];

Insert

Beim Einfügen muss das referenzierte Objekt bereits in der Datenbank vorhanden sein, ansonsten gibt es durch den FOREIGN KEY einen Fehler. Deshalb wird als erstes das referenzierte Objekt mit $this->usersRef->insertOrUpdate() gespeichert. Danach folgen die Daten des Tasks-Objekts. Für usersRef wird insertOrUpdate() benutzt, da der User schon in der Datenbank vorhanden sein kann. In diesem Fall kommen nur die Daten des Task-Objekts neu hinzu.

Wenn die _id des Tasks-Objekts bisher null war, also nicht selbst gesetzt wurde, wird sie anschließend aus der Datenbank gelesen und dem Objekt zugewiesen.

try {
    $this->app->beginTransaction();

    if(!is_null($this->usersRef)){$this->usersRef->insertOrUpdate();}

    $sqlInsertTask = 'INSERT INTO tasks (id, task, assigned)
                             VALUES (:id, :task, :assigned)';
    insert();

    if(is_null($this->_id)) {
        $this->_id = $this->dbh->lastInsertId();
    }

    $this->app->commit();
} catch (Exception $e) {
    $this->dbh->rollBack();
    throw $e;
}

Update

update() geht davon aus, dass sich sowohl das Objekt als auch die referenzierten Objekte bereits in der Datenbank befinden und nur deren Werte aktualisiert werden müssen. Damit reduziert sich die Methode auf ein UPDATE der Daten.

try {
    $this->app->beginTransaction();

    if(!is_null($this->usersRef)){$this->usersRef->update();}

     $sqlUpdateTask = 'UPDATE tasks SET task = :task, assigned = :assigned
                       WHERE tasks.id = :id';

     execute();

     $this->app->commit();
} catch (Exception $e) {
     $this->dbh->rollBack();
     throw $e;
}

Insert or Update

Zum Teil möchte oder kann man nicht zwischen INSERT und UPDATE unterscheiden. Hier hilft die insertOrUpdate()-Methode weiter.

insertOrUpdate() fügt das Objekt neu in die Datenbank ein, falls es noch nicht vorhanden ist und aktualisiert im anderen Fall die Daten des Objekts. Bei MySQL wird hierzu INSERT INTO ... ON DUPLICATE KEY UPDATE ... benutzt.

Bei eingefügten Objekten wird gegebenfalls anschließend wie bereits bei insert() die _id aus der Datenbank gelesen.

$sqlInsert = 'INSERT INTO tasks (id, task, assigned)
                          VALUES (:id, :task, :assigned)
              ON DUPLICATE KEY UPDATE task = :task, assigned = :assigned';

execute();

if(is_null($this->_id)) {
    $this->_id = $this->dbh->lastInsertId();
}

Delete

delete() löscht nur das Objekt aus der Datenbank. Die Behandlung der referenzierten Objekte wird der Datenbank durch die ON CASCADE-Deklaration überlassen.

$sqlDeleteTask = 'DELETE FROM tasks WHERE id = :id';

execute();

setAssigned($value)

Wichtig ist insbesonders die Verarbeitung einer Zuweisung an assigned und damit einer Änderung des Users: $task->assigned = 2.

Wenn sich mit assigned das zur Referenz gehörende Feld ändert, dann muss das Programm die Referenz aktualisieren. Dies geschieht durch überschreiben von setXXX() -- in diesem Fall setAssigned(). Hier wird $this->usersRef() (neu) eingelesen, wenn sich der Wert von assigned ändert.

Die setXXX()-Funktionen werden automatisch bei Zuweisungen wie $task->assigned = 2 aufgerufen.

Fehler: Was im Moment noch nicht berücksichtigt wird ist, dass sich umgekehrt mit einer Änderung in $task->usersPref->id auch der dazugehörige Wert in $task->assigned ändern muss.

public function setAssigned($value) {
    $oldValue = $this->assigned;

    parent::setAssigned($value);

    if($oldValue !== $this->assigned) {
        try {
            if(is_null($this->usersRef)) {
                $this->usersRef = new UserDB();
            }

            $this->usersRef->read($this->assigned);

            return $this;
         } catch (\Exception $e) {
             $this->usersRef = null;
             $this->_assigned   = null;
             throw $e;
         }
     }
}// setAssigned

Verschachtelte Transaktionen

Da ein Objekt die insert()-, update()-, ...-Methoden eines anderen Objekts aufruft, müssen sich die SQL-Transaktionen verschachteln lassen. Dies ist im Applikations-Objekt, in diesem Fall Todo, mit beginTransaction() und commit() umgesetzt. Gezählt wird die Verschachtelungstiefe in transactionCount.

/**
* Begin nested database transaction.
*/
public function beginTransaction() {
    if(0 == $this->transactionCount) {
        $this->dbh->beginTransaction();
    }

    $this->transactionCount++;
}// beginTransaction


/**
* Commit nested database transaction.
*/
public function commit() {
    $this->transactionCount--;

    if(0 == $this->transactionCount) {
        $this->dbh->commit();
    }
}// commit

Referenz oder Array?

Eine 1:1-Beziehung ist recht speziell. Bei der 1:n-Beziehung wird dann statt nur einer Referenz ein Array benötigt. Eventuell macht es daher Sinn für die 1:1 Beziehung auch ein Array mit nur einen Eintrag zu benutzen. Dadurch entfällt der Sonderfall mit nur einem Referenzfeld und die Programmierung wird einfacher.

Quelltext (MIT Lizenz)

Der komplette Quelltext von RuBisCO ORM liegt in Subversion: Heutige Revision 27, aktuelle Version.

Das generierte Beispiel steht in examples/onetoone/src/.

Die Generierung geschieht in erster Linie in Table.php.

[Direktlink]

< Voriger Tag   Nächster Tag >

  RSS V0.91

<April 2010 >
   01020304
05060708091011
12131415161718
19202122232425
2627282930  

Home-Produkte-Testarea-Kontakt-Datenschutz-Aktualisiert: 05-Apr-2010
(C) 2000-2018 by Sven Drieling