Best Practice Laden von Objekten aus der Datenbank und Objektlisten

philomatique

Grünschnabel
Hallo,

ich habe mal ein paar grundsätzliche Fragen. Ich hantiere öfter mit Listen von Objekten. Dabei funktioniert das ganze bisher wie folgt:

Als kurze Anmerkung:
Der Code ist bei weitem nicht vollständig und soll nur die Funktionsweise demonstrieren. Also eher eine Art Pseudocode.

Beispielklasse Element

hat eine ID, die nur vom System gesetzt werden kann. In dem Objekt liegen neben den Getter- und Setter-Methoden Funktionen zum Laden und Speichern des Objektes (siehe Beispielcode). Dabei findet die MYSQL-Abfrage zum speichern und laden direkt in der Klasse statt.

PHP:
class element {


	private $id;
	private $classStuff

	// getter...
	public function getId();
	public function getClassStuff();

	// setter
	public function getClassStuff($value);

	// load and save
	public function load($id) {

		$result = mysql_query('SELECT ... WHERE id = $id');
		$this->id = $result->value->id;
		$this->classStuff = $result->value->classStuff;


	}

	public function save() {

		if ($this->id === NULL) {
			result = mysql_query('INSERT INTO');
			$this->id = mysql_insert_id();
		}else {
			result = mysql_query('UPDATE ... WHERE id = $this->id');
		}

	}

}


Beispielklasse List

beinhaltet einen Array worein alle Objekte vom Typ Element geladen werden. Dabei kann über Parameter und je nach Anwendungsgebiet entschieden werden welche Objekte geladen werden. D.h. es ist möglich alle Objekte zu laden, die einem bestimmten Kriterium entsprechen. In der Methode Load werden alle IDs geholt und dann jedes Objekt einzeln geladen (was bei mehreren hundert Objekten schon viel zu anfrageintensiv ist und lange dauert). Über die Methode getElement() kann man dann auf ein bestimmtes Objekt im Array zugreifen.

PHP:
class list {

	private $list; // array()

	// getter methoden
	public function getElement($position) {

		return $this->list[$position];

	}

	public function getSize();
	...

	// load list
	public function load(args) {
	
		// code zum anpassen der "WHERE"-Klausel anhand von args
		result = mysql_query('SELECT id ... WHERE');

		while ($result) { 

			unset($temp);
			$temp = new Element();
			$temp->load($result.->value->id);
			$this->list[] = $temp;
		 }


	}

}

Nun meine Fragen:

1)
Würdet Ihr das Laden und Speichern der Objekte aus der Element Klasse verbannen und dafür eine extra Klasse gestalten?
Also eine Klasse, der ich beispielsweise eine Query übergebe? Oder gibt es eine noch bessere Lösung?

2)
Wie verwaltet Ihr Listen von Objekte? Die Lösung von oben ist zwar nett aber doch zu umständlich. Ich brauche jedoch definitiv die Möglichkeit die Ausgabe einzuschränken, also die Funktion in der load()-Methode der Liste, die meine Auswahl an Element-Objekten einschränkt.

3)
Ich habe beobachtet, dass der Array im Speicher sehr viel Platz einnimmt, wenn beispielsweise 500 "Mini-Objekte" (also mit höchsten zehn Attributen und
dazugehörigen Getter- und Setter-Methoden) geladen werden. Woran kann das liegen?

Danke und Grüße
Phil
 
Vorweg möchte ich klarstellen, dass sich eine möglichst optimale Lösung in PHP nicht an den Best Practices anderer Sprachen orientieren sollte.

Würdet Ihr das Laden und Speichern der Objekte aus der Element Klasse verbannen und dafür eine extra Klasse gestalten? Also eine Klasse, der ich beispielsweise eine Query übergebe? Oder gibt es eine noch bessere Lösung?
Sinnvoll ist immer eine Art Factory, die sich um die Erzeugung von entsprechenden Strukturen und Objekten kümmert. Sofern du bei deinen momentanen Klassen bleiben möchtest, solltest du die entsprechenden Methoden statisch machen und von diesen (gerne auch mit einem hart codierten Query, wenn du keine speziellen Use Cases implementieren musst) die entsprechenden Objekte erzeugen lassen.


Wie verwaltet Ihr Listen von Objekte? Die Lösung von oben ist zwar nett aber doch zu umständlich. Ich brauche jedoch definitiv die Möglichkeit die Ausgabe einzuschränken, also die Funktion in der load()-Methode der Liste, die meine Auswahl an Element-Objekten einschränkt.
In PHP ist noch immer der Einsatz rein numerischer Arrays am effizientesten, wenn es um Listen geht. Leider verliert man dadurch natürlich auch diverse Bequemlichkeiten und Features der objektorientierten Programmierung.

