0 follower

Relacyjny Rekord Aktywny

Dotychczas zobaczyliśmy jak używać Rekordu Aktywnego (AR) aby wybierać dane z jednej tabeli bazodanowej. W tej sekcji opiszemy jak używać AR aby złączyć kilka powiązanych tabel baz danych i zwrócić połączony zbiór danych.

W celu używania relacyjnego AR, wymagane jest, aby relacja dla obcego, klucza głównego była dobrze zdefiniowana pomiędzy tabelami, które będą łączone. AR polega na metadanych tych relacji gdy decyduje jak połączyć tabele.

Uwaga: Poczynając od wersji 1.0.1, możesz używać relacyjnego AR nawet jeśli nie zdefiniowałeś kluczy obcych w swojej bazie danych.

Dla uproszczenia, będziemy używali schematu bazy danych pokazanego na następnym diagramie zależności encji (ER) celem zilustrowania przykładów w tej sekcji.

Diagram ER

Diagram ER

Info: Wsparcie dla ograniczeń kluczy obcych różni sie w różnych DBMS.

SQLite nie wspiera ograniczeń kluczy obcych, lecz wciąż możesz deklarować ograniczenia, podczas tworzenia tabel. AR może wykorzystać te deklaracje aby prawidłowo wspierać relacyjne zapytania.

MySQL wspiera ograniczenia kluczy obcych gdy używany jest silnik InnoDB, w przeciwieństwie do silnika MyISAM. Zalecamy zatem używanie InnoDB dla twoich baz danych MySQL. Podczas używania MyISAM, możesz wykorzystać następujący trik aby móc wykonywać relacyjne zapytania przy użyciu AR:

CREATE TABLE Foo
(
  id INTEGER NOT NULL PRIMARY KEY
);
CREATE TABLE bar
(
  id INTEGER NOT NULL PRIMARY KEY,
  fooID INTEGER
     COMMENT 'CONSTRAINT FOREIGN KEY (fooID) REFERENCES Foo(id)'
);

Powyżej użyliśmy słowa kluczowego COMMENT aby opisać ograniczenia klucza obcego co może zostać przeczytanie przez AR aby rozponać opisywaną relację.

1. Deklarowanie relacji

Zanim użyjemy AR aby wykonać relacyjne zapytanie, musimy powiedzieć AR jak jedna klasa AR jest powiązana z drugą.

Relacja pomiędzy dwoma klasami AR jest bezpośrednio związana z relacją pomiędzy tabelami bazy danych reprezentowanych przez klasę AR. Z punkty widzenia bazy danych relacja pomiędzy dwoma tabelami A i B może występować w 3 wariantach: jeden-do-wielu (ang. one-to-many; np. User i Post), jeden-do-jednego (ang. one-to-one; np. User i Profile) oraz wiele-do-wielu (ang. many-to-many; np. Category i Post). W AR występują cztery rodzaje relacji:

  • BELONGS_TO (należy do): jeśli relacja pomiędzy tabelą A i B to jeden-do-jednego, wtedy B należy do A (np. post Post należy do użytkownika User);

  • HAS_MANY (posiada wiele): jeśli relacja pomiędzy tabelą A i B to jeden-do-wielu wtedy A ma wiele B (np. użytkownik User ma wiele postów Post);

  • HAS_ONE (posiada jedną): to jest specjalny przypadek relacji HAS_MANY gdzie A posiada co najwyżej jedno B (np. użytkownik User ma co najwyżej jeden profil Profile);

  • MANY_MANY (wiele do wielu): odpowiada relacji bazodanowej wiele-do-wielu. Aby rozbić relację wiele-do-wielu na jeden-do-wielu potrzebna jest tablica asocjacyjna, gdyż wiele DBMS nie wspiera bezpośrednio relacji wiele-do-wielu. W naszym przykładzie schemat bazy danych PostCategory zostanie użyty w tym celu. W terminologii AR, możemy wytłumaczyć. W terminologii AR, możemy wytłumaczyć relację wiele-do-wielu jako kombinację BELONGS_TO oraz HAS_MANY. Na przykład, post Post należy do wielu kategorii Category a kategoria Category posiada wiele postów Post.

