Doctrine - ORM dla PHP

Po pewnym czasie obcowania z bazą danych, ciągłe pisanie zapytań oraz ich przetwarzanie może stać się strasznie męczące. Dodatkowo pojawia się problemu z kompatybilnością baz danych

Na przeciw temu wszystkiemu wychodzą ORMy. ORM czyli Object-Relational Mapping jest to rozwiązanie w którym dane są mapowane i zwracane w postaci obiektów. Powstało kilkanaście rozwiązań ORM dla PHP. Najpopularniejsze to Propel oraz Doctrine, osobiście w projektach używam tego drugiego i tą notką postaram się przybliżyć wam użycie tego ORM.

Rekordy

Przed napisaniem pierwszego zapytania musimy stworzyć wcześniej rekordów. Stwórzmy sobie bazę danych posiadającą 3 tabele: categories (id, name), companies (id, name) oraz products (id, name, description, price, company, category) na której będziemy operować bawiąc się Doctrine. Baza ta będzie przechowywać produkty, każdy z produktów może znajdować się w jednej kategorii oraz może być stworzony przez jedną firmę.

models/Category.php

  1. < ?php
  2.  
  3. class Category extends Doctrine_Record
  4. {
  5.     public function setTableDefinition()
  6.     {
  7.         $this->setTableName("categories");       
  8.         $this->hasColumn("name", "string", 45);
  9.     }
  10.  
  11.     public function setUp()
  12.     {
  13.         $this->hasMany("Product as Products", array("local" => "id", "foreign" => "category"));
  14.     }
  15. }
  16.  
  17. ?>

models/Company.php

  1. < ?php
  2.  
  3. class Company extends Doctrine_Record
  4. {
  5.     public function setTableDefinition()
  6.     {
  7.         $this->setTableName("companies");       
  8.         $this->hasColumn("name", "string", 45);
  9.     }
  10.  
  11.     public function setUp()
  12.     {
  13.         $this->hasMany("Product as Products", array("local" => "id", "foreign" => "company"));
  14.     }
  15. }
  16.  
  17. ?>

models/Product.php

  1. < ?php
  2.  
  3. class Product extends Doctrine_Record
  4. {
  5.     public function setTableDefinition()
  6.     {
  7.         $this->setTableName("products");
  8.         $this->hasColumn("name", "string", 45);
  9.         $this->hasColumn("description", "string");
  10.         $this->hasColumn("price", "decimal", 10);
  11.         $this->hasColumn("category", "integer");
  12.         $this->hasColumn("company", "integer");
  13.     }
  14.  
  15.     public function setUp()
  16.     {
  17.         $this->hasOne("Category", array("local" => "category", "foreign" => "id"));
  18.         $this->hasOne("Company", array("local" => "company", "foreign" => "id"));
  19.     }
  20. }
  21.  
  22. ?>

Tak wyglądają zadeklarowane rekordy. Każdy rekord musi dziedziczyć po Doctrine_Record. Pierwszą rzeczą jaką trzeba zrobić zrobić jest implementacja metody setTableDefinition, w której ustawiamy nazwę tabeli oraz deklarujemy kolumny jakie posiada (pole id jest deklarowane domyślnie). Następną rzeczą którą możemy uczynić jest ustawienie relacji zachodzących z rekordem. Do dyspozycji mamy 3 typu relacji: one-to-one (hasOne), one-to-many (hasMany) oraz many-to-many (o tej relacji kiedy indziej).