Ich habe beobachtet, dass der Array im Speicher sehr viel Platz einnimmt, wenn beispielsweise 500 "Mini-Objekte" (also mit höchsten zehn Attributen und dazugehörigen Getter- und Setter-Methoden) geladen werden. Woran kann das liegen?
Abhängig von der Implementierung sind Objekte in PHP generell mehr oder weniger speicherintensiv. Der GC der Zend Engine ist dennoch sehr effizient, und mistet die Daten beim Verlassen des entsprechenden Scopes ordentlich aus. Was natürlich immer mit reinspielt ist auch der GC selbst, welcher selbst noch diverse Strukturen verwalten muss um die Referenzierungen zu evaluieren.
Zu beachten ist, dass Objektmethoden und -Eigenschaften zu jeder Instanz erneut angelegt werden. Benötigt ein Objekt also "nur" 10 KiB im Speicher, sind es bei 500 Objekten schon 5MiB. Hinzu kommt der Overhead der PHP Engine, sowie der andere Kram drumherum.
 
Hey,

danke für deine Antwort und sorry für meine späte Reaktion. Über Ostern war leider keine Zeit.

Vorweg möchte ich klarstellen, dass sich eine möglichst optimale Lösung in PHP nicht an den Best Practices anderer Sprachen orientieren sollte.

Natürlich. Mir gehts ja um eine möglichst gute Lösung für PHP. :-)

Sinnvoll ist immer eine Art Factory, die sich um die Erzeugung von entsprechenden Strukturen und Objekten kümmert

Das hab ich mir fast gedacht. Allerdings habe ich dazu noch ein zwei Fragen weiter unten.

In PHP ist noch immer der Einsatz rein numerischer Arrays am effizientesten, wenn es um Listen geht. Leider verliert man dadurch natürlich auch diverse Bequemlichkeiten und Features der objektorientierten Programmierung.

Gut zu wissen. Hatte gehofft, dass es noch was anderes schickes gibt. Meine "Listenklasse" erzeugt doch aber eigentlich dazu keinen erheblichen Overhead oder?

Abhängig von der Implementierung sind Objekte in PHP generell mehr oder weniger speicherintensiv. [...] Zu beachten ist, dass Objektmethoden und -Eigenschaften zu jeder Instanz erneut angelegt werden. Benötigt ein Objekt also "nur" 10 KiB im Speicher, sind es bei 500 Objekten schon 5MiB.

Das ist natürlich gut zu wissen, dass da doch einiges zusammenkommt bei großen Listen. Räumt er eigentlich gleich auf, wenn ich eine Variable mit unset() lösche? Weiß er beispielsweise bei meiner Liste, dass durch das entfernen des Arrays auch alle Objekte in der Liste gelöscht werden müssen oder muss ich das am besten selber machen über den Destruktor?

Und jetzt noch ein zwei offene Fragen:

Ist mein Ansatz sinnvoll? Also dass die ID eines Objekts nirgenswo geändert werden kann? Das eröffnet natürlich das Problem, dass ich die Inhalte der Objekts immer nur separat laden kann (über eine extra Query), was ziemlich zeitintensiv und eientlich unnötig ist. Oder stellt es von der Sicherheit kein Problem her? Dann könnte ich direkt aus der Listen-Klasse die Objekte mit Inhalt füllen, was erheblich schneller wäre.

Habt Ihr immer einen DB-Abstract-Layer? Dazu könnte ich ja die Methoden "load", "save", "delete" auslagern und hätte dafür nur noch eine klasse. Allerdings ist die Frage, wie ich diese Methode in dem Layer rufe. Müsste ich dann die Query komplett übergeben? Oder nur den Tabellennamen und die Werte in einem Array?

Danke für Eure Hilfe.
Phil
 
Gut zu wissen. Hatte gehofft, dass es noch was anderes schickes gibt. Meine "Listenklasse" erzeugt doch aber eigentlich dazu keinen erheblichen Overhead oder?
Sofern effizienteste Speicherverwaltung und eine extrem geringe Laufzeit erforderlich sind, ist PHP keine gute Wahl. Da man im Einsatz von PHP generell mit gewissen Abstrichen in diesen Aspekten leben muss, halte ich es für Haarspalterei es ins Extrem zu treiben. Wenn du Dictionaries (assoziative Arrays) verwenden möchtest, tu es. Wenn sich das Ausschöpfen der Möglichkeiten von OOP gut macht, dann nimm es mit rein.
Wie groß der Overhead und die allgemeine Ressourcenbeanspruchung tatsächlich ist, lässt sich mit Benchmarking-Tools und internen Evaluierungen / Loggingmechanismen ermitteln. Eine allgemeine Aussage kann ich dahingehend leider nicht machen.
Sofern du beispielsweise eAccelerator, Zend Server oder allgemein PHP6 verwenden kannst, lässt sich damit auch noch einiges rausholen. In einer kleinen Applikation auf meinem Testserver, die fast vollständig auf dem Zend Framework aufbaute, ließ sich mit eA die Laufzeit auf bis zu 1/50 dezimieren, ebenso ging der Speicherverbrauch extrem zurück, da nicht erst der PHP-Interpreter rangeschafft werden musste.
Durch solche Optimierungen lässt sich wesentlich mehr rausholen als eine PHP-Codeoptimierung in Standardsituationen erzielen könnte.

