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.
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ę.
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:
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:
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ę.
Jest kilka sposobów aby wyciągać rekordy z bazy danych. Najszybszym jest użycie magicznych metod find, udostępnianych przez Doctrine_Table:
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.
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)
Aby usunąć jakiś obiekt z bazy danych wystarczy użyć metody delete().
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ą:
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:
oraz wyciągnięcie wszystkich produktów z kategorii programowanie:
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
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.
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
chodzi o to, że przy zmianach w pliku YAML, i poleceniu build-all-reload (czy jakimkolwiek odpowiedniku) wszystkie dane z tabel są tracone :(
pozdrawiam
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
“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.
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 ;)
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.
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!
@Przemek:
http://64.233.183.104/search?q=cache:VgF7RrxWxQIJ:manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutorial_tags.property.pkg.html+phpdoc+%40property&hl=pl&ct=clnk&cd=1&gl=pl
a poza tym:
/* @var $product Product */
$product = Doctrine::getTable(”Product”) -> find(1); // Doctrine