Deklarowanie relacji w AR wymaga nadpisania metody relations() z CActiveRecord. Metoda zwraca tablicę konfiguracji relacji. Każdy element tablicy reprezentuje pojedynczą relację zapisaną w następującym formacie:

'NazwaZmiennej'=>array('TypRelacji', 'NazwaKlasy', 'KluczObcy', ...dodatkowe opcje)

gdzie NazwaZmiennej jest nazwą relacji; TypRelacji specyfikuje typ relacji i posiada jedną z czterech stałych wartości: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY oraz self::MANY_MANY; NazwaKlasy jest nazwą klasy AR powiązanej z tą klasą; oraz KluczObcy określa klucz(e) obcy(e) powiązane z tą relacją. Dodatkowe opcje moga być określone na końcu każdej relacji (więcej szczegółów w dalszej części).

Następujący kod pokazuje jak możemy zadeklarować relację dla klasy użytkownika User oraz postu Post.

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
            'categories'=>array(self::MANY_MANY, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}
 
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Info: Klucz obcy może być kluczem złożonym, zawierającym dwie lub więcej kolumn. W takim przypadku, powinniśmy złączyć nazwy kolumn dla kluczy obcych rozdzielając je spacją lub przecinkiem. Dla typu relacji MANY_MANY, nazwa tablicy asocjacyjnej musi również być określona w kluczach obcych. Na przykład, relacja kategorii categories w Post jest określona przez klucz obcy PostCategory(postID, categoryID).

Deklaracja relacji w klasie AR domyślnie dodaje właściwość do klasy dla każdej relacji. Po tym jak zapytanie relacyjne jest wykonywane, odpowiadająca właściwość będzie wypełniona odpowiadającymi instancjami AR. Na przykład, jeśli $author reprezentuje instancję AR User, możemy użyć $author->posts aby dostać się do powiązanych instancji Post.

2. Wykonywanie zapytań relacyjnych

Najprostszym sposobem wykonania relacyjnego zapytania jest odczytanie relacyjnej właściwości instancji AR. Jeśli właściwość nie była wcześniej odczytywana, relacyjne zapytanie zostanie zainicjalizowane za pomocą którego złączymy dwie połączone z sobą tabele i odfiltrujemy dane przy użyciu klucza głównego aktualnej instancji AR. Wynik zapytania będzie zapisany we właściwości jako instancja(e) powiązanej klasy AR. Jest to znane jako technika opóźnionego ładowania (ang. lazy loading approach), np. zapytanie relacyjne jest wykonywane tylko wtedy, gdy powiązane obiekty są odczytywane są po raz pierwszy. Poniższy przykład pokazuje jak używać tej techniki:

// zwróć post, którego ID wynosi 10
$post=Post::model()->findByPk(10);
// zwróć autora posta: tutaj będzie wykonane zapytanie relacyjne
$author=$post->author;

Info: Jeśli nie istnieją żadne powiązane instancje dla relacji, odpowiednie właściwości mogą być pustą tablicą bądź wartością null. Dla relacji BELONGS_TO oraz HAS_ONE wynikiem jest wartość null; dla HAS_MANY oraz MANY_MANY jest to pusta tablica.

Zauważ, że relacje HAS_MANY oraz MANY_MANY zwracają tablicę obiektów, dlatego będziesz musiał przejść w pętli przez zwrócony wynik, jeśli będziesz chciał uzyskać dostęp do jakiejkolwiek właściwości obiektu. W przeciwnym przypadku, możesz uzyskać błąd: "Próbujesz
skorzystać z właściwości elementu nie będącego obiektem" (ang. "Trying to get property of non-object" errors).

