Blog

Wiedza

System ORM na przykładzie Doctrine2. Cz1 - Wstęp

Chcesz dowiedzieć się czegoś o ORM i Doctrine 2 - tutaj możesz zacząć.

Baza danych w aplikacji

Niemalże wszystkie aplikacje dostępne w sieci, od sklepów internetowych po proste wizytówki firmowe, w celu przechowania swojej zawartości wykorzystują szeroko stosowane relacyjne bazy danych. I to jest super, ponieważ użytkownicy korzystający z usług różnych aplikacji często muszą wpierw zasilić je jakimiś danymi. Każdy zgodziłby się z tezą, że sklep który wymagałby przy każdym zakupie wypełniania danych osobowych do wykonania transakcji nie cieszyłby się zbytnią popularnością. Zatem baza danych jest kluczowym składnikiem tworzonej aplikacji.

Baza danych w aplikacji

Obecne standardy tworzenia oprogramowania

A co możemy powiedzieć o samym procesie tworzenia aplikacji? 

Obecne standardy narzucają na twórców oprogramowania paradygmat Obiektowego programowania (object-oriented programming, OOP), który został zapoczątkowany w latach 1966-1967 Przez Alana Kaya

 

Połączenie podejścia OOP wraz z relacyjnymi bazami danych stwarza przed developerem pewne wyzwanie. Rozwijanie aplikacji w oparciu o OOP ma samo w sobie wiele zalet, których teraz nie będę wymieniał, lecz ten poziom abstrakcji jest ‘nie kompatybilny’ ze strukturą danych w relacyjnej bazie danych i trudności tej niekompatybilności występują w momencie zapisu lub odczytu danych z bazy przez aplikację.

 

Cóż robić w takiej sytuacji?

Jeśli dysponujemy odpowiednią wytrwałością i dużą ilością czasu możemy przygotować własny mechanizm mapujący, który będzie dla nas ‘spłaszczać’ obiektowy szkielet danych do formy rekordów (krotki) co pozwala na przechowywanie takich danych w bazie. Takie mapowanie musielibyśmy wykonywać dla wszystkich typów obiektów występujących w aplikacji i w obu kierunkach komunikacyjnych z bazą danych odczyt/zapis a to naprawdę dużo pracy i dużo zapytań sql do napisania…

 

Na szczęście Z ratunkiem w tej sytuacji przychodzi nam podejście ORM (Obiekty relacyjnie mapowane).

 

‘O co chodzi?’ Jest to dodatkowa warstwa abstrakcji, która załatwi za nas całą komunikację z bazą danych i bez specjalnego wysiłku przypilnuje integralności w niej danych. Z perspektywy programisty super sprawa - pracujemy tylko z obiektami, które mają ‘zdolność’ utrwalania swojego stanu w bazie danych. Tego typu obiekty możemy nazywać Encjami, gdyż są już obiektową reprezentacją fizycznie istniejących danych.

Czym jest ORM?

Ale nadal nie wiem czym jest Doctrine o którym wspomniano w tytule

Wiemy do czego służą bazy danych, poznaliśmy podejście OOP i ORM więc wreszcie nadszedł moment w którym możemy was sobie przedstawić.

Dotrine jest implementacją ORM - w naszym świecie PHP jest to najbardziej rozbudowane i najpopularniejsze narzędzie do obsługi komunikacji z bazą danych.

Doctrine jest jedynie narzędziem, które pozwala wykonać określone zadania. Jest w stanie zrobić to samo co my bylibyśmy w stanie zrobić przygotowując własne zapytanie SQL. Przewagą Doctrina jest to, że jest w stanie zaoszczędzić nam znacznego nakładu pracy, który musielibyśmy włożyć w przygotowanie dedykowanego systemu ORM dla naszej aplikacji.

Trochę o wadach ORM

Tak jak nie ma róży bez kolców tak wiadomo, że nie istnieją idealne rozwiązania do wszystkiego i dlatego trzeba pamiętać również o jego ograniczeniach.

Mogą zdarzyć się sytuacje w których będziemy potrzebowali zbudować bardzo złożone zapytanie do bazy zawierające instrukcje języka SQL jak JOIN, COUNT(*), HAVING, GROUP BY ORDER BY RAND(), które nie zapewnią nam wbudowane mechanizmy skojarzeń. W takiej sytuacji konieczne będzie zagłębienie się w specjalnym języku DQL (Doctrine Query Language). Jest to już jakaś porcja dodatkowej pracy, ale nadal uważam, że warto.

