Andre: OOP - n:m Beziehung von Klassen

Hallo,

bin noch ganz frisch auf dem Gebiet der OOP und haben nun ein Problem bei dem ich nicht weiterkomme.

Folgender Ausschnitt aus dem DB-Modell:

[produkt]
id
nr
anlagedatum
lastmodified

[suchname]
id
name

[prod_such_zuo]
produkt_id
suchname_id

Wie bilde ich das auf Klassen in meinem PHP-Code ab?

Also ich habe eine Klasse produkt mit foldenden werten.

Schreiben/Lesen per Setter/Getter.

private $id;
private $nr;
private $anlagedatum;
private $lastmodified;

Das selbe dann für den suchnamen.
private $id;
private $name;

Jetzt bräuchte ich doch noch eine Klasse prod_such_zuo.
Wie muss die aussehen um eine Beziehung zwischen Klasse "produkt" und Klasse "suchname" zu bekommen?

Ich komm da irgendwie nicht weiter.

Ich hoffe mir kann jemand weiterhelfen.

Gruß
Andre

  1. Moin!

    [produkt]
    id
    nr
    anlagedatum
    lastmodified

    [suchname]
    id
    name

    [prod_such_zuo]
    produkt_id
    suchname_id

    Wie bilde ich das auf Klassen in meinem PHP-Code ab?

    Würde ich gar nicht tun.

    Meine Klassen orientieren sich immer an der zu erledigenden Aufgabe - nicht an der Datenbank.

    Das kann unter anderem bedeuten, dass ich (weil es so aus der DB herauskommt) in der Klasse direkt ein Array speichere und verarbeite, dass u.U. "viel" enthält - jedenfalls deutlich mehr, als was bei dir so drinstecken soll.

    - Sven Rautenberg

    --
    "Love your nation - respect the others."
    1. Hallo,

      Meine Klassen orientieren sich immer an der zu erledigenden Aufgabe - nicht an der Datenbank.

      Aber angenommen ich möchte nun alle Suchnamen eines Produktes auslesen, wie mache ich das dann am besten. Da muss es doch eine Verbindung geben. Oder meinst du damit das ich das ohne Klassen machen sollte und einfach ne Datenbank Abfrage starten soll, da die beziehung ja schon in der Datenbank besteht.

      Das kann unter anderem bedeuten, dass ich (weil es so aus der DB herauskommt) in der Klasse direkt ein Array speichere und verarbeite, dass u.U. "viel" enthält - jedenfalls deutlich mehr, als was bei dir so drinstecken soll.

      also bevor ich deine Antwort gelesen habe, habe ich folgendes geschrieben:

      CODE - START:
      #######################################################
      class prod_such_zuo {

      private $array_suchname_ids = array();

      function prod_such_zuo($produkt_id) {
              $erg = mysql_query(
                  "SELECT suchname_id " .
                  "FROM prod_such_zuo " .
                  "WHERE produkt_id='$produkt_id'"
              );
              while($row = mysql_fetch_object($erg))
                  $this->array_suchname_ids[] = $row->suchname_id;
          }

      public function get_array_suchname_ids(){
              return $this->array_suchname_ids;
          }

      }
      #######################################################
      CODE - ENDE:

      Macht das Sinn? Ist das eventuell das was du gemeint hast mit dem speichern in einem Array? Geht es besser?

      Danke.

      Andre

      1. Moin!

        Ich will mal grundsätzlich vorausschicken, dass ich großer Fan von "Test-Driven Design" (= TDD) bin. Manche übersetzen es leider auch als "Test-driven development" - das unterschlägt, dass einem TDD enorm beim DESIGN hilft - und das ist mehr, als "nur" Development.

        So ein Testframework (ich bevorzuge SimpleTest) erleichtert mir die Arbeit enorm. Ich muß mich nicht darum kümmern, irgendwie selbst ein Skript zu schreiben, welches die Methoden der zu entwickelnden Klasse aufruft, damit ich prüfen kann, ob es funktioniert. Ich muß insbesondere bei verschachtelten Klassen nicht dafür sorgen, dass zuerst die inneren Klassen, dann die äußeren, alle instanziiert und mit korrekten Daten (wohlmöglich aus einer Datenbank) versorgt werden, um dann Input zu simulieren und Output auszugeben, der dann visuell manuell auf Korrektheit geprüft wird.

        TDD bzw. SimpleTest liefert mir schon eine Vielzahl von Testmöglichkeiten, und insbesondere die Möglichkeit, dass ich Fake-Objekte in die zu testende Klasse hineingebe, die anstelle der echten Klasse prüfen, ob sie mit korrekten Parametern aufgerufen werden und die dann korrekte Rückgabewerte liefern, die ich einfach definiere. Diese Fake-Objekte verhalten sich äußerlich genauso, wie die echte Klasse, die sie ersetzen - und deren Verhalten teste ich separat in einem eigenen Testskript. So kann ich also insgesamt schön separiert entwickeln und Abhängigkeiten aus dem Weg gehen.

        Der Design-Aspekt bei TDD ist folgender: Dadurch, dass ich mir zuerst überlege, in welcher Form ich eine zu testende Klasse bzw. Methode aufrufen will, und welche Rückgabewerte ich erwarte, setze ich die wirkliche Designentscheidung "Wie soll die Klasse nach außen wirken?" an den Anfang. Ich kann sogar, wenn ich diese Klasse derzeit noch nicht schreiben will, mir erstmal überlegen, wie sie nach außen wirkt, dann ein passendes MockObjekt als Fake schreiben, und erstmal die darauf aufbauende Klasse schreiben.

        Und dadurch, dass ich einen einmal geschriebenen Test nicht mehr lösche (es sei denn, er ist nicht mehr zutreffend), sondern im Prinzip nur immer neue Tests hinzufüge, die auf neue bzw. neu entdeckte Anforderungen testen oder aufgetretene Fehler vom Wiederauftreten abhalten sollen, entsteht während der Entwicklungsarbeit ein komplexes, die Klassen teilweise sogar gut dokumentierendes Gerüst, welches am Ende eine hohe Softwarequalität gewährleistet.

        Ich empfehle dir wirklich sehr, dass du dir mal die verlinkte Seite durchliest und dir das Framework herunterlädst. Die Seite ist leider auf englisch - sofern das ein Problem darstellt, sollte Google aber auch deutsche Tutorials zu TDD im Allgemeinen oder TDD mit PHP und SimpleTest im speziellen finden.

        Aber jetzt erstmal ins Detail:

        Meine Klassen orientieren sich immer an der zu erledigenden Aufgabe - nicht an der Datenbank.

        Aber angenommen ich möchte nun alle Suchnamen eines Produktes auslesen, wie mache ich das dann am besten. Da muss es doch eine Verbindung geben. Oder meinst du damit das ich das ohne Klassen machen sollte und einfach ne Datenbank Abfrage starten soll, da die beziehung ja schon in der Datenbank besteht.

        Wenn du Datenbankzugriff hast, dann bedeutet das automatisch, dass du eine Datenbankzugriffsklasse hast, die allen anderen Klassen, die das benötigen, DB-Zugriffe liefert. Nirgendwo sonst, außer in der DB-Klasse, tauchen irgendwelche mysql-Befehle auf. Wobei: mysql_*-Befehle tauchen sowieso besser nirgendwo mehr auf, nutze mysqli_* stattdessen! Die MySQLi-Erweiterung kann viel mehr, als das veraltete MySQL-Interface.

        Diese Datenbankklasse sollte als Singleton ausgeführt werden, damit sichergestellt ist, dass dein Skript wirklich nur eine Instanz der DB-Klasse, und damit nur eine Connection zur DB, aufbaut, und darüber den gesamten Datenverkehr fließen läßt. Ausnahmen wären nur erlaubt, wenn du auf mehr als eine Datenbank zugreifen mußt - da wäre es dann aber je Datenbank ein Singleton.

        Da die Datenbankklasse nur funktionieren kann, wenn die Datenbank existiert und funktioniert, ist die DB-Klasse in der Teststrategie als "Border-Klasse" (d.h. mit Kontakt zu nicht fakebaren externen Einrichtungen) zu separieren und abgetrennt von den "inneren" Klassen zu testen. Das SimpleTest-Tutorial erklärt dir näheres.

        Die erste spannende Design-Entscheidung beim Programmieren: Wo stehen die SQL-Querys, die die DB-Klasse letztendlich ausführen soll.

        Wenn das Datenmodell klein, überschaubar und garantiert nicht erweiterbar ist, könnte man die Querys auch noch mit in die DB-Klasse packen. Das ist aber keine gute Idee. Erstens bläht sich die Klasse dann mit Methoden auf, die im Zweifel doch immer mehr werden. Zweitens lassen sich, wie erwähnt, "Border-Klassen" nur schlecht testen, man muß die DB real laufen haben, und echte Querys kosten Zeit.

        Deshalb ist es schlauer, davor eine Art Layer zu packen, der in der Realität die DB-Klasse verwendet (und zum Testen ein DB-MockObjekt, dass vorprogrammiert einfach mit realen Daten antwortet, dabei aber viel schneller ist, als ein echter Query - und auch keine laufende DB benötigt), und seinerseits die benötigten Querys kennt. So eine Layer-Klasse könnte beispielsweise dafür sorgen, dass auf Anfrage einer anderen Klasse ein User-Objekt entsteht und zurückgegeben wird, welches aus der Datenbank alle notwendigen Daten enthält (wie Username, Mailadresse, Rechte, etc.). Da es vermutlich höchst unterschiedliche Abfragen aus der Datenbank zu unterschiedlichen Themengebieten gibt, wird es mit Sicherheit auch mehr als eine solche Layerklasse geben.

        Was noch typische Dinge sind, die sich für eine Klasse eignen:

        • Sessiondatenzugriff. Zwar könnte man auch $_SESSION direkt verwenden, aber diese Zugriffsart erfordert wieder, dass eine Session läuft, die erstmal gestartet werden muß, und die Drumherum benötigt - also: Border-Klasse für den Zugriff auf $_SESSION, und alles schön objektorientiert kapseln. Fühlt sich dann deutlich besser an.

        • Zugriff auf Browserdaten ($_GET, $_POST): Eines der unabdingbarsten Arbeiten mit diesen Daten ist die Validierung. Ich habe mir mal eine Validierungsklasse geschrieben, der ich ein Array mit den vorzunehmenden Tests übergebe, und das zu prüfende Datenarray (z.B. $_POST). Das Objekt erlaubt mir dann, abzufragen, ob die Daten valide sind (sind alle Felder vorhanden, fehlen welche, erfüllen alle Felder die Anforderungen), und liefert mir auch in jedem Falle in einem Ergebnisarray immer alle Felder, auf die ich teste, selbst wenn sie in den Ausgangsdaten nicht vorhanden sind. Das macht das Affenformular-Wiederausfüllen sehr praktisch, da ich nicht mehr testen muß, ob das Feld $_POST['name'] nun existiert, oder nicht (nichtexistente Arrayfelder liefern einen NOTICE-Fehler, und sowas vermeide ich grundsätzlich).

        Es hängt von der Komplexität der Anwendung ab, ob nach den Abfragelayern noch eine weitere Schicht Klassen kommt, die die Abfragen zu sinnvollen komplexen Dingen zusammensetzt, oder ob die dadurch geschaffene Struktur schon ausreicht, dass man im Hauptprogrammteil einfach nur noch die jeweils notwendigen Klassen instanziiert, mit Daten versorgt, und das Resultat dann der Template-Engine rüberreicht und die Ausgabe veranlaßt.

        also bevor ich deine Antwort gelesen habe, habe ich folgendes geschrieben:

        class prod_such_zuo {

        private $array_suchname_ids = array();

        function prod_such_zuo($produkt_id) {
                $erg = mysql_query(
                    "SELECT suchname_id " .
                    "FROM prod_such_zuo " .
                    "WHERE produkt_id='$produkt_id'"
                );
                while($row = mysql_fetch_object($erg))
                    $this->array_suchname_ids[] = $row->suchname_id;
            }

        public function get_array_suchname_ids(){
                return $this->array_suchname_ids;
            }

        }

        
        >   
        > Macht das Sinn? Ist das eventuell das was du gemeint hast mit dem speichern in einem Array? Geht es besser?  
          
        Es ist schlecht, den DB-Query direkt in der Klasse zu haben. Nirgendwo wird, falls noch nicht geschehen, die DB-Connection gestartet - sowas würde eine DB-Singleton-Klasse, die du dieser prod\_such\_zuo-Klasse übergibst, im Zweifel selbsttätig erledigen.  
          
        Zweitens würfelst du PHP4- und PHP5-Objektorientiertheit durcheinander. In PHP 4 mußte der Konstruktor als Funktion den Namen der Klasse tragen, und es gab keinen Destruktor. In PHP 5 heißt der Konstruktor als Funktion immer "\_\_construct", und der Destruktor "\_\_destruct" (dass die PHP4-Variante noch funktioniert, liegt an der Abwärtskompatibilität - so zu programmieren ist aber keine gute Idee mehr, PHP 4 ist offiziell tot seit dem 8.8.2008).  
          
        Drittens ist die Frage: Was willst du mit dieser Suchnamenzuordnung bezwecken? Wenn du in der DB eine n:m-Zuordnung hast, so wirst du dennoch selten in EINEM Objekt die gesamte Menge n der einen Tabelle abfragen und deshalb gezwungen sein, auch noch die gesamte Menge m der anderen Tabelle mit auszulesen.  
          
        Üblicher erscheint mir stattdessen, dass ein "Hauptobjekt" aus der Tabelle n ausgelesen wird, und alle damit tatsächlich (über die n:m-Hilfstabelle) verknüpften Einträge aus m werden in dieses Objekt ebenfalls hineingetan, z.B. als Array.  
          
        Und wenn sich an diesem Objekt durch Benutzerinteraktion irgendetwas verändern sollte, so weiß das Objekt auch direkt, wie es diese Änderung wieder in die DB-Konstruktion der n:m-Relation speichern kann, ohne dass du selbst dich auf dieser Ebene der Applikation mit den Details rumärgern mußt.  
          
        Und schon sparst du dir zwei Klassen und viele unnütze Objekte.  
          
         - Sven Rautenberg
        
        -- 
        "Love your nation - respect the others."
        
        1. Moin!

          Hallo,

          danke erstmal dafür das du dir die Zeit genommen hast, mir eine so auführliche Antwort zu schreiben.

          Ich empfehle dir wirklich sehr, dass du dir mal die verlinkte Seite durchliest und dir das Framework herunterlädst.

          Danke für den Tip. Ich werde mir das auf jedenfall genauer anschauen.

          Wenn du Datenbankzugriff hast, dann bedeutet das automatisch, dass du eine Datenbankzugriffsklasse hast

          Nein bis jetzt habe ich noch keine Klasse die alle Datenbankzugriffe regelt. Ist aber schon auf meiner ToDo Liste.

          Nirgendwo sonst, außer in der DB-Klasse, tauchen irgendwelche mysql-Befehle auf.

          Die mysql-Befehle werd ich aus meinen Klassen entfernen. Das wird dann gleich in einem rutsch mit der Datenbankzugriff-Klasse gemacht.

          Die erste spannende Design-Entscheidung beim Programmieren: Wo stehen die SQL-Querys, die die DB-Klasse letztendlich ausführen soll.

          Wenn das Datenmodell klein, überschaubar und garantiert nicht erweiterbar ist, könnte man die Querys auch noch mit in die DB-Klasse packen. Das ist aber keine gute Idee. Erstens bläht sich die Klasse dann mit Methoden auf, die im Zweifel doch immer mehr werden. Zweitens lassen sich, wie erwähnt, "Border-Klassen" nur schlecht testen, man muß die DB real laufen haben, und echte Querys kosten Zeit.

          Deshalb ist es schlauer, davor eine Art Layer zu packen, der in der Realität die DB-Klasse verwendet ... Da es vermutlich höchst unterschiedliche Abfragen aus der Datenbank zu unterschiedlichen Themengebieten gibt, wird es mit Sicherheit auch mehr als eine solche Layerklasse geben.

          Also ein Klasse die für eine andere Klasse per Anfrage entsprechende Daten aus der DB zieht und diese dann zurück gibt?

          Klasse User will DB Daten -> Anfrage an Klasse Layer_User -> DB Abfrage -> DB Daten als Objekt an User zurückgeben

          Hab ich das richtig verstanden?

          Was noch typische Dinge sind, die sich für eine Klasse eignen:

          • Sessiondatenzugriff.
          • Zugriff auf Browserdaten ($_GET, $_POST)

          Das steht ebenfalls schon auf meiner ToDo-Liste

          $erg = mysql_query(
                      "SELECT suchname_id " .
                      "FROM prod_such_zuo " .
                      "WHERE produkt_id='$produkt_id'"
                  );

          Es ist schlecht, den DB-Query direkt in der Klasse zu haben.

          Da wären wir doch wieder bei der Layer Lösung, oder?

          Zweitens würfelst du PHP4- und PHP5-Objektorientiertheit durcheinander. In PHP 4 mußte der Konstruktor als Funktion den Namen der Klasse tragen, und es gab keinen Destruktor. In PHP 5 heißt der Konstruktor als Funktion immer "__construct", und der Destruktor "__destruct"

          Danke, dass hab ich nicht gewusst. Habe das bisher immer so gemacht und seid ich eclipse nutze wird das schon automatisch so eingefügt.

          Werd ich ändern, vor allem gefällt mir die __construct Variante eh besser.

          Üblicher erscheint mir stattdessen, dass ein "Hauptobjekt" aus der Tabelle n ausgelesen wird, und alle damit tatsächlich (über die n:m-Hilfstabelle) verknüpften Einträge aus m werden in dieses Objekt ebenfalls hineingetan, z.B. als Array.

          Also ein zweidimensionales Array das als Objekt zurückgegeben wird und alle Beziehungen zwischen m und n enthält?

          Das Hauptobjekt müsste mir ja genau das zurückgeben was ich in dem Moment suche. z.b. wenn ich n angebe will ich alle m die zu n gehören. Genauso andersrum.

          Soll ich da zwei seperate Klassen erstellen oder eine flexible?

          • Sven Rautenberg

          Vielen Dank nochmal für deine Mühe.

          Gruß
          Andre

          1. echo $begrüßung;

            $erg = mysql_query(
                        "SELECT suchname_id " .
                        "FROM prod_such_zuo " .
                        "WHERE produkt_id='$produkt_id'"
                    );
            Es ist schlecht, den DB-Query direkt in der Klasse zu haben.

            Ebenso schlecht ist, keine kontextgerechte Behandlung von Werte vorzunehmen und sich damit eine potenzielle SQL-Injection-Lücke einzubauen. Wenn du schon auf mysqli umsteigst, informiere dich doch mal über das Thema Prepared Statements. Damit umgehst du (meistens) die Notwendigkeit Statements mit variablen Teilen zu versehen, die beim Einfügen zu behandeln sind.

            echo "$verabschiedung $name";

    2. Hallo,

      Meine Klassen orientieren sich immer an der zu erledigenden Aufgabe - nicht an der Datenbank.

      nochmal ne Frage dazu.
      Was genau bedeutet das? Mir fällt es immer noch sehr schwer, eine sinnvolle Klassenstruktur aufzubauen.

      Gibt es da gewisse Richtlinien die ich befolgen kann?

      z.b. was alles in eine Klasse gehört und was nicht. Wie Klasse am sinnvollsten miteinander verknüpft werden, usw..

      Auf was achten man da am besten. Woran erkenne ich für was ich eine Klasse benötige?

      Danke, Andre.

      1. Moin!

        Meine Klassen orientieren sich immer an der zu erledigenden Aufgabe - nicht an der Datenbank.

        nochmal ne Frage dazu.
        Was genau bedeutet das? Mir fällt es immer noch sehr schwer, eine sinnvolle Klassenstruktur aufzubauen.

        Gibt es da gewisse Richtlinien die ich befolgen kann?

        z.b. was alles in eine Klasse gehört und was nicht. Wie Klasse am sinnvollsten miteinander verknüpft werden, usw..

        Auf was achten man da am besten. Woran erkenne ich für was ich eine Klasse benötige?

        Da du mein anderes langes Posting gelesen hast, hier nur ganz kurz:

        Wie man Klassen gut zusammenführt, ist auch Erfahrungssache. Und es gibt keinesfalls nur immer genau eine einzige Lösung.

        Sei davor gewarnt, immer eierlegende Wollmilchsäue zu produzieren, die für alle Eventualitäten gewappnet sind, auch wenn dieser Fall noch gar nicht absehbar ist. Programmiere das und NUR das, was aktuell das Problem ist, mit der geringstmöglichen Komplexität.

        - Sven Rautenberg

        --
        "Love your nation - respect the others."
    3. Hallo Sven,

      Würde ich gar nicht tun.
      Meine Klassen orientieren sich immer an der zu erledigenden Aufgabe -
      nicht an der Datenbank.

      Das sind zwei grundsaetzlich verschiedene Ansaetze: zum einen der datenbankbasierte, und zum anderen der anwendungsbasierte. Beide Ansaetze koennen jedoch auch aufeinander aufbauen, so dass man einen Mehrwert erhaelt. Nicht selten dient ersterer als grundsaetzliche Basis. Auf diesem Layer aufbauend kann man dann die Businesslogik und -Objekte erstellen.
      Ich zum Beispiel mag es lieber, wenn ich eine Objektabbildung der Datenbanktabellen als Grundlage habe. Bei diversen Persistenceframeworks wird es ja groesstenteils auch so implementiert. Das heisst, man arbeitet in diesem Fall mit mehreren Layern: Dem Persistencelayer, dem Persistence-Adapter und dem Businesslayer. (Hinzu kommt dann schliesslich noch die Repraesentationsschicht). Erstere ist eine identische Abbildung der Datenbankbeziehungen. Das heisst im Falle der Tabelle Person und Adresse, besaesse das Objekt Person ein Referenzobjekt auf die Adresse. Im Code der Klasse Person existiere also eine GetAdresse-Methode, die das zugehoerige Adress-Objekt liefert. Dieses wiederum besaesse zum Beispiel ein Dependency-Objekt, was uns das ihm zugewiesene Country-Objekt liefern wuerde.
      Wie bereits erwaehnt, mag ich diesen Ansatz, da man zum einen recht leicht anhand des Datenbankschemas arbeiten kann und, zum anderen, die einzelnen Schichten gut voneinander getrennt und gekapselt werden.

      MfG,
      Sympatisant

      --
      "Non dura iubeantur, non prohibeantur inpura."
  2. Hallo Andre

    [produkt]
    id
    nr
    anlagedatum
    lastmodified

    [suchname]
    id
    name

    [prod_such_zuo]
    produkt_id
    suchname_id

    Wie bilde ich das auf Klassen in meinem PHP-Code ab?

    Meiner Ansicht nach fängst Du am falschen Ende an. Die DB-Tabellen zu erstellen bzw. sich einer Datenstruktur bewusst zu werden ist anfangs sicher nicht verkehrt. Aber ich würde daraus keine Klassen ableiten.

    Meine Überlegung wäre folgende:
    Welche Objekte werde ich voraussichtlich haben?
    Produkte, Suchanfragen
    Also würde ich 2 Klassen entwerfen: Produkt, Suchanfrage
    Möglicherweise bietet sich dann bei möglicher Spezialisierung von beiden die Entwicklung von Kindklassen derselben an (spezielle Produkte, spezielle Suchanfragen)

    Eine Methode in Produkt-Objekt könnte dann bei Bedarf auf das Suchanfrage-Objekt zugreifen oder umgekehrt.

    Ich habe Anfangs immer den Fehler gemacht, zuviel (Spezielles) in eine einzige Klasse zu packen, sodass sie schnell viel zu speziell auf eine einzige Anwendung festgelegt war. Vor dem Hintergrund der Wiederverwendbarkeit macht es aber Sinn das Vererbungsprinzip von vorneherein zu berücksichtigen.

    'Suchanfragen' könnte also z.B. schon wieder eine Kindklasse einer (allgemeineren) DB-Zugriffsklasse sein.

    Gruß vom foomaker