Technika opóźnionego ładowania jest bardzo poręczne w użyciu, ale nie zawsze jest wydajne we wszystkich scenariuszach. Dla przykładu, jeśli chcemy uzyskać dostęp do informacje o autorze dla N postów, używając techniki opóźnionego ładowania wykonamy N zapytań z użyciem join. W tych warunkach powinniśmy uciec się do tak zwanej techniki gorliwego ładowania (ang. eager loading approach).

Technika gorliwego ładowania zwraca powiązane instancje AR razem z główną(ymi) instancją(ami) AR. Osiągamy to poprzez użycie metody with() razem z jedną z metod AR find lub findAll. Na przykład,

$posts=Post::model()->with('author')->findAll();

Powyższy kod zwróci tablicę instancji Post. W odróżnieniu od techniki leniwego ładowania właściwość author w każdej instancji Post jest już wypełniona odpowiednią instancją User przed jej pierwszym odczytaniem. Zamiast wywoływania zapytania z join dla każdego posty, technika gorliwego ładowania zwraca wszystkie posty razem wraz z autorami za pomocą jednego zapytania z join!

Możemy używać jednocześnie wielu nazw relacji w metodzie with() a technika gorliwego łączenia dostarczy nam je z powrotem za jednym razem. Na przykład, następujący kod zwróci posty razem z ich autorami oraz kategoriami:

$posts=Post::model()->with('author','categories')->findAll();

Możemy również korzystać z zagnieżdżonych gorliwych ładowań. Zamiast listy nazw relacji, przekazujemy w postaci hierarchicznej nazwy relacji do metody with() w następujący sposób:

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

Powyższy przykład zwróci wszystkie posty razem z ich autorami oraz kategoriami. Zwróci również dla każdego autora profil oraz posty.

Uwaga: Sposób użycia metody with() został zmieniony wraz z wersją 1.0.2. Proszę przeczytać z uwagą odpowiadającą jej dokumentację API.

Implementacja AR w Yii jest bardzo wydajna. Jeśli gorliwie ładujemy hierarchię powiązanych obiektów zawierających N-relacji HAS_MANY lub MANY_MANY, aby uzyskać pożądany rezultat zostanie wykonanych N+1 zapytań SQL. Oznacza to, że w ostatnim przykładzie potrzebujemy wykonać 3 zapytania SQL, ponieważ występują tam właściwości posts oraz categories. Pozostałe frameworki mają bardziej radykalne podejście używając tylko jedno zapytanie SQL. Na pierwszy rzut oka, to radykalne podejście wygląda bardziej wydajnie, ponieważ parsowanych i wykonywanych przez DBMS jest mniej zapytań. W rzeczywistości jest to niepraktyczne z dwóch powodów. Po pierwsze, występowanie wielu powtarzających kolumn danych w rezultacie zapytania, co zajmuje dodatkowy czas podczas przetwarzania oraz przesyłania. Po drugie, liczba wierszy w zwracanych wzrasta wykładniczo wraz z zwiększającą się ilością tabel, co czyni je po prostu trudnymi w zarządzaniu wraz ze wzrostem ilości relacji.

Od wersji 1.0.2, możesz również zmusić zapytanie relacyjne do wykonania tylko jednego zapytania SQL. Po prostu dołącz wywołanie metody together() po with(). Na przykład,

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->together()->findAll();

Powyższe zapytanie wykona się w jednym zapytaniu SQL. Bez wywołania metody together, wymagane będą trzy zapytania SQL: jedno łączące tabele Post, User oraz Profile, drugie połączy tabele User i Post a trzecie połączy tabele Post, PostCategory oraz Category.

3. Opcje zapytań relacyjnych