Drugim negatywnym efektem korzystania z Doctine z pewnością jest kwestia wydajności. Dopóki nasza baza nie przechowuje milionów rekordów to nie powinniśmy odczuwać niekorzystnych efektów zmniejszonej wydajności. Niestety, mniejsza ilość naszej pracy jest kompensowana zwiększonym zużyciem zasobów serwera.

Z czym to się je? Czyli praktyczne przykłady zastosowania ORM.

Załóżmy, że już mamy przygotowaną klasę Product

class Product{
private $id;
private $name;
private $createdAt;
}

Jak Doctrine rozpozna mój obiekt jako encję, która powinna być zapisywana w bazie danych?

No cóż, trzeba mu to trochę podpowiedzieć a w tym celu posługiwać się będziemy metadanymi dodanymi do naszej klasy.

/** @Entity */
class Product{
private $id;
private $name;
private $createdAt;
}

Teraz Doctrine już wie że klasa Product jest obiektem ORM i powinien mieć swoje miejsce w bazie danych czyli tabeli o tej samej nazwie Oczywiście jeśli zajdzie potrzeba możemy zmienić tabelę docelową dodając kolejną adnotację ‘@Table(name=”my_products”)’

Z czym to się je? Czyli praktyczne przykłady zastosowania ORM.

Mapowanie

Następnym etapem jest mapowanie właściwości. Celem jest przypisanie właściwościom naszej klasy odpowiednich typów zmiennych i innych parametrów. Pozwoli to na identyfikowanie parametrów naszej encji do właściwych kolumn i nadawania tym kolumnom żądanych przez nas właściwości. Tę sprawę załatwimy dodając adnotację /** @Column (type=”string”, length=75) */

Od razu widać, że adnotacje ORM nie powinny sprawiać trudności, właśnie oznaczyliśmy właściwość naszej klasy typem string o długości 75 znaków.

Zaktualizujemy zatem wszystkie kolumny:

/**
* @Entity
* @Table(name=”my_products”)
 */
class Product{
	/** @Column(type=”integer”) */
private $id;

/** @Column(type=”string” length=”75”) */
private $name;

/** @Column(type=”datetime”, name=”created_at” */
private $createdAt;
}

Tyle wystarczy by obiekt klasy Product za pośrednictwem Entity Managera (o którym trochę później) mógł zostać zapisany i pobrany z bazy danych.

Dużo więcej o właściwościach adnotacji.


