Ostatnio natrafiłem na pewien problem podczas prac nad nowym projektem (opartym o Zend Framework). Rozwiązanie problemu sprowadza się do odpowiedzi na pytanie: Co jest ważniejsze: optymalizacja czy zasady programowania obiektowego?

Mamy model Photos oraz klasę PhotosRow reprezentujący pojedynczy rekord. W aplikacji istnieją różne rodzaje zdjęć (profile, albumy, wydarzenia). Każde zdjęcia, w zależności od typu posiada kilka różnych rozmiarów.

class Photos extends Advaf_Db_Table {
    public function deleteAlbumPhoto($photosId, $usersId) {
         if ($this->_hasAccessToPhoto(..., 'albums')) {
             $row = $this->findOne($photosId);
             return $row->delete();
         }
         return null;
    }

    public function deleteEventPhoto($photosId, $usersId) {
         if ($this->_hasAccessToPhoto(..., 'events')) {
             $row = $this->findOne($photosId);
             return $row->delete();
         }
         return null;
    }
}
class PhotosRow extends Advaf_Db_Table_Row {
    public function _postDelete() {
        $mgr = new PhotoMgr();
        foreach ($mgr->getSizes(this->type) as $size) {
            $path = $mgr->getPhotoFullPath(...);
            if(file_exists($path)) {
                unlink($path);
            }
        }
    }
}

Chciałem umieścić w metodzie _postDelete() klasy PhotosRow cały kod odpowiedzialny za usuwanie z przestrzeni dyskowej odpowiednich zdjęć. Mój kolega, Szymon od razu zwrócił uwagę, że to nie jest optymalne. Aby usunąć rekord, najpierw go pobieramy, potem usuwany (SELECT + DELETE, zamiast tylko DELETE). Wykonujemy jedno zapytanie dodatkowo, dla każdego zdjęcia. Dla 20 zdjęć do 20 extra zapytań! Szymon zaproponował aby przenieść cały kod odpowiedzialny za usuwanie zdjęć do Photos, a zdjęcia usuwać bezpośrednio $this->delete($where).

class Photos extends Advaf_Db_Table {
    public function deleteAlbumPhoto($photosId, $usersId) {
         if ($this->_hasAccessToPhoto(..., 'albums')) {
            $where = $this->quoteInto('id = ?', $photosId);
            $this->_unlink(...);
            return $this->delete(where);
         }
         return null;
    }

    private function _unlink(...) {

    }
}

Mimo wszystko obstaje przy swoim. Zgodnie z zasadą odpowiedzialności to klasa PhotosRow powinna po sobie posprzątać. Wydaje mi się, że właśnie w tym celu developerzy Zenda stworzyli takie metody jak _postDelete czy _postInsert w klasie Zend_Db_Table_Row.

Nie oznacza to oczywiście, że twórcy ZF nie moją głupich pomysłów… moim zdaniem do nich m.in należy obsługa formularzy i osobiście preferuje rozwiązanie Szymona: Fasic_Form.

Jestem bardzo Ciekawy twojej opinii, bo sprawa nadal nie jest rozstrzygnięta. Być może znasz lepsze rozwiązanie?

Społeczność: Powiązane posty:

