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 |
|---|---|---|
| 1 | Task 1 | alice | 2 | Task 2 | alice | 3 | Task 3 | bob |
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.
