Dobra praktyka #4: W akcjach i widokach nie ma miejsca na zapytania SQL

10-27-2009 - autor: Piotr Plenik Zostaw odpowiedź »
W widokach i akcjach nie wkładamy niczego co dotyczy zapytań ORM-owych.

Ciąg dalszy artykułów z serii: Czy wykorzystujesz dobre praktyki?

W widokach i akcjach nie powinniśmy dodawać żadnych SQL-owych składni (ani Propelowe Criteria, ani też Doctrine Query), jak również innych odwołań ORM-wych.

Odnośnie widoku, chyba nie trzeba się się rozwodzić – pisaliśmy o tym przy jednej z wcześniejszych dobrych praktyk.

Ale dlaczego nie powinno się wkładać SQL-i w akcjach, skoro nawet w dokumentacji Jobeet-a znajdziemy sporo takich zapisów?

Wyobraźmy sobie, że w trakcie realizacji sporej aplikacji, okazała się konieczna zmiana w modelu danych (nie powinno, ale w praktyce niestety się zdarza).

I teraz czeka nas przejrzenie całej nasze aplikacji, modelu (i jeżeli już całkowicie “popłyneliśmy” – i w widoku) w poszukiwaniu odwołań SQL-owych. A wystarczyło zadbać, aby model pozostawić w modelu i od razu mniej niepotrzebnej pracy :)

Złe rozwiązanie:

<?php
class defaultActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
     $this->products = Doctrine::getTable(‘Product’)
                      ->createQuery(‘p’)
                      ->where(‘p.is_active = ?’, true)
                      ->limit(10)
                      ->execute();  // <- TAK NIE ROBIMY!
  }
}

Poprawne rozwiązanie:

<?php
// plik: actions.class.php
class defaultActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
     $this->products = ProductTable::getRecent(10, true);
  }
}

i model:

<?php
// plik: ProductTable.class.php
class ProductTable extends Doctrine_Table
{
  public static function getRecent($max = 10, $published = true)
  {
     return Doctrine::getTable(‘Product’)
                     ->createQuery(‘p’)
                     ->where(‘p.is_active = ?’, $published)
                     ->limit($max)
                     ->execute();
  }
}

Zapraszam do komentowania i dobrej praktyki #5.

Publikuj w:
  • Wykop
  • del.icio.us
  • Facebook
  • Twitter
  • Technorati
  • MySpace
  • Google Bookmarks

