PHP: RuBisCO ORM -- 1:n-Beziehung
Als Beispiel für die 1:n-Beziehung dient, dass einem User beliebig viele Aufgaben zugewiesen werden können.
ID | Task | Login |
---|---|---|
1 | Task 1 | alice |
2 | Task 2 | alice |
3 | Task 3 | alice |
SQL-Tabellen
Die SQL-Tabellen bleiben gegenüber dem vorigen 1:1 Beispiel gleich. Womit sich die Frage stellt wie dann später anhand der SQL-Deklaration zwischen 1:1 und 1:n unterschieden wird? Im Moment ist die Unterscheidung erst einmal per "Hack" in rorm.php enthalten.
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
Bei den PHP-Objekten gibt es jetzt statt einer Referenz nun
ein Array mit den n
-Objekten.
class User { protected $_id = null; protected $_login = null; protected $_tasks = array(); } class Task { protected $_id = null; protected $_task = null; protected $_assigned = null; }
Read
Das Einlesen kann mit einer oder zwei SQL-Abfragen erfolgen.
SELECT id, login FROM users WHERE id = 1; SELECT id, task, assigned FROM tasks WHERE assigned = 1;
Und mit einer SQL-Abfrage:
SELECT * FROM users LEFT JOIN tasks ON users.id = tasks.assigned WHERE users.id = 3;
Die zwei Abfragen haben den Vorteil, dass weniger Daten
übertragen werden und der Fall "Keine Tasks zugewiesen" sich
einfacher verarbeiten lässt, weil nicht zusätzlich auf NULL
geprüft werden muss. Benutzt wird daher die Variante mit zwei
SQL-Abfragen. Die zweite ist in readMany($id)
ausgelagert. Aufgerufen wird sie indirekt durch die Zuweisung an
$users->id
.
public function read($id = null) { if(null === $id) { $id = $this->_id; } // TODO Lock? $sql = 'SELECT users.id AS users_id, users.login AS users_login FROM users WHERE users.id = :id'; fetch(); $this->id = $result['users_id']; $this->login = $result['users_login']; // $this->readMany($id) is triggered by $this->id = $result['users_id']; }// read private function readMany($id) { $sql = 'SELECT tasks.id AS tasks_id, tasks.task AS tasks_task, tasks.assigned AS tasks_assigned FROM tasks WHERE assigned = :id'; $sth = $this->dbh->prepare($sql); $sth->execute(array(':id' => $id)); while($result = $sth->fetch()) { $obj = new TaskDB(); $obj->id = $result['tasks_id']; $obj->task = $result['tasks_task']; $obj->assigned = $result['tasks_assigned']; $this->_tasks[] = $obj; } }// readMany
Insert, Update, InsertOrUpdate
Arbeiten wie die vorigen Methoden. Der Unterschied besteht nur
darin, dass jetzt statt einer Referenz eine
foreach
-Schleife benutzt wird, um die einzelnen,
zugehörigen Objekte zu speichern.
foreach($this->_tasks as $obj) { $obj->insertOrUpdate(); }
Delete
Das Löschen erfolgt ebenfalls wie gehabt. Nur der User wird
gelöscht. Die assigned = NULL
-Zuweisung in den Tasks
wird der Datenbank per CASCADE überlassen.
setID($value)
Wenn sich die id des Users ändert, werden die Tasks neu eingelesen:
public function setId($value) { $oldValue = $this->id; parent::setId($value); if($oldValue !== $this->id) { try { $this->_tasks = array(); $this->readMany($this->id); return $this; } catch (\Exception $e) { $this->_tasks = array(); $this->_id = null; throw $e; } } }// setId
Frage: Sollen geänderte Tasks vorher gespeichert werden?
$task->assigned = 42
Fragen: Was ist, wenn einem Task innerhalb des
$user->tasks
-Array ein anderer User zugewiesen
wird? In diesem Fall müsste er aus jenen Array entfernt werden und
dem tasks
-Array des anderer Users zugefügt werden?
Was ist, wenn dieser User erst später geladen wird? Das
Task
-Objekt müsste dann zwischengelagert und später dem
neu geladenen User zugefügt werden. Als Alternative könnte
$task->assigned
nur lesbar sein.
TODO: Add/Delete/Modify Tasks
Im Moment fehlt noch eine API, um die
$user->tasks
bearbeiten zu können.
Optimierung: Dirty-Flag
Bei insert(), update(), ...
werden im Moment
immer alle zugehörigen Objekte gespeichert, selbst wenn sie
unverändert sind. Zur Optimierung muss es später noch ein
Dirty-Flag geben, das neue oder veränderte Objekte markiert, so dass
dann nur diese Objekte gespeichert werden.
Zyklische Abhängigkeiten
Einem User können beliebig viele Tasks zugeordnet werden. Wenn
zusätzlich die 1:1-Beziehung aus dem vorigen Beispiel
hinzugenommen wird, kann einem Task wiederum ein User zugeordnet
werden. Alle im Array $users->tasks
enthaltenen
Tasks könnten also eine Referenz auf dem User enthalten:
$task->userRef = $user
.
Wenn ein solcher Task gespeichert wird, ruft er das Speichern des Users auf und dieses Speichern wiederum das Speichern der Tasks, die wieder den User speichern usw. -- eine Endlosschleife. Die müsste erkannt und verhindert werden. Die jetzige Alternative ist, dass die Tasks keine Referenz auf dem User enthalten.
Implementierung
Weil sich die Methoden ähnlich zur vorigen 1:1-Beziehung
verhalten ist der generierte Quelltext recht ähnlich zum vorigen.
Um die Generierung zu vereinfachen, liegt er aber zunächst einmal
seperat im classDBMany.rtpl
-Template.
Außerdem kann es derzeit nur ein Feld mit einer 1:1- oder 1:n-Beziehung
geben.
Der Programmcode für die neuen many
-Variablen
liegt in erster Linie im case 'onetomany':
-Abschnitt
von Table.php.
Die jetzige Implementierung ist einfach aber unvollständig, da mit der 1:n-Beziehung viele offene Fragen hinzugekommen sind.
Quelltext (MIT Lizenz)
Der komplette Quelltext von RuBisCO ORM liegt in Subversion: Heutige Revision 28, aktuelle Version.
Das generierte Beispiel steht in examples/onetomany/src/.