Zauważyliście Pewnie że adnotacja @Column może przyjmować różnego rodzaju parametry, co więcej parametry te mogą mieć różne wartości:

  • type: opcjonalnie, domyślnie z wartością ‘string’. Ponieważ ten parametr jest kluczowy w poprawnym mapowaniu ORM i może definiować naprawdę dlatego zróbmy dokładny przegląd jego możliwych wartości.
    • string: parametr obiektu jest mapowany do kolumny bazy danych typu VARCHAR
    • integer: parametr obiektu jest mapowany do kolumny bazy danych typu INT i jest typowym wyborem dla wartości typu całkowitego w przedziale -2147483648 to +2147483647
    • smallint: W bazie danych zostanie zmapowany do kolumny SMALLINT z zakresem -32768 to +32767
    • bigint: Również dla wartości typu integer ale bardzo dużego zasięgu i zakres obejmuje -9223372036854775808 to 9223372036854775807.
    • boolean: Parametr mapowany do kolumny typu boolean zawierającej wartości Prawda/Fałsz/Undefined lub odpowiednik TINYINT gdzie można przechować trzy wartości 0/1/null
    • decimal: Parametr mapowany do kolumny typu decimal gdzie możemy zapisać liczby dziesiętne zbudowane z 131072 cyfr oraz z częścią ułamkową po kropce zbudowaną z 16383 cyfr
    • date: Pole mapowane do kolumny typu DATETIME gdzie mapowane są dane o dacie bez informacji o godzinie  oraz bez informacji o strefie czasowej. Jeśli parametr Twojego obiektu będzie zawierał informacje o godzinie lub o strefie czasowej to przy zapisie do bazy informacja ta zostanie pominięta.
    • time: Pole mapowane do kolumny typu DATETIME gdzie mapowane są dane o czasie. Jeśli parametr obiektu będzie zawierał informacje o dacie lub strefie czasowej to przy zapisie do bazy zostaną pominięte.
    • datetime: Pole mapowane do kolumny typu DATETIME/TIMESTAMP. Połączenie dwóch poprzednich typów gdzie w kolumnie zapisane zostaną informacje o Dacie i czasie, natomiast ewentualna informacja strefy czasowej zostanie pominięta.
    • datetimetz: Pole mapowane do kolumny DATETIME/TIMESTAMP. Najszersza forma przechowania informacji gdzie zapisane zostaną dane o dacie, czasie oraz strefy czasowej.
    • text: Pole mapowane do kolumny text bez informacji o maksymalnej długości ciągu znaków.
    • object: Typ który mapuje SQL CLOB na obiekt PHP za pomocą serialize() i unserialize()
    • array: Typ który mapuje SQL CLOB na tablicę PHP za pomocą serialize() i unserialize()
    • simple_array: Typ który mapuje SQL CLOB na tablicę PHP używając implode() oraz explode() z przecinkiem jako ogranicznikiem. Ważne - Używaj tylko jeśli jesteś pewny tego, że wartość nie może przyjąć znaku “,”.
    • json_array: Typ który jest mapowany z SQL CLOB do tablicy PHP przy użyciu json_encode() oraz json decode().
    • float: Mapowanie do kolumny typu Float (podwójna precyzja). Ważne - działa tylko z ustawieniami lokalnymi, które używają separatorów dziesiętnych.
    • guid: Mapowanie kolumny GUID/UUID do PHP varchar. Domyślnie varchar, ale może być specyficzny typ jeśli platforma takowy wspiera.
    • blob: Mapuje SQL BLOB do PHP resource stream.
  • name: (opcjonalnie, domyślnie przyjmuje wartość właściwości klasy której dotyczy) Warto uzupełnić ten parametr jeśli chcemy mapować właściwość klasy do kolumny o innej nazwie.
  • length: (opcjonalnie, domyślnie 255) Jest to długość kolumny w bazie danych (ma zastosowanie tylko przy kolumnach zawierających łańcuchy znaków.
  • unique: (opcjonalnie, domyślnie FALSE) Użyj tego parametru jeśli kolumna powinna być kluczem unikalnym.
  • nullable: (opcjonalnie, domyślna wartość FALSE) Ważna parametr, decyduje o tym czy kolumna w bazie danych może przechowywać wartość typu NULL.
  • precision: (opcjonalnie, domyślna wartość to 0) Określa precyzję dla liczby dziesiętnych czyli definiuje ilość cyfr przechowywanych w kolumnie dla całej wartości. Dla przykłady precision=5 pozwoli nam na przechowanie wartości z zakresu -99999 do 99999.
  • scale: (opcjonalnie, domyślnie 0) Reprezentuje liczbę cyfr po prawej stronie przecinka i nie może być większa od precision. Dla przykładu precision=5, scale=2 pozwoli na przechowanie wartości z zakresu -999.99 do 999.99.
  • columnDefinition: (opcjonalnie) Ten parametr powinien zawierać DDL SQL (Data Definition Language) i powinien określać pełną definicję kolumny. Miej na uwadze to że atrybut “type” nadal będzie obsługiwał konwersję pomiędzy wartościami PHP i bazą danych. Pole to pozwala na wykorzystywanie zaawansowanych funkcjonalności relacyjnych Baz danych (advanced RMDBS features) jednak użycie tego atrybutu ma wpływ na narzędzie SchemaTool, który to nie będzie w stanie poprawnie wykrywać zmian dla tej kolumny. Dlatego używaj tego parametru ostrożnie.
  • options: (opcjonalnie) Tablica dodatkowych ustawień taki jak np.:
    • default: parametr ustawia dla kolumny wartość domyślną np. options={“default”:0}.
    • unsigned: Jeśli jest ustawiona na TRUE, wówczas kolumna może zawierać tylko liczby dodatnie, takie jak x> = 0.
    • fixed: wartość logiczna określająca czy wskazana długość łańcucha znaków powinna być stała (niezmienna).
    • comment: komentarz kolumny w schemacie.
    • collation: parametr ten określa porównanie, ma to wpływ na to jak informacje są sortowane i jak są porównywane, determinuje to również reguły sortowania, wielkości liter i czułości akcenty w bazie. W ramach ciekawostki wspomnę również o tym że w samej dokumentacji jest pewna nieścisłość. W sekcji FAQ można znaleźć wzmiankę ”How do I set the charset and collation for MySQL tables?” i w odpowiedzi “You can't set these values inside the annotations, yml or xml mapping files. To make a database work with the default charset and collation you should configure MySQL”.
    • check: Dodaje typ ograniczenia sprawdzającego do kolumny (może nie być obsługiwany przez wszystkich dostawców).

Co dalej?

Taka porcja jak na jeden dzień powinna być wystarczająca.

Doctrine to bardzo rozbudowane narzędzie którego nie sposób przybliżyć pojedynczym artykułem.

Szykujcie się na więcej  bo tematów jest na cała serię i do zobaczenia przy następnej publikacji!

 

Podobne wpisy
lsb bulb

Masz pomysł? Porozmawiajmy

Masz pomysł? Opowiedz o swoim projekcie.