luq techblog

o tworzeniu słów kilka…

Obiektowe odwzoraowanie struktury bazy danych 23 czerwca 2009

Filed under: PHP,Programowanie — Łukasz @ 10:25
Tags: , ,

Znów dość długa przerwa we wpisach – nie było czasu, nie było o czym pisać. Dziś jednak wpadł mi do głowy pomysł na wpis. Tematem będzie problem „rozwiązany” (właściwie trudno to nazwać problemem, ale niech i tak zostanie) przeze mnie już jakiś czas temu, a wszystko związane jest ze zleceniem, które aktualnie wykonuje. Zlecenie to, nie jest zwykła stroną www – reklamówką jakiejś firmy etc. – bardziej jest to aplikacja webowa, a jeszcze konkretniej, nazwałbym to wyszukiwarką.

 

Wyszukiwarka rządzi się swoimi prawami, właściwie wszystko czego nam potrzeba jest zapisane w bazie danych. Teraz pojawia się problem, jak najlepiej stworzyć model obiektowy do operacji na danych pochodzenia z bazy danych? Przy prostych stronach tworzenie obiektów z tabelek zawartych nie ma sensu – konkretnie jeśli obiektów nie musimy wrzucać do innych obiektów, czy nie mamy dość trudnych relacji do zrealizowania. Pewnie dziwnie to brzmi, ale już tłumacze.

 

Przypuśćmy, że musimy napisać coś takiego, bardzo prosty przykładzik:
W bazie musimy przechowywać dane o książkach, wyrazach, oraz autorach książek. Do tego każda książka zawiera zbiór słów, a każde słowo może mieć synonimy. A więc tworzymy takie tabelki w bazie:

 

book_name:
bookId | title | authorId

author_name:
authorId | name | surname

word_name:
wordId | name

word_synonim:
wordId1 | wordId2

book_words:
bookId | wordId

 

Teraz chcemy szybko i łatwo edytować dane tabelki. Oczywiście tworzymy obiekty (w naszej koncepcji obiekt będzie równoważny z rekordem w bazie lub kilkoma rekordami). Naszymi obiektami będą: książka, autor oraz wyraz. Każdy z tych obiektów będzie bardzo podobny, no bo przecież poza kilkoma dodatkowymi funkcjami każdy można edytować – do tego nam posłuży metoda edit(), usuwać – delete() oraz dodawać add(). Oczywiście każdy będzie miał odpowiednie właściwości prywatne, każdy swoje id oraz resztę zależną od pozostałych pól rekordu, a także potrzeb (obiekt autora – name i surname)

 

W ten sposób powstały pierwsze metody wszystkich tych klas (nazwane przeze mnie metodami administracyjnymi – bo tylko w PA takie opcje powinny być dostępne).
Metoda statyczna add() będzie przyjmowała tablicę paramentrów, zwracać natomiast będzie obiekt np. do autorów będzie to wyglądać w taki sposób:

    $oAuthor = Author::add(array(
        'name' => 'foo',
        'surname' => 'bar'
    ));

 

Następnie chcemy zedytować rekord, nie trzeba edytować wszystkich danych:

    $oAuthor->edit(array(
        'name' => 'bla'
    ));

Fajnie jest zrobić inteligentną funkcję, tzn. porównująca czy dane przekazane w parametrze są inne od właściwości, jeśli są identyczne – nie potrzeba edytować obiektu, co za tym idzie nie trzeba wysyłać zapytania UPDATE do bazy. Dodatkowo należy zadbać aby po zedytowaniu obiektu, jego właściwości zmieniły się! Jeśli tego nie zrobimy, otrzymamy niespójność, obiekt będzie mówił co innego, baza co innego.

 

Usuwać rekord będziemy za pomocą:

    $oAuthor->delete();
    unset( $oAuthor );

Metoda usuwa rekord, natomiast nie można usunąć obiektu w swoim wnętrzu! Kod:

    public function delete(){
        // wywal rekordy w bazie
         $this = NULL;
    }