Räumt er eigentlich gleich auf, wenn ich eine Variable mit unset() lösche? Weiß er beispielsweise bei meiner Liste, dass durch das entfernen des Arrays auch alle Objekte in der Liste gelöscht werden müssen oder muss ich das am besten selber machen über den Destruktor?
Wäre mal interessant die Implementierung im Sourcecode von PHP anzuschauen, ich gehe aber einfach mal davon aus, dass die unset-Funktion implizit den Speicher freiräumt.
Der PHP Garbage Collector verfolgt die selbe Strategie wie der Java GC: Ein Reference Counter zählt den Bestand an Referenzen auf ein Objekt, und markiert es für den GC, sofern keine Referenzen mehr bestehen. Das Markieren lässt jedoch nicht darauf schließen, dass der Garbage Collector den Speicher sofort wieder freigibt. Verwendet man hingegen unset, könnte ich mir gut vorstellen, dass implizit auch der Speicher "sofort" freigeräumt wird.

Ist mein Ansatz sinnvoll? Also dass die ID eines Objekts nirgenswo geändert werden kann? Das eröffnet natürlich das Problem, dass ich die Inhalte der Objekts immer nur separat laden kann (über eine extra Query), was ziemlich zeitintensiv und eientlich unnötig ist. Oder stellt es von der Sicherheit kein Problem her? Dann könnte ich direkt aus der Listen-Klasse die Objekte mit Inhalt füllen, was erheblich schneller wäre.
Im Zweifelsfall für die in diesem Fall nicht nur leichtere sonder auch vor allem effizientere Methode. Sofern du anschließend eine Implementierung bereitstellst, könnte man nochmal gemeinsam an eventuellen Verbesserungen feilen.

Habt Ihr immer einen DB-Abstract-Layer? Dazu könnte ich ja die Methoden "load", "save", "delete" auslagern und hätte dafür nur noch eine klasse. Allerdings ist die Frage, wie ich diese Methode in dem Layer rufe. Müsste ich dann die Query komplett übergeben? Oder nur den Tabellennamen und die Werte in einem Array?
Sofern du es tatsächlich abstrahieren möchtest, kannst du dich bei Lust und Zeit mit Object-relational Mapping (ORM) beschäftigen, jedoch ist mir bisher keine meiner Meinung nach sinnvolle PHP-Implementierung bekannt. Das Zend Framework bietet mit der Zend_Db_Table_Abstract Klasse ORM-ärtige Features, kann jedoch unter Umständen umständlicher in der Organisation werden als eine Implementierung sinnvoll ist. Für dein Vorhaben aber klingt es durchaus gut geeignet.
Bin gerade nicht mehr in der Materie drin, aber ich dächte sogar es wäre Möglich die geholten Datensätze über Zend_Db_Table(_Abstract) direkt in Objekte deiner Wahl zu transformieren.

Sofern du eine eigene Implementierung wählst, empfiehlt es sich meiner Meinung nach nur die nötigen Daten zu übergeben und entsprechend in die Queries einzubetten. Möchtest du für ein einzelnes Element lediglich einen Spaltenwert bearbeiten, macht es schließlich wenig Sinn auch sämtliche weitere Daten des Objekts nochmal für das Update anzugeben.
 
Hallo,

vor einem ähnlichen Problem stand ich vor ein paar Wochen auch mal. Ich hatte versucht eine Lösung des NestedSets Model zu schreiben.
Beim auslesen der Menüknoten aus der Datenbank stand ich auch vor der Frage, ob ich eine Array nehme oder mehr über FactoryModel eine verschachtelung von Objekten durchführe.

Wie gesagt, das Problem ist nicht identisch, sondern nur ähnlich da ich keine einfache Liste mit der Ergebnissen der Datenbank habe, sondern eine Struktur und Abhängigkeit brauchte.

Ich habe mich dann nachher für die in Objekten gestaffelte Lösung entschieden, trotz Overhead, da die Übersichtlichkeit besser ist und ich einfacher auf bestimmte Knoten in der n-ten Ebene zugreifen kann.

Ich habe dabei PDO genutzt und die Ergebnisse "aus der Datenbank" direkt als Objekte instanzieren lassen.

