This post comes from the first version of this blog.
Please send me an email if anything needs an update. Thanks!
Od kiedy programiści stron internetowych [i nie tylko] dostali w swoje ręce pierwsze biblioteki wspomagające technikę ORM - mapowania obiektowo-relacyjnego, stało się jasne, że w bardzo wielu przypadkach oszczędzą nam one samodzielnego pisania zapytań SQL i związanych z tym problemów i błędów. Za bardzo przyjemną abstrakcją tychże bibliotek kryje się jednak jeden problem - ze względu na fakt, iż próbuje ona zastąpić nam całkowicie leżącą niżej warstwę bazy danych, bardzo często nie mamy możliwości skorzystania z natywnych funkcji SQLa. W niniejszym wpisie pokażę, jak sobie z tym poradzić wykorzystując bibliotekę ORM Doctrine.
Fotografia: juokaz, CC-BY-SA.
Doctrine: Dosłowne wyrażenia i funkcje SQL.
Mówiąc o Doctrine nie ucieknę oczywiście od powoływania się na mechanizmy obecne we frameworku symfony, ponieważ jest to główny ekosystem, w jakim pracuję z tą biblioteką. Załóżmy zatem milcząco, że mamy jakieś pojęcie o tym, co się mniej więcej w symfony dzieje, aczkolwiek postaram się jednak za bardzo tej kwestii nie komplikować.Weźmy pod uwagę następujący model opisany fragmentem pliku YAML:
CustomModel:
tableName: customModels
columns:
id:
type: integer(4)
primary: true
notnull: true
autoincrement: true
name:
type: string(255)
description:
type: clob(65535)
createdAt:
type: timestamp
Wychodząc na przeciw pytaniu “co my tu mamy”, załóżmy, że naszym CustomModelem jest produkt, który posiada:
- identyfikator i klucz główny id,
- nazwę name,
- opis description,
- datę i czas dodania do bazy danych.
|
|
Oraz sama [jakże skomplikowana!] klasa modelu:
|
|
A kod, którego potencjalnie użyjemy będzie prezentował się następująco:
|
|
Jak się zapewne domyślacie, problematyczne jest pole typu TIMESTAMP, któremu trzeba podać odpowiednie dane do aktualizacji / wstawienia danego rekordu. Skoro jednak jest funkcja date(), to w czym problem? Nie jest to może jakiś błąd w sztuce programistycznej, aczkolwiek w kwestii daty / czasu o wiele lepiej jest polegać na danych pochodzących bezpośrednio z serwera SQL. Lepiej będzie, jeśli wykorzystamy jego wewnętrzne mechanizmy pobierania tych informacji. Dlatego też zamiast podawać bezpośrednio gotowy string z danymi, lepiej byłoby skorzystać z jednego z wariantów aktualizacji pola czasowego w MySQL:
- funkcji NOW(),
- funkcji CURRENT_TIMESTAMP(),
- funkcji UNIX_TIMESTAMP().
|
|
wstawimy tą:
|
|
? Nasz skrypt wykona wtedy dosyć nieprzyjemne w skutkach zapytanie:
|
|
Co w konfrontacji z polem TIMESTAMP zapisze w rekordzie wartość 0000-00-00 00:00:00. Okazuje się, że wstawienie własnej, literalnej wartości / fragmentu kodu do zapytania SQL nie jest możliwe w ramach zazwyczaj używanych elementów biblioteki Doctrine.
Okazuje się jednak, że można to zrobić w relatywnie prosty sposób - trzeba tylko wiedzieć jak. ;] Mamy tu kolejny klasyczny przykład problemu związanego z “wiedzą”, bowiem za wstawianie literałów do zapytania odpowiada klasa Doctrine_Expression. Aby z niej skorzystać, należy podmienić błędną linijkę na następującą:
|
|
W tym momencie w zapytaniu pojawi się już klasyczna funkcja NOW() nie zamknięta w broniących przed SQL Injection ciapkach, dzięki czemu w wynikowym rekordzie otrzymamy takie dane, jakich oczekiwaliśmy. Oczywiście w ten sposób można umieścić dowolną inną funkcję SQL w zapytaniu do bazy danych, aczkolwiek problem z NOW() wydaje mi się najczęstszym, jaki pojawia się w programowaniu stron internetowych.
Dziękuję za miło spędzony czas i zapraszam do dyskusji na temat innych sposobów na osiągnięcie tego celu. Biblioteka Doctrine jest na tyle wielkim tworem, że na pewno można sobie poradzić z tym problemem wykorzystując inne rozwiązania. Przy okazji - mała niespodzianka - kolejnym wpisem nie będzie Linkdump, ale coś nieco innego - zapraszam do zgadywania. ;]