Wspominaliśmy, że dodatkowe opcje mogą być dookreślone w deklaracji relacji. Opcje te zapisane jako pary nazw i wartości używane są w celu dostosowania do własnych potrzeb zapytań relacyjnych. Poniżej znajdziemy ich zestawienie.

  • select: lista kolumn, które będą zwrócone dla powiązanej klasy AR. Domyślną wartością jest '*', oznaczająca wszystkie kolumny. Nazwy kolumn powinny zostać rozróżnione za pomocą aliasToken (tokenu aliasu) jeśli pojawiają się w wyrażeniu (np. COUNT(??.name) AS nameCount).

  • condition: klauzula WHERE. Domyślnie pusta. Zauważ, że referencje do kolumn powinny zostać rozróżnione przy użyciu aliasToken (np. ??.id=10).

  • params: parametry, które zostaną przypięte do wygenerowanego wyrażenia SQL. Powinny zostać przekazane jako tablica par nazwa-wartość. Opcja ta jest dostępna od wersji 1.0.3.

  • on: klauzula ON. Warunki określone tutaj będą dołączone do warunków złączenia przy użyciu operatora AND. Zauważ, ze referencje kolumn muszą zostać ujednoznacznione poprzez użycie
    tokenu aliasu (np. ??.id=10). Opcja ta jest dostępna od wersji 1.0.2.

  • order: klauzula ORDER BY. Domyślnie pusta. Zauważ, że referencje do kolumn powinny zostać rozróżnione przy użyciu aliasToken (np. ??.age DESC).

  • with: lista obiektów potomnych, które powinny zostać załadowane wraz z tym obiektem. Należy pamiętać, że niewłaściwe użycie tej opcji może skutkować utworzeniem nigdy niekończącej się pętli.

  • joinType: typ złączenia dla relacji. Domyślna wartość to LEFT OUTER JOIN.

  • aliasToken: symbol zastępczy (ang. placeholder) dla prefiksu kolumny. Zostanie zastąpiony przez odpowiedni alias tabeli w celu rozróżnienia referencji do kolumn. Domyślnie '??.'.

  • alias: alias dla tablicy powiązanej z relacją. Opcja ta jest dostępna od wersji 1.0.1. Domyślnie posiada wartość null, co oznacza iż alias tablicy jest generowany automatycznie. Opcja ta różni się od aliasToken gdyż ta ostatnia jest tylko symbolem zastępczym i będzie zastąpiona przez aktualny alias tabeli.

  • together: jeśli tabele powiązane mają zostać zmuszone do połączenia się razem z główną tabelą. Opcja ta ma znaczenie dla relacji HAS_MANY oraz MANY_MANY. Jeśli opcja ta nie jest ustawiona lub ma wartość false, każda relacja HAS_MANY lub MANY_MANY będzie posiadała własne wyrażenie JOIN w celu zwiększenia wydajności. Opcja ta jest dostępna od wersji 1.0.3.

  • group: klauzula GROUP BY. Domyślnie pusta. Zauważ, że referencje do kolumn muszą zostać rozróżnione przy użyciu aliasToken (np. ??.age).

  • having: klauzula HAVING. Domyślnie pusta. Zauważ, że reference do kolumn muszą zostać rozróżnione przy użyciu aliasToken (np. ??.age). Uwaga, opcja ta jest dostępna od wersji 1.0.1.

  • index: nazwa kolumny, której wartość powinna zostać użyta jako klucz tablicy, która zawiera powiązane obiekty. Bez ustawienia tej opcji, tablica obiektów powiązanych będzie używała indeksu całkowitoliczbowego rozpoczynającego się od liczby zero. Opcja ta może zotać ustawiona tylko dla relacji HAS_MANY oraz MANY_MANY. Opcja ta jest dostępna od wesji 1.0.7.

Dodatkowo, następujące opcje są dostępne dla wybranych relacji podczas opóźnionego ładowania:

  • limit: ogranicza ilość zwracanych wierszy. Opcja ta nie ma zastosowania dla relacji BELONGS_TO.

  • offset: offset dla zwracanych wierszy. Opcja ta nie ma zastosowania dla relacji BELONGS_TO.

    Poniżej zmodyfikujemy deklarację relacji posts w klasie User poprzez wybranie części z powyższych opcji.

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'
                            'order'=>'??.createTime DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

Teraz jeśli odczytamy $author->posts, powinniśmy otrzymać autorów postów posortowanych malejąco wg czasu ich utworzenia. Każda instancja postu będzie również posiadała wczytaną odpowiadającą jej kategorię.