Nie przejdzie. Szczerze powiedziawszy nie podoba mi się taki sposób, bo może prowadzić do błędów, zapomnimy o unset() i mino że rekordu w bazie już nie ma to my mamy jeszcze obiekt.

    $oAuthor->delete();
    //unset( $oAuthor ); <- tego zapomnimy
    $oAuthor->edit([...]); // !?

Można to napisać w ten sposób:

    public function delete(){
        // wywal rekordy w bazie
         return NULL;
    }
    $oAuthor = $oAuthor->delete();

Ale, dalej nie rozwiązał się problem z możliwością powstania sporych błędów. Jedynym rozwiązaniem jest napisanie funkcji do tego celu. Jednak takie rozwiązanie jeszcze bardziej mi się nie podoba.

 

Kolejnymi metodami są statyczne metody pobierające- get(), getAll(), getSite() i dodatkowo getCount().
Metoda get() pobiera rekord o podanym w parametrze id oraz zwraca ten obiekt, kiedy rekordu nie ma zwraca NULL.

   $oAuthor = Author::get( 1 );

Metoda getAll() pobiera wszystkie rekordy, tworzy tablice obiektów i ją zwraca. Natomiast getSite() pobiera stronę obiektów i zwraca ją jako tablicę, gdy podamy parametr 2 dostaniemy obiekty od 21. do 40. (ORDER BY authorId LIMIT (…)) oczywiście zależne jest to od wartości stałej, która określa ile ma być elementów na stronie. W tym wypadku stała jest ustawiona na 20. getCount() jak się nie trudno domyśleć, zwraca ilość rekordów.

 