Wszystko pięknie, jednak gdy mamy do czynienia z dużą bazą danych gdzie operujemy na 20-30 tabelach, stworzenie rekordów dla bazy może być czasochłonne. Zresztą “przepisanie” struktury nawet dla 10 już jest męczące ;) Tutaj przychodzi z pomocą generator który udostępnia nam Doctrine. Może on stworzyć za nas wszystkie rekordy, a jego użycie sprowadza się do połączenia się z bazą danych oraz wywołania metody generateModelsFromDb, podając folder do którego mają być wygenerowane rekordy:

  1. < ?php
  2.  
  3. //Dołączenie główengo pliku Doctrine.
  4. require_once(‘./lib/Doctrine.php’);
  5. //Zarejestrowanie autoloadera Doctrine.
  6. spl_autoload_register(array(‘Doctrine’, ‘autoload’));
  7.  
  8. //Ustawienie połączenia
  9. Doctrine_Manager::connection(‘mysql://root:pass@localhost/doctrine’);
  10. //Wywołanie generatora
  11. Doctrine::generateModelsFromDb(‘models’);
  12.  
  13. //W folderze models mamy już gotowe do użycia rekordy
  14. ?>

Podstawowe operacje

Posiadając rekordy na których będziemy operować możemy przystąpić do działania. Zacznijmy od wypełnienia naszej bazy jakimiś danymi, by później móc wykonywać jakieś zapytania.

Użycie rekordów jest bardzo proste. Sprowadza się do stworzenia obiektu rekordu, następnie wypełnienia go danymi i wywołania metody save. Dodajmy pierwszy produkt:

Dodawanie

  1. < ?php
  2.  
  3.  
  4. //Dołączenie główengo pliku Doctrine.
  5. require_once(‘./lib/Doctrine.php’);
  6. //Zarejestrowanie autoloadera Doctrine.
  7. spl_autoload_register(array(‘Doctrine’, ‘autoload’));
  8. //Załadowanie wygenerowanych rekordów które znajdują się w folderze models
  9. Doctrine::loadModels(‘models’);
  10.  
  11.  //Ustawienie połączenia
  12.  $conn = Doctrine_Manager::connection(‘mysql://root:pass@localhost/doctrine’);
  13.  
  14.  $product = new Product();
  15.  $product -> name = "Zend Studio Professional Edition";
  16.  $product -> description = "IDE dla PHP";
  17.  $product -> price = 1500.00;
  18.  $product -> Company -> name = "Zend Technologies";
  19.  $product -> Category -> name = "Programowanie";
  20.  $product -> save();
  21.  
  22. ?>

Tworzymy nowy obiekt rekordu Product. Następnie ustawiamy po kolei wartości. W deklaracji rekordu zaznaczyliśmy że zachodzą relacje one-to-one z rekordami Category i Company. Dostęp do obiektów tych rekordów mamy poprzez właściwości o nazwie relacji. Mogli byśmy podać $product -> Company -> id = 1; ale na razie nie mamy żadnej kategorii ani firmy więc dodając produkt dodajemy nową kategorię oraz firmę.

Pobieranie

Jest kilka sposobów aby wyciągać rekordy z bazy danych. Najszybszym jest użycie magicznych metod find, udostępnianych przez Doctrine_Table:

  1. < ?php
  2.  $table = Doctrine::getTable("Product");
  3.  
  4.  $product = $table -> find(1);
  5.  $product = $table -> findOneByPrice(1500);
  6.  
  7.  echo "Produkt: ".$product -> name."<br />";
  8.  echo "Kategoria: ".$product -> Category -> name."<br />";
  9.  echo "Producent: ".$product -> Company -> name;
  10.  
  11.  $products = $table -> findByCompany(1);
  12.  $products = $table -> findAll();
  13.  
  14.  echo "<ul>";
  15.  foreach($products as $product)
  16.  {
  17.     echo "<li>".$product -> name."</li>";
  18.  }
  19.  echo "</ul>";
  20. ?>

Pierwsze dwie metody zwracają nam obiekt Product. Następnie wyświetlamy nazwę: produktu, kategorii oraz firmy. I tutaj trzeba zwrócić uwagę na bardzo ważną rzecz. Otóż stosując lazy-loading wywoływane są 3 zapytania, a nie jedno z JOINEM. A o tym jak pobrać w jednym zapytaniu to wszystko za chwilę (DQL).

Następnie znajdujemy wszystkie produktu, lub te gdzie company = 1. Zmienna products jest obiektem Doctrine_Collection, posiadającym pobrane rekordu. Jako że Doctrine_Collection implementuje interface ArrayAccess, oraz Iterator, możemy używać $products jako tablica.

Zmiana

Aby zmienić rekord używając obiektu rekordu, najpierw musimy pobrać rekord z bazy danych. Najprostszym sposobem jest użycie metody find która szuka po kluczu głównym (id)

  1. < ?php
  2.  $product = Doctrine::getTable("Product") -> find(1);
  3.  print_r($product -> toArray());
  4.  
  5.  $product -> name = "Zend Studio v.5 Professional Edition";
  6.  
  7.  //Update zmienionych wartości czyli tylko "name"
  8.  $product -> save();
  9.  print_r($product -> toArray());
  10. ?>

Usunięcie

Aby usunąć jakiś obiekt z bazy danych wystarczy użyć metody delete().

  1. < ?php
  2.  
  3.  $product = Doctrine::getTable("Product") -> find(1);
  4.  //Produkt został usunięty z bazy danych.
  5.  $product -> delete();
  6.  
  7. ?>

DQL - Doctrine Query Language

Magiczne DQL nie jest żadnym nowym językiem służącym do komunikacji z bazą danych - właściwie jest to trochę uproszczony SQL. Zapytania DQL pisze się za pomocą fluent interface, używając obiektu Doctrine_Query. Oto jak wygląda zapytanie które zwróci nam produkt wraz z jego kategorią oraz firmą:

  1. < ?php
  2.  $product = Doctrine_Query::create() -> from("Product p")
  3.                                      -> leftJoin("p.Category cat")
  4.                                      -> leftJoin("p.Company c")
  5.                                      -> where("p.id = ?", 6)
  6.                                      -> fetchOne();
  7.  
  8.  echo "Produkt: ".$product -> name."<br />";
  9.  echo "Kategoria: ".$product -> Category -> name."<br />";
  10.  echo "Producent: ".$product -> Company -> name;
  11.  
  12.  print_r($product -> toArray());
  13. ?>

Zasadniczą różnicą między DQL a SQL jest to że nie podajemy nazw tabel lecz nazwy rekordów. Dodatkowo przy JOINach nie musimy pisać ON col1 = col2 ponieważ Doctrine zna zarówno nazwę tabeli jak i nazwy kolumn między którymi zachodzi relacja. Na końcu wywołujemy fetchOne, chcąc dostać obiekt Product a nie obiekt Doctrine_Collection z jednym produktem :) Tak natomiast będzie wyglądać wyciągnięcie firmy oraz jej produktów:

  1. < ?php
  2.  $company = Doctrine_Query::create() -> from("Company c")
  3.                                      -> leftJoin("c.Products p")
  4.                                      -> where("c.id = ?", 1)
  5.                                      -> fetchOne();
  6.  
  7.  echo "Firma: ".$company -> name;
  8.  echo "<ul>";
  9.  foreach($company -> Products as $product)
  10.  {
  11.     echo "<li>".$product -> name."</li>";   
  12.  }
  13.  echo "</ul>";
  14. ?>

oraz wyciągnięcie wszystkich produktów z kategorii programowanie:

  1. < ?php
  2.  $products = Doctrine_Query::create() -> from("Product p")
  3.                                       -> leftJoin("p.Category cat")
  4.                                       -> where("cat.name = ?", "Programowanie")
  5.                                       -> execute();
  6.  echo "Produkty należace do kategori Programowanie";
  7.  echo "<ul>";                                     
  8.  foreach($products as $product)
  9.  {
  10.     echo "<li>".$product -> name."</li>";
  11.  }
  12.  echo "</ul>";
  13. ?>

Zaawansowane możliwości

Zaprezentowane wyżej możliwości to podstawowe funkcjonalności które oferuje nam ten ORM. Doctrine jest zaawansowanym narzędziem z którego można “wycisnąć” dużo więcej niż proste operacje CRUD oraz zapytania do bazy. O zaawansowanych możliwościach takich jak Event Listeners, Templates, Plugins, Cache oraz Migrations w następnej notce.

Jeżeli zachęciłem was do Doctrine to zapraszam do dokumentacji. Polecam ją przeczytaj od początku do końca aby dowiedzieć się jak ogromne możliwości oferuje nam to narzędzie. Jeżeli pojawią się jakieś problemy a dokumentacja nie wystarczy, pomoc zawsze znajdziecie na kanale #doctrine na serwerze freenode.net

Wszystkie listningi można ściągnąć i uruchomić na swoim komputerze.

Komentarze

  1. wojak (#) 05.04.2008 13:20

    rzeczywiście fajna sprawa, ale wciąż nie mogę zrozumieć jak modyfikować strukturę tabeli za pomocą pliku YAML, nie tracąc danych? dump do pliku to trochę w moim przypadku szaleństwo będzie, a znając życie będę musiał zmieniać te struktury często i gęsto.

  2. Strzałek (#) 05.04.2008 19:28

    Nie do końca rozumiem o co chodzi. W YAML można zadeklarować sobie strukturę tabeli (rekordu). Następnie można odpalić sobie z konsoli polecenie build-all-reload i stworzy nam tablę w bazie danych oraz nasze rekordy :)

    Dokumentacja: Doctrine Manual::Building Everything

  3. wojak (#) 05.04.2008 21:09

    chodzi o to, że przy zmianach w pliku YAML, i poleceniu build-all-reload (czy jakimkolwiek odpowiedniku) wszystkie dane z tabel są tracone :(
    pozdrawiam

  4. wojak (#) 05.05.2008 02:47

    pozwolę sobie jeszcze raz napisać. to o co mi chodziło to komendy:
    dump-data
    rebuild-all-reload

    jednak nie obędzie się bez zrzutu do plików.

    pozdrawiam ponownie

  5. Michał Mech (#) 05.16.2008 09:43

    “O zaawansowanych możliwościach takich jak Event Listeners, Templates, Plugins, Cache oraz Migrations w następnej notce.”

    Niestety to co zamierzasz umieścić w kolejnej notce to raczej gadżety. Doctrine niestety nie nadąża za Propelem już dużo wcześniej. Niestety w Doctrine mechanizmy relacji bardzo kuleją. Nie ich definiowanie, bo to jest akurat świetne. Bardzo łatwo i przyjemnie można łączyć tabele w dowolnych relacjach, neistety niewele później z tego wynika i większość zapytań trzeba budować za pomocą Doctrine_Query, co czyni definiowanie relacji bezużytecznym.

    Szkoda bo Doctrine już mogłoby zagrozić Propelowi, ale musimy na to poczekać jeszcze trochę czasu.

  6. Strzałek (#) 05.16.2008 14:20

    Tak jak napisałem, nie używałem Propela więc nie wiem. Mógł byś pokazać jakiś przykład Doctrine vs. Propel w którym wyraźnie wg. Ciebie Propel jest lepszy? Może następna notka będzie o Propelu ;)

  7. Michał Mech (#) 05.30.2008 16:50

    Strzałek poprosiłeś o przykład więc pokuszę się o jeden.
    Zarówno Doctrine jak i Propel potrafią łączyć obiekty w sensowne relacje, oczywiście jesli odpowiednio zdefiniujemy pliki schema (czy to .yml czy .xml). Załóżmy, że mamy obiekty User i Product z relacją gdzie użytkownik może mieć wiele produktów.
    W Doctrine przy poprawnej definicji obiektów możemy pobrać wszystkie produkty użytkownika tak: http://phpfi.com/320776

    Doctrine jest tak zaprojektowany, że oferuje nam wszędzie fluent interfaces co jest dodatkowym plusem.

    A jak to wygląda w Propelu? A tak: http://phpfi.com/320777

    Gdzie jest przewaga? Otóż wygenerowana funkcja getProducts() posiada opcjonalny parametr będący obiektem Criteria. Różnica błaha, ale istniejąca w całej filozofii oby ORMów. Dzięki takiemu szczegółowi w Propelu jesteśmy w stanie skorzystać z tak oczywistej funkcjonalności jak pobranie produktów usera ale … posortowanych. Robimy to przekazując dodatkowy obiekt do funkcji. A Doctrine nie da się tego zrobić. Trzeba zapytać ORM korzystając z obiektu Doctrine_Query i samodzielnie zbudować zapytanie, co generuje sporą ilość kodu i jest niewygodne. Zanika wtedy również sens definiowania relacji między obiektami.

    Ale żeby nie to że demonizuję. Doctrine posiada wiele ciekawych rzeczy, który nie ma Propel. Wspomniane wcześniej bajery ze zdarzeniami i ich nasłuchiwaniem. Fajna sprawa.

  8. Przemek (#) 10.19.2008 12:13

    Mi przeszkadza inna rzecz. Otóż wygenerowane obiekty przez Propel posiadają jawnie zdefiniowane settery/gettery (czy jak kto woli mutatory/akcesory) do wszystkich kolumn obiektu. W sumie mała rzecz, ale jeżeli używasz porządnego IDE (patrz Eclipse PDT) to posiadanie zdefiniowanych funkcji przyspiesza proces programowania (komu by się chciało pamiętać wszystkie kolumny np. ze 100 obiektów w projekcie). Zawsze lepiej jest eliminować takie błędy na etapie tworzenia kodu niż podczas długi sesji debugowania projektu.

    $product = Doctrine::getTable(”Product”) -> find(1); // Doctrine
    vs
    $product = ProductPeer::doSelectOne(someCriteria); // Propel

    W wersji Doctrine, żaden IDE do PHP nie będzie wiedział co zawiera $product, więc żadnego podpowiadania właściwości/metod dla $product, dla wersji Propel, Eclipse wyświetli mi całą zawartość tej klasy.

    Ludzie! Nie utrudniajcie sobie życia!

  9. @property (#) 10.31.2008 18:02

    a poza tym:

    /* @var $product Product */
    $product = Doctrine::getTable(”Product”) -> find(1); // Doctrine

Dodaj komentarz

wymagane
wymagane (nie będzie publikowane)