Info: Kiedy nazwa kolumny pojawia się w dwóch lub więcej tabelach, które są złączane, należy je rozróżnić. Robi się to za pomocą dodania prefiksu do nazwy kolumny zawierającego nazwę tabeli kolumny. Na przykład, id staje się Team.id. Jednakże w relacyjnych zapytaniach AR nie mamy takiej możliwości ponieważ wyrażenia SQL są generowane automatycznie przez AR, które dodaje systematycznie do każdej tabeli alias. Dlatego też, w celu uniknięcia konfliktu nazw kolumn używamy symbolów zastępczych do wskazania występowania kolumn, które powinny zostać rozróżnione. AR zastąpi symbole zastępcze odpowiednim aliasem tabeli i prawidłowo rozróżni kolumny. and properly disambiguate the column.

4. Dynamiczne opcje pytań relacyjnych

Wraz z wersją 1.0.2 możemy używać dynamicznych opcji zapytań relacyjnych zarówno w metodzie with() jak i opcji with. Dynamiczne opcje nadpiszą istniejące opcje tak jak zostało to określone w metodzie relations(). Na przykład, dla powyższego modelu User, jeśli chcemy używać techniki gorliwego ładowania aby zwrócić posty należące do autora sortujące jest rosnąco (opcja order w specyfikacji relacji jest malejąca), możemy zrobić to następująco:

User::model()->with(array(
    'posts'=>array('order'=>'??.createTime DESC'),
    'profile',
))->findAll();

Poczynając od wersji 1.0.5, opcje dynamicznych zapytań mogą być również używane dla leniwego ładowania
w celu wykonywania zapytań relacyjnych. Aby to zrobić, powinniśmy wywołać metodę, której nazwa jest identyczna z nazwą relacji oraz przekazać opcje dynamicznego zapytania jako parametr metody. Na przykład, następujący kod zwróci posty użytkownika, których status wynosi 1:

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

5. Zapytania statystyczne (ang. Statistical Query)

Uwaga: Zapytania statystyczne są wspierane od wersji 1.0.4

Poza zapytaniami relacyjnymi opisanymi powyżej, Yii wspiera również tak zwane zapytania statystyczne (lub zapytania agregacyjne). Odnoszą się one do uzyskiwania agregowanych informacji o powiązanych obiektach, takich jak liczba komentarzy dla każdego postu, średni ocena dla każdego produktu, itp. Zapytania statystyczne mogą być wykonywane dla obiektów
wskazywanych w relacji HAS_MANY (np. post ma wiele komentarzy) lub MANY_MANY (np. post należy do wielu kategorii a kategoria ma wiele postów).

Wykonywania zapytań statystycznych jest bardzo podobne do wykonywania opisanych wcześniej zapytań relacyjnych. Najpierw musimy zdefiniować zapytanie statystyczne w metodzie relations() klasy CActiveRecord tak jak robimy to dla zapytań relacyjnych.

class Post extends CActiveRecord
{
  public function relations()
  {
    return array(
      'commentCount'=>array(self::STAT, 'Comment', 'postID'),
      'categoryCount'=>array(self::STAT, 'Category', 'PostCategory(postID, categoryID)'),
    );
  }
}

Powyżej zadeklarowaliśmy dwa zapytania statystyczne: commentCount oblicza ilość komentarzy należących do postu a categoryCount oblicza ilość kategorii do których należy post. Zauważ, że relacja pomiędzy Post a Comment jest relacją HAS_MANY, podczas gdy relacja pomiędzy Post a Category jest relacją MANY_MANY (wraz z łączącą je tabelą PostCategory). Jak możemy zauważyć, deklaracja jest bardzo podobna do tych z relacji opisywanych we wcześniejszych podpunktach. Jedyną różnicą jest to, że typem relacji jest tutaj STAT.