Komentarze

  1. timi

    A gdybyś dodał np. przeładowaną statyczną metodę row.delete(int photoId)?
    Wykonywałaby ona DELETE bez SELECTa i była w klasie row (odpowiedzialność zachowana). Jedyną wadą tego rozwiązanie jaką widzę byłoby nietrzymanie się ogólnej koncepcji pobierz + wykonaj akcje (zamiast tego mamy tylko akcję).

    PS. Dawno nie pisałem w PHP, z góry przepraszam za niezgodność językową;)

  2. Maksymus007

    Ja tam uważam, że kolega ma racje - jeśli zdjęcie ma być usunięte to nie ma sensu próbować go wybrać - leci delete i tyle - jak było to nie ma, jak nie było to nadal nie ma ;]
    A doktryny doktrynami ale w przypadku języków jak PHP można (a nawet trzeba) zrezygnować czasem z pełnej obiektowości na rzecz szybkości działania.

  3. Hubert Marzec

    @timi Chcesz trochę oszukać :) Instacja klasy PhotosRow istnieje tylko wtedy gdy istnieje rekord w bazie (mapowanie). Skorzystanie z metody statycznej PhotosRow::delete() to chyba trochę obejście problemu na siłę….

  4. Hubert Marzec

    @Maksymus007 W tym przypadku nie chodzi o szybkość php tylko dodatkowe zapytanie sql, sam php nic do tego nie ma. Gdyby tylko trzeba było usunąć tylko rekord z bazy to oczywiście masz racje. Ale ponieważ trzeba jeszcze usunąć z dysku 4 różne wersje (rozmiary) tego zdjęcia do co innego…

  5. Szymon

    @timi zgadzam sie z Tobą, wg mnie to jest lepsze rozwiązanie, a pobieranie danych aby je później usunąć - tylko dlatego aby skorzystać z *Row, to jest dopiero obejście problemu na siłę ;)

  6. Szymon

    Hubert odnośnie usuwania 4 różnych rzeczy - to również można zrealizować za pomocą sposobu by timi ;) Jedyną wadę jako widzę to fakt, iż wszystko jest trzymane w modelu (przez co szybko się rozrasta).

  7. Szymon

    I jeszcze jedno “stworzyli takie metody jak _postDelete czy _postInsert” zapewne po to je stworzyli - pytanie tylko czy są z tego dumni i czy sami tego używają ;) Aby była jasność nie jestem zagorzałym przeciwnikiem tego, ale do zwolenników tego rozwiązania też nie należę ;)

  8. piotrf

    Szymon: “Jedyną wadę jako widzę to fakt, iż wszystko jest trzymane w modelu (przez co szybko się rozrasta).” - ale przy wiekszym projekcie takie podejscie moze bardzo pomoc, jak dla mnie taki “photosrow” nie jest jedynie zmapowanym wpisem do bazy, to jest reprezentacja pojdynczej fotki, wiec tak ja trzeba traktowac. Wywolujac na obiekcie fotki metode Delete, usuwana jest fotka, i mnie nie powinno juz interesowac czy sa usuwane zdjecia z dysku czy inne rzeczy - chyba na tym polega idea OOP

  9. piotrf

    btw jesli taki jeden select jest dokuczliwy to moze lepiej pomyslec nad jakims cachem :P

  10. Marcin Kłeczek

    W tym konkretnym przypadku bez problemu można dodać tego selecta - jego czasochłonność jest minimalna (0.000003sec?) - zakładam że to proste pobranie po ID. Bardziej czasochłonne jest usuwanie czegoś, co nie istnieje (nie z bazy, a z plików). Co prawda można pominąć wszelkie sprawdzania błędów… tylko, później trzeba pisać skrypty sprawdzające “spójność” systemu DB/pliki.

    Rozszerzając moją propozycję - zawsze warto rozszerzyć/rozbudować/ułatwić model kosztem prędkości. Co najmniej 2 powody -
    A. Koszt procesora/pamięci/dysku maleje w szybkim tempie, koszt pracy programisty jest droższy
    B. Wszelkie zmiany struktury w przypadku (niespodziewanej?) rozbudowy są dużo łatwiejsze i unikamy sytuacji “nie da się”, lub “jest baaaardzo czasochłonne”.

  11. Koziołek

    Skoro kasowanie zdjęcia składa się z kilku etapów, a chcemy wykonać je w ramach jednego rzutu to istnieją ciekawe rozwiązania:
    1. Trigger + wspólny katalog - usuwamy rekord, a następnie triggery odpowiednio propagują nam tą zmianę. Z dysku usuwamy całe katalogi przy założeniu, że wszystkie pochodne zdjęcia i zdjęcie główne znajdują się w jednym katalogu.
    2. Trigger + kolejka + demon - usunięcie zdjęcia jest dwu etapowe. Najpierw jest oznaczane jako do usunięcia i informacja o nim jest wysyłana do osobnej tabeli. tabelę co pewien czas np. przy niskim obciążeniu serwera czyta demon i wykonuje sprzątanie fizyczne. Sam demon może chodzić jako osobny proces systemu na serwerze ciągle lub być uruchamiany z crona.

  12. pablo picasso

    @koziołek:
    trigger usuwajacy zdjecia? perwersja. deamon? nierealne dla serwerow hostingowych (my world).
    nie bardzo rozumiem o czym jest ta dyskusja?
    $row->delete() powinno usuwac zdjecia + ew. miniaturke.
    nie korzystasz z $row->delete() i usuwasz hurtowo zdjecia z bazy? metoda to obslugujaca powinna tez usuwac te zdjecia. tak, ta metoda (np. deleteById(array $ids) {…} ) powinna znalezc sie w modelu, jezeli mowa o MVC. nie wyobrazam sobie szukac kodu ktory usuwa zdjecia z dysku np. w kontrolerze (w ktorym?)…

  13. Hubert Marzec

    @pablo Tutaj jest mowa o dwóch rzeczach: usuniecie rekordu z bazy i usuniecie zdjęcia z systemu plików. Dalszej Twojej wypowiedzi to ja nie rozumiem. Cały kod odpowiedzialny za usuwanie zdjęcia (baza / system plików) jest modelu, więc nie wiem skąd Twoje sugestie odnośnie szukania kodu w kontrolerze…

Dodaj komentarz