Przymierzam się do kolejnego projektu w Yii, w którym będę wykorzystywać strukturę drzewiastą nested set i nagle okazało się, że potrzebuję bardziej skomplikowanych zapytań do bazy niż wykorzystywane ostatnio podstawowe CRUDy. Troszkę poszperałam, troszkę poczytałam i ku pamięci zanotuję.
Jak w wielu (a może we wszystkich) frameworkach, w Yii można na różne sposoby pobierać dane z bazy. Ja, choć wcześniej nie byłam przekonana do korzystania z ORM uznałam, że w Yii jest to bardzo przyzwoicie zaimplementowane i większość modeli opieram na klasie CActiveRecord. Jednak implementując strukturę drzewa typu nested set muszę połączyć w jednym zapytaniu tabelę samą ze sobą w dodatku nie używając relacji. Dla przypomnienia, zapytanie wygląda tak:
SELECT concat( repeat('-', COUNT(parent.id) - 1),child.title) AS title, child.id FROM tree AS child, tree AS parent WHERE child.lft BETWEEN parent.lft AND parent.rgt GROUP BY child.id ORDER BY child.lft
Konstruktor zapytań
Pierwsze co przychodzi do głowy to porzucenie metod klasy CActiveRecord, skoro nie da się z jej mechanizmów relacyjnych skorzystać i zbudowanie zapytania za pomocą konstruktora zapytań. Nie będę go tu szczegółowo opisywać, bo jest to przyzwoicie zrobione w dokumentacji. Pokażę tylko jak powyższe zapytanie należy przetłumaczyć na język konstruktora zapytań:
$tree= Yii::app()->db->createCommand() ->select("child.id, CONCAT(REPEAT('-', COUNT(parent.id) - 1),child.name) AS title") ->from('category AS child, category AS parent') ->where('child.lft BETWEEN parent.lft AND parent.rgt') ->group('child.id') ->order('child.lft') ->queryAll();
Proste? No proste i działa.
Wykorzystanie relacji rekordu aktywnego (CActiveRecord)
W przypadku złączeń opartych na relacjach między tabelami (albo relacjach wewnątrz tabeli) można by było wykorzystać do złączeń wspomniane już mechanizmy rekordu aktywnego (CActiveRecord). Nie ma to zastosowania w przypadku, który tu rozpatruję, ale dla uzupełnienia wspomnę. W modelu opartym na klasie CActiveRecord możemy zdefiniować tablice relacji. Wymaga to nadpisania metody relations() i dodania w niej tablicy opisującej potrzebne relacje. Każdy element tablicy opisuję relację tabeli z inną tabelą (do której również zdefiniowano model w oparciu o CActiveRecord). Jest to więc w gruncie rzeczy relacja pomiędzy dwoma klasami AR, która odzwierciedla relację pomiędzy tabelami bazy danych reprezentowanych przez te klasy.
Rekordy opisujące poszczególne relacje mają następującą budowę:
'NazwaRelacji'=>array('TypRelacji', 'NazwaPowiązanejKlasy', 'KluczObcy', ...dodatkowe opcje)
W przypadku drzewa, którego konstrukcja opiera się na kluczu wskazującym rodzica (dodatkowa kolumna o nazwie parentID zawierająca informacje o rekordzie nadrzędnym) taka relacja zdefiniowana w klasie ‘Tree’ mogłaby wyglądać następująco:
'rodzic'=>array(self::HAS_ONE , 'Tree', 'parentID'); 'dziecko'=>array(self::HAS_MANY , 'Tree', 'parentID')
Dodatkowe opcje to zestaw informacji takich jak dodatkowe klauzule WHERE czy LIMIT, aliasy itp. Po zdefiniowaniu relacji można wywołać następujące zapytanie
$tree = Tree::model()->with('rodzic')->findByPk(10);
które zwróci dane rekordu o id=10 i powiązanego z nim rekordu nadrzędnego. Albo zapytanie:
$tree = Tree::model()->with('dziecko')->findByPk(10);
Które zwróci nam dane rekordu o id=10 i wszystkich jego rekordów podrzędnych, jednak tylko pierwszego poziomu. Kolejne poziomy można by próbować odczytywać przez zapytanie:
$tree = Tree::model()->with('dziecko.dziecko')->findByPk(10);
Nie przepadam jednak za taką konstrukcja drzewa jako, że nie jest ona wygodna jeśli chodzi o częste odczytywanie jej jako całości czy nawet w formie wybranych gałęzi. Nie mniej jednak same relacje jako mechanizm pozwalający wykorzystywać rzeczywiste relacje w bazie jest interesujący i bardzo przydatny. Wystarczy bowiem raz dobrze zdefiniować jakąś relację i można jej później wielokrotnie używać w różnych miejscach w skrypcie nie kłopocząc się już tym za bardzo.
Wykorzystanie Kryteriów (CDbCriteria)
Przytoczone na początku zapytanie, które pozwala odtworzyć strukturę drzewa nested set może jednak być wywołane w modelu opartym na CActiveRecord przy użyciu metody findAll() i odpowiednio skonstruowanych kryteriów (obiekt klasy CDbCriteria).
$criteria = new CDbCriteria; $criteria->select = "select = "t.id, concat( repeat('-', COUNT(parent.id) - 1),t.title) AS title"; $criteria->join = ' JOIN `category` AS `parent`'; $criteria->group = 't.id'; $criteria->order = 't.lft'; $criteria->addCondition("t.lft BETWEEN parent.lft AND parent.rgt"); $resultSet = $this->findAll($criteria);
Tak samo proste i przejrzyste jak w przypadku konstruktora zapytań.