Hier mal mein Beispielcode (nicht vollständig):
Zuerst die Klasse für die Nodes, die direkt durch PDO generiert werden.
PHP:
class nestedSetNode {
    /**
     * id of the node
     * @var int
     * @access private
     */
    private $id;
    /**
     * name of the node
     * @var string
     * @access private
     */
    private $name;
    /**
     * id of the parent node
     * @var int
     * @access private
     */
    private $parentid;
    /**
     * array with direct childnodes
     * @var array
     * @access private
     */
    private $childNodes = array();


    /**
     * constructor of this class
     */
    public function __construct()
    {
    }
    /**
     * Return the ID of the parent Node
     *
     * @return int
     * @access public
     */
    final public function getParentId()
    {
        RETURN (int) $this->parentid;
    }
    /**
     * Return the ID of the parent Node
     *
     * @return int
     * @access public
     */
    final public function getNodeId()
    {
        RETURN (int) $this->id;
    }

    /**
     * Add a childnode to this node
     *
     * @param nestedSetsNode $node an nestedSetsNode instance for a node
     * @access public
     */
    final public function addChildNode(nestedSetsNode $node)
    {
        $id = $node->getNodeId();
        $this->childNodes[$id] = $node;
    }
    /**
     * Return Node with it's childnodes as array
     *
     * @return
     * @access public
     */
    final public function toArray()
    {
        $return = array();
        $return[] = $this->name;
        IF($this->hasChildNodes() === TRUE) {
            $aChnodes = array();
            FOREACH($this->childNodes AS $cNode) {
                $aChnodes[] = $cNode->toArray();
            }
            $return[] = $aChnodes;
        }

        RETURN $return;
    }
}

Dann die "Factory" die die Nodes aus der Datenbank als Objekte generiert
PHP:
class nestedSets {
    // ....
    /**
     * Get the complete tree
     *
     * @return todo
     * @access public
     */
    final public function getFullTree()
    {
        $stmt = $this->db->query('SELECT
                                        node.kbc_id AS id,
                                        node.kbc_name AS name,
                                        node.kbc_parent AS parentid,
                                        /* .... */
                                    FROM
                                        /* usw */
        ');
        // ggf. noch via PDO::bindParam() WHERE clause einschränken
        $stmt->setFetchMode(PDO::FETCH_CLASS, 'nestedSetsNode');
        $resultSet = $stmt->fetchAll();
        // ab hier liegen alle Objekte der Ergebnisse in $resultSet
        // weitere bearbeitung nun zum beispiel durch iteration
        FOREACH($resultSet AS $obj) {
                echo $obj->getNodeId(), "\n";
        }
        // beim NestedSets Model müssen die Daten nicht als Liste vorliegen sondern
        // geordnet und "untereinander" strukturiert werden
        $tmp = array();
        $rootNode = NULL;
        $smallLeftValue = PHP_INT_MAX;
        FOREACH($resultSet AS $obj) {
            $nodeId = $obj->getNodeId();
            $parentId = $obj->getParentId();
            $tmp[$nodeId] = $obj;
            IF(array_key_exists($parentId, $tmp)) {
                $tmp[$parentId]->addChildNode($obj);
            }

            IF($obj->getLeftValue() < $smallLeftValue) {
                $smallLeftValue = $obj->getLeftValue();
                $rootNode = $obj;
            }
        }
        // todo: need closeCursor? used PDO::fetchAll()
        // $stmt->closeCursor();

        unset($tmp, $smallLeftValue, $resultSet);
        // rückgabe des root Nodes
        RETURN $rootNode;
    }
}

Das löst vermutlich nicht dein Problem des niedrigen Speicherverbrauchs, allerdings ist mir die Übersicht in dem Fall lieber. Bei Testcases ist mir damals aufgefallen, das bei hohem Speicherverbrauch und bei Erreichung des max_memory_limit PDO komplett still und ohne Fehlermeldung oder Exception sich verabschiedet und das Script beendet (super zu debuggen! *kotz*).
In dem Beispielcode habe ich noch die toArray() Methode mitgegeben. Sodass ich den "Baum mit Objekten" in ein Array wandeln kann.
Beispielhafter Code:
PHP:
// Beispielcode, nicht funktionsfähig
$ns = new nestedSets;
$root = $ns->getFullTree();
var_dump($root->toArray());

So habe ich damals das Problem gelöst, ob es für deinen Fall auch passend ist, musst du entscheiden.
Die Nutzung von PDO und PDO::FETCH_CLASS ist meiner meinung eine Erleichterung. Die im SQL ausgewählten Spalten sind nachher die Klassenvariablen (in meinem Beispiel id, name, parentId). Es ist vielleicht interessant: Der Konstruktor wird erst nach der erzeugung der "nestedSetNode" Objekten aufgerufen und die Paramter können über den 3. (bei mir nich genutzen) Paramter in PDO::setFetchmode() übergeben werden.

Gruss
 
Zurück