Oczywiście każdy obiekt będzie miał metody odpowiedzialne za zwrócenie prywatnych właściwości (tzw. gettery). Każdy będzie miał metodę getId() oraz właściwe dla siebie, o czym była już mowa wcześniej.

    $aAuthors = Author::getAll();
    
    $cnt = count( $aAuthors );
    for( $i = 0; $i < $cnt; $i++ ){
        echo $aAuthors[$i]->getId().' : '.$aAuthors[$i]->getName().' - '.$aAuthors[$i]->getSurname().'
'; }

Dodatkowo, stworzyłem statyczną metodę setDataBase(), która wrzuca obiekt bazy do właściwości statycznej i jest wykorzystywane do operacji z bazą danych. Tylko z tego powodu tak to jest skonstruowane, że model który ja pisałem jest postawiony na moi silniku (o którym można przeczytać we wcześniejszym wpisie), gdzie jest możliwość korzystania z kilku serwerów bazodanowych.

    Author::setDataBase( new DataBase() );
    // new DataBase() bez paramentów tworzy bazę danych 
    // wykorzystując dane zapisane z configu
    $oAuthor = Author::get( 1 );

Ostatnią metoda, jest check(), sprawdzający czy rekord o podanych danych istnieję, jeśli tak zwraca jego id, jeśli nie – false. Metodzie należy podawać tablicę asocjacyjną z polami, które tworzą niepowtarzalny rekord, tak, tak, wiem – id robi nam niepowtarzalne rekordy, ale nie chcemy mieć czegoś takiego:
author_name:

    authorId | name | surname
       1     | foo  |   bar
       2     | foo  |   bar

Tutaj każda para name – surname musi być inna, (np. przy userach można niepowtarzalność rozpoznawać po mailu). Metoda przydaje się w przypadku gdy, tworzymy jakiś obiekt, do którego należy wrzucić inny obiekt aby go stworzyć. W takim wypadku w interfejsie użytkownika pojawia się lista z której można wybrać obiekt będący częścią tego głównego, ale można też dodać pole, gdzie wpisuje się nazwę obiektu. Jeśli obiekt wybierzemy z listy, jesteśmy pewni, że on istnieję. Jednak w przypadku wpisania, może on istnieć, ale może użytkownik chce stworzyć nowy – takiego którego jeszcze nie ma.

    $check = Author::check(array(
        'name' => 'foo',
        'surname' => 'bar'
    ));

    if( $check === false ){
        $oAuthor = Author::add(array(
            'name' => 'foo',
            'surname' => 'bar'
        ));
    }
    else{
        $oAuthor = Author::get( $check );
    }

Ostatnią, nie opisaną jeszcze metodą jest konstruktor, który jest metodą prywatną, przez co, obiekt możemy tworzyć jedynie metodą add() – gdy chcemy stworzyć nowy rekord w bazie, oraz get(), getAll(), getSite() – gdy chcemy pobrać obiekty już istniejące. Jedynym zadaniem naszego konstruktora jest ustawianie właściwości. Dla przykładu, konstruktor klasy Author będzie wyglądał następująco:

    private function __construct( $aData ){
         $this->id = $aData['authorId'];
         $this->name = $aData['name'];
         $this->surname = $aData['surname'];
    }

Od drugiej strony – metoda get() – wyglądać to będzie tak:

    static public function get( $id ){
        // Pobieranie z bazy nas nie interesuje
        // ale w gwoli wyjaśnienia - $db to obiekt wrzucony
        // przez metodę setDataBase(), select zwraca tablice 2d -
        // $array[kolejny_rekord:integer][pole_nazwane_jak_w_db:string]
        $q = self::$db->select( 
            array( 'name', 'surname' ),
            'author_name',
            "WHERE authorId = '$id'"
        );

        if( $q ){
            return new self( $q[0] );
        }
        else{
            return NULL;
        }
    }

I to już wszystkie standardowe metody klas. Dla klasy Author tyle wystarczy, jednak Word powinien wiedzieć o swoich synonimach, a Book wiedzieć z jakich Word-ów się składa, dodatkowo powinniśmy zapewnić możliwość operacji na tych relacjach. Tak więc dla Word powinniśmy dopisać metodę addSynonim(), którą będzie dodawać do tabelki word_synonim $this->getId() oraz id podanego w parametrze obiektu, oraz oczywiście dodawać obiekt podany jako parametr do specjalnie do tego stworzonej właściwości – $aSynonims

    public function addSynonim( Word $oWord ){
        // dodaj do tabelki word_synonim rekord 
        // NULL | $this->getId() | $oWord->getId() 
        array_push( $aSynonims, $oWord );
    }

Przyda się także metoda deleteSynonim() która oczywiście będzie usuwać relacje synonimu z podanym obiektem, dodatkowo należy zadbać aby usuwając obiekt Word były usuwane wszystkie relacje synonimów z tym obiektem!
Przyda nam się metoda wczytująca synonimy gdy obiekt jest tworzony metodą pobierającą (get(), getAll(), getSite()), bo tworząc obiekt, dodajemy do niego także obiekty synonimów, a więc wtedy $aSynonims jest już zapełniona. Piszemy metodę setSynonims(), która wysyła odpowiednie zapytanie do bazy i wynikami zapełnia tablicę $aSynonims.

 

Można byłoby wywoływać metodę setSynonims() w konstruktorze, tyle że taki sposób mógłby bardziej zwolnić niż przyśpieszyć pracę. Gdybyśmy chcieli tylko wypisać wszystkie słowa z bazy:

    $aWords = getAll();
    $cnt = count( $aWords );
    for( $i = 0; $i < $cnt; $i++ ){
        echo $aWords[$i]->getName().'
'; }

W konstruktorze byłoby tworzone i pobierane dla każdego obiektu synonimy co przy 100 słowach daje 100 kolejnych zapytań! A nie wykorzystujemy przecież w tym momencie synonimów, więc nie są one nam potrzebne. Gdy chcielibyśmy napisać coś takiego jak powyżej należałoby raczej.

    $aWords = getAll();
    $cnt = count( $aWords );
    for( $i = 0; $i < $cnt; $i++ ){
        $aWords[$i]->setSynonims(); // pobieramy synonimy możemy potem także je wypisać
        echo $aWords[$i]->getName().'
'; }

No właśnie potrzebna nam jest także getter dla synonimów – getSynonims() zwracający tablice $aSynonims, i to wszystko. Dla klasy Book powinniśmy stworzyć podobny do wyżej przedstawionego zestaw metod – addWord(), deleteWord(), setWords(), getWords(). Dodatkowo w konstruktorze powinniśmy pobrać obiekt Author, stworzyć go i ustawić.

 

Przykładowy kod.

    $db = new DataBase();
    Book::setDataBase( $db );
    Word::setDataBase( $db );
    Author::setDataBase( $db );
    
    // pierwsze słowa
    $oWord1 = Word::add(array(
        'name' => 'kolor'
    ));
    $oWord1->addSynonim(
        Word::add(array(
            'name' => 'barwa'
        ))
    );
    
    // kolejne
    $oWord2 = Word::add(array(
        'name' => 'OOP'
    ));
    $oWord3 = Word::add(array(
        'name' => 'Programowanie obiektowe'
    ));
    $oWord4 = Word::add(array(
        'name' => 'Object oriented programing'
    ));
    $oWord2->addSynonim(
        $oWord3
    );
    $oWord2->addSynonim(
        $oWord4
    );
    $oWord3->addSynonim(
        $oWord4
    );
    
    // książka
    $oBook = Book::add(array(
        'title' => 'tytuł',
        'authorId' => Author::add(array(
            'name' => 'foo',
            'surname' => 'bar'
        ))->getId()
    ));
    $oBook->addWord( $oWord1 );
    $oBook->addWord( $oWord2 );

Powinniśmy też sprawdzać metodą check() czy podane rekordy już istnieją, jeśli tak to korzystać już z tych istniejących, ale my jesteśmy pewni, że mamy pustą bazę więc napisaliśmy to w ten sposób. Teraz piszemy:

    $db = new DataBase();
    Book::setDataBase( $db );
    Word::setDataBase( $db );
    Author::setDataBase( $db );
    
    $oBook = Book::get( 1 );
    $oBook->setWords();
    
    $aWords = $oBook->getWords();
    $cnt = count( $aWords );
    for( $i = 0; $i < $cnt; $i++ ){
        echo '<strong>'.$aWords[$i]->getName().'</strong>
'; $aWords[$i]->setSynonims(); $aSynonims = $aWords[$i]->getSynonims(); $cnt2 = count( $aSynonims ); for( $j = 0; $j < $cnt2; $j++ ){ echo $aSynonims[$j]->getName().'
'; } }

Myślę, że przedstawiony sposób przerzucenie bazy danych na model obiektowy, w tym przypadku PHP, jest bardzo wygodny. Poza tym, ten wpis, chyba, może pomóc młodym adeptom OOP, którzy na początku tworzą klasy jako opakowanie na zestaw funkcji, nie rozumiejąc do końca idei OOP. Nie twierdzę, że przedstawiony powyżej model jest idealny, można być coś jeszcze dodać – jak choćby zestaw setterów wykorzystywanych w konstruktorze oraz w metodzie edit() do aktualizacji właściwości. Jeśli ktoś widzi jakieś ograniczenie wynikające z kodu czy ma jakiekolwiek słowa komentarza piszcie

Reklamy
 

2 Responses to “Obiektowe odwzoraowanie struktury bazy danych”

  1. tommy Says:

    Mając już pewne doświadczenie z Symfony, a szczególnie z Doctrine, radziłbym Ci we własnym frameworku „własnoręcznie” implementować model każdej tabeli – najlepiej byłoby zrobić sobie podstawową klasę, np. CBaseTable, która implementowałaby zwykły CRUD dla konkretnej tabeli z jej prywatnej definicji, a dla danych powiązanych z różnych tabel mógłbyś własnoręcznie edytować wykonywane zapytania. Chodzi o to, że i tak sam nie zrobisz czegoś na kształt współczesnych ORMów, a będziesz miał dalej bardzo dobrą kontrolę nad przetwarzaniem tych danych.

  2. luq Says:

    Nowy obiektowy model danych został już zbudowany, jest całkiem niezły… zresztą w najbliższym czasie (czytaj. jak znajdę chwilę) opiszę pokrótce jak to działa.


Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s