17 komentarzy

  1. Michał Górny mówi:

    Witam,

    Po pierwsze gratuluję reaktywacji strony. Mam nadzieję, że polska społeczność symfony się rozrośnie.

    A teraz uwagi: 1) Dlaczego metoda statyczna? Dlaczego nie tworzysz instancji klasy tabeli przez Doctrine::getTable(nazwa komponentu) ?

    2) Co w przypadku prostych zapytań, które nie powtarzają się? Czy na pewno warto mnożyć metody w klasie Table? Oczywiście w przypadku, jaki opisałeś (zmiana struktury bazy danych) będzie wtedy więcej roboty (przeglądanie modelu + akcji).

  2. Gribo mówi:

    ja osobiście przy prostych zapytaniach robię to w akcji wg mnie nie ma sensu tworzyć kolejnych metod żeby np. wyciągnąć jeden rekord z bazy po PK.

  3. SZoPer mówi:

    Czy ta metoda getRecent() nie powinna przypadkiem czegoś zwrócić? :> Poza tym, static i $this ? Ja rozumiem, że wszyscy się domyślają, o co chodziło autorowi, ale skoro to mają być dobre praktyki, to piszmy bez błędów :-)

  4. Piotr Plenik mówi:

    dzięki za zwrócenie uwagi – poprawione :) .

  5. Michał Górny mówi:

    Metoda statyczna dalej pozostała? Dlaczego?

  6. Puchacz mówi:

    Nie ma rozroznienia na “proste” i “nie proste” zapytania do bazy. MVC to MVC i nie wolno mieszac modelu z kontrolerem. Bo w przeciwnym wypadku, po co w ogole stosowac ten wzorzec? Przeciez wszystko mozna zrobic w jednym skrypcie (odpowiedniku widoku).

  7. Michał Górny mówi:

    Puchacz:

    [code lang=”PHP”]

    public function executeAction() { $wynik1 = Doctrine_Query::create("Produkt")->where("kategoria = ?", "samochody")->execute();

    $wynik2 = Doctrine::getTable("Produkt")->dajWgKategorii("samochody"); } [/code]

    Wg mnie oba te przypadki nie naruszają zasad MVC, bo w kontrolerze mam zapytanie do modelu o dane. Tylko zapisane w inny sposób. Nie widzę tu mieszania modelu z kontrolerem. Możesz wyjaśnić swój punkt widzenia?

  8. Michał Górny mówi:

    Spróbuję jeszcze raz (nie zadziałało formatowanie i wkradł się błąd):

    [code lang=”PHP”]

    public function executeAction() { $wynik1 = Doctrine_Query::create()->from("Produkt")->where("kategoria = ?", "samochody")->execute();

    $wynik2 = Doctrine::getTable("Produkt")->dajWgKategorii("samochody"); } [/code]

  9. Puchacz mówi:

    @Michał Górny Oczywiście wszystko jest kwestią umowną, również wzorce projektowe. Jednak częściej spotykaną i ogólnie uznaną za dobrą praktykę jest opakowywanie zapytań, nawet jeśli realizuje się je w sposób obiektowy jak w Doctrine czy Hibernate. Tu chodzi wyłącznie o czytelność kodu i łatwość konserwacji. Jeśli lubisz napisać coś raz, a porządnie to warto się stosować do “best practice”.

  10. Michał Górny mówi:

    Puchacz:

    Rozumiem, tylko mam obawy, że potem w klasie *Table znajdzie się mnóstwo prostych metod wykorzystanych tylko w pojedynczych miejscach.

  11. cysiaczek mówi:

    Symfony cierpi na brak 4 warstwy – biznesowej (i nie, to nie tylko transakcje w bazie). Propel i Doctrine ją tylko próbuja symulować, ale tak naprawde nie nadają się do tego celu, zwłaszcza, że ucierpiałaby wymienność ORM. Nie mogę jednak przy każdym projekcie tworzyć takiego modelu, bo raz, że terminy gonią, a dwa – małe projekty i tak na tym nic nie zyskają

    Sam czasami wrzucam gołą SQLkę do akcji i do tego generuję np. jsona z wyników – tak – zła praktyka, ale jednocześnie skutaczna, ze względu na to, co mówi Michał Górny w ostatnim komentarzu.

    Pozdrawiam

  12. required mówi:

    @cysiaczek “Symfony cierpi na brak 4 warstwy – biznesowej (i nie, to nie tylko transakcje w bazie).” Czy mógłbyś to rozwinąć ? W jaki sposób framework MVC mógłby wspomóc programistę w tym zadaniu ?

  13. cysiaczek mówi:

    MVC w żaden sposób. Użyłem skrótu myślowego. Chodzi o to, że zdecydowana większość developerów używa modeli generowanych przez Doctrine lub Propel i pakuje do nich wszystko. Przykłady takie jak newsy, arty, czy jakieś proste komentarze nie wymagają wiele, ale weźmy np. taki sklep – Cart, Product… i co? To wystarczy? IMO nie bardzo, bo koszyk zakupowy posiada ciekawe funkcje jak podanie wartości zakupów z uwzględnieniem rabatów, kosztów wysyłki itp. Nie można tego (biorąc projekt serio) wpakować do obiektu odpowiedzialnego za odwzorowanie wiersza tabeli. Potrzebujemy coś wyżej – ShoppingCart, który operuje na obiekcie koszyka, produktach w nim zawartych, obiektach promocji itd. – musi operować na wyższym poziomie abstrakcji. Owo “symfony cierpi” to raczej wniosek z obserwacji developerów, którzy wiążą się z jednym ORMem (sam tak robię :P ) i potem przy większych projektach płaczą (ja też :P ), że “coś jest mało elastyczne”. Problemem środowiska SF jest promowanie takich rozwiązań w ramach RAD, którego filozofia nadaje się do małych, kilkudniowych projektów (maksymalnie średnich). Tworzyłem aplikacje, które zaczynały jako małe projekty, a potem rosły, rosły i rosły :) Zaryzykuję więc twierdzenie, że do zaleceń developerów SF i ich wizji tworzenia oprogramowania należy podchodzić krytycznie i jednak czasem zaprojektować sobie 4 warstwę, co jednak wymaga kodowania na piechotę.

    Pozdrawiam

  14. Michał Górny mówi:

    cysiaczek:

    Czyli jeżeli dobrze rozumiem, klasa Cart nie powinna zawierać żadnej logiki biznesowej? Byłaby odpowiedzialna tylko za ORM?

    Natomiast logika biznesowa jest zawarta w klasie ShoppingCart która operuje na klasie Cart? Czy dziedziczy z klasy Cart?

  15. required mówi:

    Najprostszym przykładem 4 warstwy byłaby klasa obsługująca rejestrację użytkownika. Klasa jest w tym momencie odpowiedzialna za dodanie użytkownika do bazy danych, wygenerowanie kodu aktywacyjnego i wysłania via email. Teoretycznie można to wykonać w kontrolerze, ale jeżeli chcemy mieć ładne obiektowe API to przenosimy do jakiejś oddzielnej klasy. Obiekt takiej klasy karmimy instancją mailera i jakimś DAO do obsłgi użytkowników. W więkoszości wypadków (mniejsze projekty) taka architektura będzie overkillem, ale w większych pozwala rozbijać problemy na małe kawałki, dzięki czemu łatwiej zarządzać takim kodem.

  16. Michał Górny mówi:

    required:

    Ja bym w akcji zrobił coś takiego (zakładając użycie doctrina):

    [...] $user = $form->save(); new RegisterMail($user)->send(); [...]

    Generowanie kodu aktywacyjnego umieściłbym w klasie User (metoda preInsert).

    Rozumiem, że Ty byś umieścił generowanie kodu w innej klasie, dlaczego?

    Z tego co rozumiem, uważacie że w klasach doctrina nie powinno być logiki biznesowej? Powinny one tylko być tylko odpowiedzialne za dostęp do danych? Dobrze rozumiem? Jeśli tak, to dlaczego?

  17. required mówi:

    @Michał Górny Twoje rozwiązanie jest oczywiście prawidłowe, nawet bardziej pożądane w większości wypadków ze względu na prostotę. Z drugiej strony zakładasz, że kod aktywacyjny zależy wyłącznie od obiektu konta użytkownika – a nie od innych parametrów, np jakiś salt ustawiony na poziomie aplikacji. W tym momencie nie jesteś w stanie wygenerować kodu w samym modelu – potrzebujesz jakiegoś kontekstu.

Dodaj komentarz