Przy użyciu powyższej deklaracji możemy otrzymać ilość komentarzy dla postu przy użyciu wyrażenia $post->commentCount. Jeśli użyjemy tej właściwości po raz pierwszy,
wyrażenie SQL zostanie wywołane w ukryciu w celu uzyskania pożądanego rezultatu. Jak już wiemy, jest to tak zwane leniwe ładowanie. Możemy również używać gorliwego ładowania jeśli chcemy dowiedzieć uzyskać ilość komentarzy dla wielu postów.

$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

Powyższe wyrażenie wykona trzy zapytania SQL aby zwrócić wszystkie posty razem z policzonymi komentarzami oraz policzonymi kategoriami. Używając podejścia leniwego ładowania skończylibyśmy z 2*N+1 zapytaniami SQL, gdzie N to ilość postów.

Domyślnie zapytanie statystyczne użyje wyrażenia COUNT do obliczeń (i w ten sposób policzymy komentarze oraz kategorie w powyższych przykładach). Możemy je dostosować do własnych potrzeb poprzez określenie dodatkowych opcji podczas deklarowania go w metodzie relations(). Podsumowanie dostępnych opcji znajduje się poniżej.

  • select: wyrażenie statystyczne. Domyślnie COUNT(*), co oznacza liczenie obiektów potomnych.

  • defaultValue: wartość jaka będzie przypisana do tych rekordów, dla których nie zostaną zwrócone rezultaty zapytania statystycznego. Na przykład, jeśli post nie posiada żadnych komentarzy, jego commentCount otrzyma tą wartość. Wartość domyślna dla tej opcji to zero.

  • condition: klauzula WHERE. Domyślnie jest pusta.

  • params: parametry, które mają zostać związane z wygenerowanym zapytaniem SQL. Powinny być one przekazane jako tablica par nazwa-wartość.

  • order: klauzula ORDER BY. Domyślnie jest pusta.

  • group: klauzula GROUP BY. Domyślnie jest pusta.

  • having: klauzula HAVING. Domyślnie jest pusta.

6. Relacyjne zapytania z nazwanymi podzbiorami

Uwaga: Wsparcie dla nazwanych podzbiorów zostało wprowadzone wraz z wersją 1.0.5.

Relacyjne zapytania mogą być wykonywane w połączeniu z nazwanymi zbiorami. Rozróżniamy dwa przypadki. W pierwszym, nazwany podzbiór stosowany jest do głównego modelu. W drugim, nazwany podzbiór stosowany jest do powiązanego modelu.

Następujący kod pokazuje, jak zastosować nazwane podzbiory do głównego modelu.

$posts=Post::model()->published()->recently()->with('comments')->findAll();

Przypomina to bardziej nierelacyjne zapytania. Jedyna różnica polega na tym, że mamy wywołanie metody with() po łańcuchu wywołań nazwanych podzbiorów. Zapytanie zwróci ostatnio opublikowane posty wraz z ich komentarzami.

Następujący kod pokazuje, jak zastosować nazwane podzbiory do modelu powiązanego.

$posts=Post::model()->with('comments:recently:approved')->findAll();

Powyższe zapytanie zwróci wszystkie posty wraz z zatwierdzonymi komentarzami. Zauważ, że comments jest referencją do nazwy relacji, a recently oraz approved referuje do dwóch nazwanych podzbiorów w klasie modelu Comment. Nazwa relacji oraz nazwane podzbiory powinny być rozdzielone dwukropkiem.

Nazwane podzbiory mogą również zostać zdefiniowane w opcji with reguły relacyjnej zadeklarowanej w CActiveRecord::relations(). W następnym przykładzie, jeśli odczytamy $user->posts, zwróci nam wszystkie zatwierdzone komentarze (approved) postu.

class User extends CActiveRecord
{
  public function relations()
  {
    return array(
      'posts'=>array(self::HAS_MANY, 'Post', 'authorID',
        'with'=>'comments:approved'),
    );
  }
}

Uwaga: Nazwane podzbiory zastosowane dla modelów w relacji muszą zostać określone w CActiveRecord::scopes. W rezultacie, nie mogą one zostać sparametryzowane.