Active Record ============= [Active Record](http://en.wikipedia.org/wiki/Active_record_pattern) zapewnia zorientowany obiektowo interfejs dostępu i manipulacji danymi zapisanymi w bazie danych. Klasa typu Active Record jest powiązana z tabelą bazodanową, a instacja tej klasy odpowiada pojedynczemu wierszowi w tabeli - *atrybut* obiektu Active Record reprezentuje wartość konkretnej kolumny w tym wierszu. Zamiast pisać bezpośrednie kwerendy bazy danych, można skorzystać z atrybutów i metod klasy Active Record. Dla przykładu, załóżmy, że `Customer` jest klasą Active Record, powiązaną z tabelą `customer` i `name` jest kolumną w tabeli `customer`. Aby dodać nowy wiersz do tabeli `customer`, wystarczy wykonać następujący kod: ```php $customer = new Customer(); $customer->name = 'Qiang'; $customer->save(); ``` Kod z przykładu jest odpowiednikiem poniższej komendy SQL dla MySQL, która jest mniej intuicyjna, bardziej podatna na błędy i, co bardzo prawdopodobne, niekompatybilna z innymi rodzajami baz danych: ```php $db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [ ':name' => 'Qiang', ])->execute(); ``` Yii zapewnia wsparcie Active Record dla następujących typów relacyjnych baz danych: * MySQL 4.1 lub nowszy: poprzez [[yii\db\ActiveRecord]] * PostgreSQL 7.3 lub nowszy: poprzez [[yii\db\ActiveRecord]] * SQLite 2 i 3: poprzez [[yii\db\ActiveRecord]] * Microsoft SQL Server 2008 lub nowszy: poprzez [[yii\db\ActiveRecord]] * Oracle: poprzez [[yii\db\ActiveRecord]] * CUBRID 9.3 lub nowszy: poprzez [[yii\db\ActiveRecord]] (zwróć uwagę, że z powodu [błędu](http://jira.cubrid.org/browse/APIS-658) w rozszerzeniu PDO cubrid, umieszczanie wartości w cudzysłowie nie będzie działać, zatem wymagane jest zainstalowanie CUBRID 9.3 zarówno jako klienta jak i serwer) * Sphinx: poprzez [[yii\sphinx\ActiveRecord]], wymaga rozszerzenia `yii2-sphinx` * ElasticSearch: poprzez [[yii\elasticsearch\ActiveRecord]], wymaga rozszerzenia `yii2-elasticsearch` Dodatkowo Yii wspiera również Active Record dla następujących baz danych typu NoSQL: * Redis 2.6.12 lub nowszy: poprzez [[yii\redis\ActiveRecord]], wymaga rozszerzenia `yii2-redis` * MongoDB 1.3.0 lub nowszy: poprzez [[yii\mongodb\ActiveRecord]], wymaga rozszerzenia `yii2-mongodb` W tej sekcji przewodnika opiszemy sposób użycia Active Record dla baz relacyjnych, jednakże większość zagadnień można zastosować również dla NoSQL. ## Deklarowanie klas Active Record Na początek zadeklaruj klasę typu Active Record rozszerzając [[yii\db\ActiveRecord]]. Ponieważ każda klasa Active Record jest powiązana z tabelą bazy danych, należy nadpisać metodę [[yii\db\ActiveRecord::tableName()|tableName()]], aby wskazać odpowiednią tabelę. W poniższym przykładzie deklarujemy klasę Active Record nazwaną `Customer` dla tabeli `customer` w bazie danych. ```php namespace app\models; use yii\db\ActiveRecord; class Customer extends ActiveRecord { const STATUS_INACTIVE = 0; const STATUS_ACTIVE = 1; /** * @return string nazwa tabeli powiązanej z klasą ActiveRecord. */ public static function tableName() { return 'customer'; } } ``` Instancje Active Record są traktowane jak [modele](structure-models.md). Z tego powodu zwykle dodajemy klasy Active Record do przestrzeni nazw `app\models` (lub innej, przeznaczonej dla klas modeli). Dzięki temu, że [[yii\db\ActiveRecord]] rozszerza [[yii\base\Model]], dziedziczy *wszystkie* funkcjonalności [modelu](structure-models.md), takie jak atrybuty, zasady walidacji, serializację danych itd. ## Łączenie się z bazą danych Domyślnie Active Record używa [komponentu aplikacji](structure-application-components.md) `db` jako [[yii\db\Connection|połączenia z bazą danych]], do uzyskania dostępu i manipulowania jej danymi. Jak zostało to już wyjaśnione w sekcji [Obiekty dostępu do danych (DAO)](db-dao.md), komponent `db` można skonfigurować w pliku konfiguracyjnym aplikacji jak poniżej: ```php return [ 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=testdb', 'username' => 'demo', 'password' => 'demo', ], ], ]; ``` Jeśli chcesz użyć innego połączenia do bazy danych niż za pomocą komponentu `db`, musisz nadpisać metodę [[yii\db\ActiveRecord::getDb()|getDb()]]: ```php class Customer extends ActiveRecord { // ... public static function getDb() { // użyj komponentu aplikacji "db2" return \Yii::$app->db2; } } ``` ## Kwerendy Po zadeklarowaniu klasy Active Record, możesz użyć jej do pobrania danych z powiązanej tabeli bazy danych. Proces ten zwykle sprowadza się do następujących trzech kroków: 1. Stworzenie nowego obiektu kwerendy za pomocą metody [[yii\db\ActiveRecord::find()]]; 2. Zbudowanie obiektu kwerendy za pomocą [metod konstruktora kwerend](db-query-builder.md#building-queries); 3. Wywołanie [metod kwerendy](db-query-builder.md#query-methods) w celu uzyskania danych jako instancji klasy Active Record. Jak widać, procedura jest bardzo podobna do tej używanej przy [konstruktorze kwerend](db-query-builder.md). Jedyna różnica jest taka, że zamiast użycia operatora `new` do stworzenia obiektu kwerendy, wywołujemy metodę [[yii\db\ActiveRecord::find()]], która zwraca nowy obiekt kwerendy klasy [[yii\db\ActiveQuery]]. Poniżej znajdziesz kilka przykładów pokazujących jak używać Active Query do pobierania danych: ```php // zwraca pojedynczego klienta o ID 123 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::find() ->where(['id' => 123]) ->one(); // zwraca wszystkich aktywnych klientów posortowanych po ID // SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id` $customers = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->orderBy('id') ->all(); // zwraca liczbę aktywnych klientów // SELECT COUNT(*) FROM `customer` WHERE `status` = 1 $count = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->count(); // zwraca wszystkich klientów w tablicy zaindeksowanej wg ID // SELECT * FROM `customer` $customers = Customer::find() ->indexBy('id') ->all(); ``` W powyższych przykładach `$customer` jest obiektem typu `Customer`, a `$customers` jest tablicą obiektów typu `Customer`. W obu przypadkach dane pobrane są z tabeli `customer`. > Info: Dzięki temu, że [[yii\db\ActiveQuery]] rozszerza klasę [[yii\db\Query]], możesz użyć *wszystkich* metod dotyczących kwerend i ich budowania > opisanych w sekcji [Konstruktor kwerend](db-query-builder.md). Ponieważ zwykle kwerendy korzystają z zapytań zawierających klucz główny lub też zestaw wartości dla kilku kolumn, Yii udostępnia dwie skrótowe metody, pozwalające na szybsze ich użycie: - [[yii\db\ActiveRecord::findOne()]]: zwraca pojedynczą instancję klasy Active Record, zawierającą dane z pierwszego pobranego odpowiadającego zapytaniu wiersza danych. - [[yii\db\ActiveRecord::findAll()]]: zwraca tablicę instancji klasy Active Record zawierających *wszystkie* wyniki zapytania. Obie metody mogą przyjmować jeden z następujących formatów parametrów: - wartość skalarna: wartość jest traktowana jako wartość klucza głównego, który należy odszukać. Yii automatycznie ustali, która kolumna jest kluczem głównym, odczytując informacje ze schematu bazy. - tablica wartości skalarnych: tablica jest traktowana jako lista poszukiwanych wartości klucza głównego. - tablica asocjacyjna: klucze tablicy są poszukiwanymi nazwami kolumn a wartości tablicy są odpowiadającymi im wartościami kolumn. Po więcej szczegółów zajrzyj do rozdziału [Format asocjacyjny](db-query-builder.md#hash-format). Poniższy kod pokazuje, jak mogą być użyte opisane metody: ```php // zwraca pojedynczego klienta o ID 123 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // zwraca klientów o ID 100, 101, 123 i 124 // SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124) $customers = Customer::findAll([100, 101, 123, 124]); // zwraca aktywnego klienta o ID 123 // SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1 $customer = Customer::findOne([ 'id' => 123, 'status' => Customer::STATUS_ACTIVE, ]); // zwraca wszystkich nieaktywnych klientów // SELECT * FROM `customer` WHERE `status` = 0 $customers = Customer::findAll([ 'status' => Customer::STATUS_INACTIVE, ]); ``` > Note: Ani metoda [[yii\db\ActiveRecord::findOne()]] ani [[yii\db\ActiveQuery::one()]] nie dodaje `LIMIT 1` do wygenerowanej > kwerendy SQL. Jeśli zapytanie może zwrócić więcej niż jeden wiersz danych, należy wywołać bezpośrednio `limit(1)`, w celu zwiększenia > wydajności aplikacji, np. `Customer::find()->limit(1)->one()`. Oprócz korzystania z metod konstruktora kwerend możesz również użyć surowych zapytań SQL w celu pobrania danych do obiektu Active Record za pomocą metody [[yii\db\ActiveRecord::findBySql()]]: ```php // zwraca wszystkich nieaktywnych klientów $sql = 'SELECT * FROM customer WHERE status=:status'; $customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all(); ``` Nie wywołuj dodatkowych metod konstruktora kwerend po wywołaniu [[yii\db\ActiveRecord::findBySql()|findBySql()]], ponieważ zostaną one pominięte. ## Dostęp do danych Jak wspomniano wyżej, dane pobrane z bazy danych są dostępne w obiekcie Active Record i każdy wiersz wyniku zapytania odpowiada pojedynczej instancji Active Record. Możesz odczytać wartości kolumn odwołując się do atrybutów obiektu Active Record, dla przykładu: ```php // "id" i "email" są nazwami kolumn w tabeli "customer" $customer = Customer::findOne(123); $id = $customer->id; $email = $customer->email; ``` > Note: nazwy atrybutów Active Record odpowiadają nazwom powiązanych z nimi kolumn z uwzględnieniem wielkości liter. > Yii automatycznie definiuje atrybut Active Record dla każdej kolumny powiązanej tabeli. > NIE należy definiować ich własnoręcznie. Ponieważ atrybuty Active Record nazywane są zgodnie z nazwami kolumn, możesz natknąć się na kod PHP typu `$customer->first_name`, gdzie podkreślniki używane są do oddzielenia poszczególnych słów w nazwach atrybutów, w przypadku, gdy kolumny tabeli nazywane są właśnie w ten sposób. Jeśli masz wątpliwości dotyczące spojności takiego stylu programowania, powinieneś zmienić odpowiednio nazwy kolumn tabeli (używając np. formatowania typu "camelCase"). ### Transformacja danych Często zdarza się, że dane wprowadzane i/lub wyświetlane zapisane są w formacie różniącym się od tego używanego w bazie danych. Dla przykładu, w bazie danych przechowywane są daty urodzin klientów jako uniksowe znaczniki czasu, podczas gdy w większości przypadków pożądana forma zapisu daty to `'RRRR/MM/DD'`. Aby osiągnąć ten format, można zdefiniować metody *transformujące dane* w klasie `Customer`: ```php class Customer extends ActiveRecord { // ... public function getBirthdayText() { return date('Y/m/d', $this->birthday); } public function setBirthdayText($value) { $this->birthday = strtotime($value); } } ``` Od tego momentu, w kodzie PHP, zamiast odwołać się do `$customer->birthday`, można użyć `$customer->birthdayText`, co pozwala na wprowadzenie i wyświetlenie daty urodzin klienta w formacie `'RRRR/MM/DD'`. > Tip: Powyższy przykład pokazuje podstawowy sposób transformacji danych. Podczas zwyczajowej pracy z formularzami danych można skorzystać z > [DateValidator](tutorial-core-validators.md#date) i [[yii\jui\DatePicker|DatePicker]], co jest prostsze w użyciu i daje więcej możliwości. ### Pobieranie danych jako tablice Pobieranie danych jako obiekty Active Record jest wygodne i elastyczne, ale nie zawsze pożądane, zwłaszcza kiedy konieczne jest uzyskanie ogromnej liczby danych, z powodu użycia sporej ilości pamięci. W takim przypadku można pobrać dane jako tablicę PHP, wywołując metodę [[yii\db\ActiveQuery::asArray()|asArray()]] przed wykonaniem kwerendy: ```php // zwraca wszystkich klientów // każdy klient jest zwracany w postaci tablicy asocjacyjnej $customers = Customer::find() ->asArray() ->all(); ``` > Note: Powyższy sposób zwiększa wydajność aplikacji i pozwala na zmniejszenie zużycia pamięci, ale ponieważ jest on znacznie bliższy niskiej warstwie > abstrakcji DB, traci się większość funkcjonalności Active Record. Bardzo ważną różnicą jest zwracany typ danych dla wartości kolumn. Kiedy dane zwracane > są jako obiekt Active Record, wartości kolumn są automatycznie odpowiednio rzutowane zgodnie z typem kolumny; przy danych zwracanych jako tablice > wartości kolumn są zawsze typu string (jako rezultat zapytania PDO bez żadnego przetworzenia), niezależnie od typu kolumny. ### Pobieranie danych seriami W sekcji [Konstruktor kwerend](db-query-builder.md) wyjaśniliśmy, że można użyć *kwerendy serii*, aby zmniejszyć zużycie pamięci przy pobieraniu dużej ilości danych z bazy. Tej samej techniki można użyć w przypadku Active Record. Dla przykładu: ```php // pobiera dziesięciu klientów na raz foreach (Customer::find()->batch(10) as $customers) { // $customers jest tablicą dziesięciu lub mniej obiektów Customer } // pobiera dziesięciu klientów na raz i iteruje po nich pojedynczo foreach (Customer::find()->each(10) as $customer) { // $customer jest obiektem Customer } // kwerenda seryjna z gorliwym ładowaniem foreach (Customer::find()->with('orders')->each() as $customer) { // $customer jest obiektem Customer } ``` ## Zapisywanie danych Używając Active Record możesz w łatwy sposób zapisać dane w bazie, w następujących krokach: 1. Przygotowanie instancji Active Record 2. Przypisanie nowych wartości do atrybutów Active Record 3. Wywołanie metody [[yii\db\ActiveRecord::save()]] w celu zapisania danych w bazie. Przykład: ```php // dodaj nowy wiersz danych $customer = new Customer(); $customer->name = 'James'; $customer->email = 'james@example.com'; $customer->save(); // zaktualizuj istniejący wiersz danych $customer = Customer::findOne(123); $customer->email = 'james@newexample.com'; $customer->save(); ``` Metoda [[yii\db\ActiveRecord::save()|save()]] może zarówno dodawać jak i aktualizować wiersz danych, w zależności od stanu instacji Active Record. Jeśli instancja została dopiero utworzona poprzez operator `new`, wywołanie [[yii\db\ActiveRecord::save()|save()]] spowoduje dodanie nowego wiersza. Jeśli instacja jest wynikiem użycia kwerendy, wywołanie [[yii\db\ActiveRecord::save()|save()]] zaktualizuje wiersz danych powiązanych z instancją. Można odróżnić dwa stany instancji Active Record sprawdzając wartość jej właściwości [[yii\db\ActiveRecord::isNewRecord|isNewRecord]]. Jest ona także używana przez [[yii\db\ActiveRecord::save()|save()]] w poniższy sposób: ```php public function save($runValidation = true, $attributeNames = null) { if ($this->getIsNewRecord()) { return $this->insert($runValidation, $attributeNames); } else { return $this->update($runValidation, $attributeNames) !== false; } } ``` > Tip: Możesz również wywołać [[yii\db\ActiveRecord::insert()|insert()]] lub [[yii\db\ActiveRecord::update()|update()]] bezpośrednio, aby, odpowiednio, > dodać lub uaktualnić wiersz. ### Walidacja danych Dzięki temu, że [[yii\db\ActiveRecord]] rozszerza klasę [[yii\base\Model]], korzysta z tych samych mechanizmów [walidacji danych](input-validation.md). Możesz definiować zasady walidacji nadpisując metodę [[yii\db\ActiveRecord::rules()|rules()]] i uruchamiać procedurę walidacji wywołując metodę [[yii\db\ActiveRecord::validate()|validate()]]. Wywołanie [[yii\db\ActiveRecord::save()|save()]] automatycznie wywołuje również metodę [[yii\db\ActiveRecord::validate()|validate()]]. Dopiero po pomyślnym przejściu walidacji rozpocznie się proces zapisywania danych; w przeciwnym wypadku zostanie zwrócona flaga false - komunikaty z błędami walidacji można odczytać sprawdzając właściwość [[yii\db\ActiveRecord::errors|errors]]. > Tip: Jeśli masz pewność, że dane nie potrzebują przechodzić procesu walidacji (np. pochodzą z zaufanych źródeł), możesz wywołać `save(false)`, > aby go pominąć. ### Masowe przypisywanie Tak jak w zwyczajnych [modelach](structure-models.md), instancje Active Record posiadają również mechanizm [masowego przypisywania](structure-models.md#massive-assignment). Funkcjonalność ta umożliwia przypisanie wartości wielu atrybutom Active Record za pomocą pojedynczej instrukcji PHP, jak pokazano to poniżej. Należy jednak pamiętać, że w ten sposób mogą być przypisane tylko [bezpieczne atrybuty](structure-models.md#safe-attributes). ```php $values = [ 'name' => 'James', 'email' => 'james@example.com', ]; $customer = new Customer(); $customer->attributes = $values; $customer->save(); ``` ### Aktualizowanie liczników Jednym z częstych zadań jest zmniejszanie lub zwiększanie wartości kolumny w tabeli bazy danych. Takie kolumny nazywamy licznikami. Metoda [[yii\db\ActiveRecord::updateCounters()|updateCounters()]] służy do aktualizacji jednego lub wielu liczników. Przykład: ```php $post = Post::findOne(100); // UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100 $post->updateCounters(['view_count' => 1]); ``` > Note: Jeśli używasz [[yii\db\ActiveRecord::save()]] do aktualizacji licznika, możesz otrzymać nieprawidłowe rezultaty, ponieważ jest możliwe, że > ten sam licznik zostanie odczytany i zapisany jednocześnie przez wiele zapytań. ### Brudne atrybuty Kiedy wywołujesz [[yii\db\ActiveRecord::save()|save()]], aby zapisać instancję Active Record, tylko *brudne atrybuty* są zapisywane. Atrybut uznawany jest za *brudny* jeśli jego wartość została zmodyfikowana od momentu pobrania z bazy danych lub ostatniego zapisu. Pamiętaj, że walidacja danych zostanie przeprowadzona niezależnie od tego, czy instancja Active Record zawiera brudne atrybuty czy też nie. Active Record automatycznie tworzy listę brudnych atrybutów, poprzez porównanie starej wartości atrybutu do aktualnej. Możesz wywołać metodę [[yii\db\ActiveRecord::getDirtyAttributes()]], aby otrzymać najnowszą listę brudnych atrybutów. Dodatkowo można wywołać [[yii\db\ActiveRecord::markAttributeDirty()]], aby oznaczyć konkretny atrybut jako brudny. Jeśli chcesz sprawdzić wartość atrybutu sprzed ostatniej zmiany, możesz wywołać [[yii\db\ActiveRecord::getOldAttributes()|getOldAttributes()]] lub [[yii\db\ActiveRecord::getOldAttribute()|getOldAttribute()]]. > Note: Porównanie starej i nowej wartości atrybutu odbywa się za pomocą operatora `===`, zatem atrybut zostanie uznany za brudny nawet jeśli > ma tą samą wartość, ale jest innego typu. Taka sytuacja zdarza się często, kiedy model jest aktualizowany danymi pochodzącymi z formularza > HTML, gdzie każda wartość jest reprezentowana jako string. > Aby upewnić się, że wartości będą odpowiednich typów, np. integer, możesz zaaplikować [filtr walidacji](input-validation.md#data-filtering): > `['attributeName', 'filter', 'filter' => 'intval']`. ### Domyślne wartości atrybutów Niektóre z kolumn tabeli bazy danych mogą mieć przypisane domyślne wartości w bazie danych. W przypadku, gdy chcesz wypełnić takimi wartościami formularz dla instancji Active Record, zamiast ponownie ustawiać wszystkie domyślne wartości, możesz wywołać metodę [[yii\db\ActiveRecord::loadDefaultValues()|loadDefaultValues()]], która przypisze wszystkie domyślne wartości odpowiednim atrybutom: ```php $customer = new Customer(); $customer->loadDefaultValues(); // $customer->xyz otrzyma domyślną wartość, zadeklarowaną przy definiowaniu kolumny "xyz" ``` ### Aktualizowanie wielu wierszy jednocześnie Metody przedstawione powyżej działają na pojedynczych instancjach Active Record, dodając lub aktualizując indywidualne wiersze tabeli. Aby uaktualnić wiele wierszy jednocześnie, należy wywołać statyczną metodę [[yii\db\ActiveRecord::updateAll()|updateAll()]]. ```php // UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%` Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']); ``` W podobny sposób można wywołać [[yii\db\ActiveRecord::updateAllCounters()|updateAllCounters()]], aby uaktualnić liczniki wielu wierszy w tym samym czasie. ```php // UPDATE `customer` SET `age` = `age` + 1 Customer::updateAllCounters(['age' => 1]); ``` ## Usuwanie danych Aby usunąć pojedynczy wiersz danych, utwórz najpierw instancję Active Record odpowiadającą temu wierszowi, a następnie wywołaj metodę [[yii\db\ActiveRecord::delete()]]. ```php $customer = Customer::findOne(123); $customer->delete(); ``` Możesz również wywołać [[yii\db\ActiveRecord::deleteAll()]], aby usunąć kilka lub wszystkie wiersze danych. Dla przykładu: ```php Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]); ``` > Note: Należy być bardzo ostrożnym przy wywoływaniu [[yii\db\ActiveRecord::deleteAll()|deleteAll()]], ponieważ w efekcie można całkowicie usunąć > wszystkie dane z tabeli bazy, jeśli popełni się błąd przy ustalaniu warunków dla metody. ## Cykl życia Active Record Istotnym elementem pracy z Yii jest zrozumienie cyklu życia Active Record w zależności od metodyki jego użycia. Podczas każdego cyklu wykonywane są określone sekwencje metod i aby dopasować go do własnych potrzeb, wystarczy je nadpisać. Można również śledzić i odpowiadać na eventy Active Record uruchamiane podczas cyklu życia, aby wstrzyknąć swój własny kod. Takie eventy są szczególnie użyteczne podczas tworzenia wpływających na cykl życia [behaviorów](concept-behaviors.md) Active Record. Poniżej znajdziesz wyszczególnione cykle życia Active Record wraz z metodami/eventami, które są w nie zaangażowane. ### Cykl życia nowej instancji Podczas tworzenia nowej instancji Active Record za pomocą operatora `new`, zachodzi następujący cykl: 1. Konstruktor klasy. 2. [[yii\db\ActiveRecord::init()|init()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]]. ### Cykl życia przy pobieraniu danych Podczas pobierania danych za pomocą jednej z [metod kwerendy](#querying-data), każdy świeżo wypełniony obiekt Active Record przechodzi następujący cykl: 1. Konstruktor klasy. 2. [[yii\db\ActiveRecord::init()|init()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]]. 3. [[yii\db\ActiveRecord::afterFind()|afterFind()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_AFTER_FIND|EVENT_AFTER_FIND]]. ### Cykl życia przy zapisywaniu danych Podczas wywołania [[yii\db\ActiveRecord::save()|save()]], w celu dodania lub uaktualnienia danych instancji Active Record, zachodzi następujący cykl: 1. [[yii\db\ActiveRecord::beforeValidate()|beforeValidate()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]]. Jeśli metoda zwróci false lub właściwość [[yii\base\ModelEvent::isValid]] ma wartość false, kolejne kroki są pomijane. 2. Proces walidacji danych. Jeśli proces zakończy się niepowodzeniem, kolejne kroki po kroku 3. są pomijane. 3. [[yii\db\ActiveRecord::afterValidate()|afterValidate()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_AFTER_VALIDATE|EVENT_AFTER_VALIDATE]]. 4. [[yii\db\ActiveRecord::beforeSave()|beforeSave()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_BEFORE_INSERT|EVENT_BEFORE_INSERT]] lub [[yii\db\ActiveRecord::EVENT_BEFORE_UPDATE|EVENT_BEFORE_UPDATE]]. Jeśli metoda zwróci false lub właściwość [[yii\base\ModelEvent::isValid]] ma wartość false, kolejne kroki są pomijane. 5. Proces właściwego dodawania lub aktulizowania danych. 6. [[yii\db\ActiveRecord::afterSave()|afterSave()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] lub [[yii\db\ActiveRecord::EVENT_AFTER_UPDATE|EVENT_AFTER_UPDATE]]. ### Cykl życia przy usuwaniu danych Podczas wywołania [[yii\db\ActiveRecord::delete()|delete()]], w celu usunięcia danych instancji Active Record, zachodzi następujący cykl: 1. [[yii\db\ActiveRecord::beforeDelete()|beforeDelete()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_BEFORE_DELETE|EVENT_BEFORE_DELETE]]. Jeśli metoda zwróci false lub właściwość [[yii\base\ModelEvent::isValid]] ma wartość false, kolejne kroki są pomijane. 2. Proces właściwego usuwania danych. 3. [[yii\db\ActiveRecord::afterDelete()|afterDelete()]]: uruchamia event [[yii\db\ActiveRecord::EVENT_AFTER_DELETE|EVENT_AFTER_DELETE]]. > Note: Wywołanie poniższych metod NIE uruchomi żadnego z powyższych cykli: > > - [[yii\db\ActiveRecord::updateAll()]] > - [[yii\db\ActiveRecord::deleteAll()]] > - [[yii\db\ActiveRecord::updateCounters()]] > - [[yii\db\ActiveRecord::updateAllCounters()]] ## Praca z transakcjami Są dwa sposoby użycia [transakcji](db-dao.md#performing-transactions) podczas pracy z Active Record. Pierwszy zakłada bezpośrednie ujęcie wywołań metod Active Record w blok transakcji, jak pokazano to poniżej: ```php $customer = Customer::findOne(123); Customer::getDb()->transaction(function($db) use ($customer) { $customer->id = 200; $customer->save(); // ...inne operacje bazodanowe... }); // lub alternatywnie $transaction = Customer::getDb()->beginTransaction(); try { $customer->id = 200; $customer->save(); // ...inne operacje bazodanowe... $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } ``` Drugi sposób polega na utworzeniu listy operacji bazodanowych, które wymagają transakcji za pomocą metody [[yii\db\ActiveRecord::transactions()]]. Dla przykładu: ```php class Customer extends ActiveRecord { public function transactions() { return [ 'admin' => self::OP_INSERT, 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, // powyższy zapis jest odpowiednikiem następującego skróconego: // 'api' => self::OP_ALL, ]; } } ``` Metoda [[yii\db\ActiveRecord::transactions()]] powinna zwracać tablicę, której klucze są nazwami [scenariuszy](structure-models.md#scenarios) a wartości to operacje bazodanowe, które powinny być objęte transakcją. Używaj następujących stałych do określenia typu operacji: * [[yii\db\ActiveRecord::OP_INSERT|OP_INSERT]]: operacja dodawania wykonywana za pomocą [[yii\db\ActiveRecord::insert()|insert()]]; * [[yii\db\ActiveRecord::OP_UPDATE|OP_UPDATE]]: operacja aktualizacji wykonywana za pomocą [[yii\db\ActiveRecord::update()|update()]]; * [[yii\db\ActiveRecord::OP_DELETE|OP_DELETE]]: operacja usuwania wykonywana za pomocą [[yii\db\ActiveRecord::delete()|delete()]]. Używaj operatora `|`, aby podać więcej niż jedną operację za pomocą powyższych stałych. Możesz również użyć stałej dla skróconej definicji wszystkich trzech powyższych operacji [[yii\db\ActiveRecord::OP_ALL|OP_ALL]]. ## Optymistyczna blokada Optymistyczne blokowanie jest jednym ze sposobów uniknięcia konfliktów, które mogą wystąpić, kiedy pojedynczy wiersz danych jest aktualizowany przez kilku użytkowników. Dla przykładu, użytkownik A i użytkownik B edytują artykuł wiki w tym samym czasie - po tym jak użytkownik A zapisał już swoje zmiany, użytkownik B klika przycisk "Zapisz", aby również wykonać identyczną operację. Ponieważ użytkownik B pracował w rzeczywistości na "starej" wersji artykułu, byłoby wskazane powstrzymać go przed nadpisaniem wersji użytkownika A i wyświelić komunikat wyjaśniający sytuację. Optymistyczne blokowanie rozwiązuje ten problem za pomocą dodatkowej kolumny w bazie przechowującej numer wersji każdego wiersza. Kiedy taki wiersz jest zapisywany z wcześniejszym numerem wersji niż aktualna rzucany jest wyjątek [[yii\db\StaleObjectException]], który powstrzymuje zapis wiersza. Optymistyczne blokowanie może być użyte tylko przy aktualizacji lub usuwaniu istniejącego wiersza za pomocą odpowiednio [[yii\db\ActiveRecord::update()]] lub [[yii\db\ActiveRecord::delete()]]. Aby skorzystać z optymistycznej blokady: 1. Stwórz kolumnę w tabeli bazy danych powiązaną z klasą Active Record do przechowywania numeru wersji każdego wiersza. Kolumna powinna być typu big integer (przykładowo w MySQL `BIGINT DEFAULT 0`). 2. Nadpisz metodę [[yii\db\ActiveRecord::optimisticLock()]], aby zwrócić nazwę tej kolumny. 3. W formularzu pobierającym dane od użytkownika, dodaj ukryte pole, gdzie przechowasz aktualny numer wersji uaktualnianego wiersza. Upewnij się, że atrybut wersji ma dodaną zasadę walidacji i przechodzi poprawnie jej proces. 4. W akcji kontrolera uaktualniającej wiersz za pomocą Active Record, użyj bloku try-catch, aby wyłapać wyjątek [[yii\db\StaleObjectException]]. Zaimplemetuj odpowiednią logikę biznesową (np. scalenie zmian, wyświetlenie komunikatu o nieaktualnej wersji, itp.), aby rozwiązać konflikt. Dla przykładu, załóżmy, że kolumna wersji nazywa się `version`. Implementację optymistycznego blokowania można wykonać za pomocą następującego kodu: ```php // ------ kod widoku ------- use yii\helpers\Html; // ...inne pola formularza echo Html::activeHiddenInput($model, 'version'); // ------ kod kontrolera ------- use yii\db\StaleObjectException; public function actionUpdate($id) { $model = $this->findModel($id); try { if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('update', [ 'model' => $model, ]); } } catch (StaleObjectException $e) { // logika rozwiązująca konflikt } } ``` ## Praca z danymi relacji Oprócz korzystania z indywidualnych tabel bazy danych, Active Record umożliwia również na uzyskanie danych relacji, pozwalając na odczytanie ich z poziomu głównego obiektu. Dla przykładu, dane klienta są powiązane relacją z danymi zamówienia, ponieważ jeden klient może złożyć jedno lub wiele zamówień. Odpowiednio deklarując tę relację, można uzyskać dane zamówienia klienta, używając wyrażenia `$customer->orders`, które zwróci informacje o zamówieniu klienta jako tablicę instancji `Order` typu Active Record. ### Deklarowanie relacji Aby móc pracować z relacjami używając Active Record, najpierw musisz je zadeklarować w obrębie klasy. Deklaracja odbywa się za pomocą utworzenia prostej *metody relacyjnej* dla każdej relacji osobno, jak w przykładach poniżej: ```php class Customer extends ActiveRecord { public function getOrders() { return $this->hasMany(Order::className(), ['customer_id' => 'id']); } } class Order extends ActiveRecord { public function getCustomer() { return $this->hasOne(Customer::className(), ['id' => 'customer_id']); } } ``` W powyższym kodzie, zadeklarowano relację `orders` dla klasy `Customer` i relację `customer` dla klasy `Order`. Każda metoda relacyjna musi mieć nazwę utworzoną według wzoru `getXyz`. `xyz` (pierwsza litera jest mała) jest *nazwą relacji*. Zwróć uwagę na to, że nazwy relacji *uwzględniają wielkość liter*. Deklarując relację powinno się zwrócić uwagę na następujące dane: - mnogość relacji: określona przez wywołanie odpowiednio [[yii\db\ActiveRecord::hasMany()|hasMany()]] lub [[yii\db\ActiveRecord::hasOne()|hasOne()]]. W powyższym przykładzie można łatwo zobaczyć w definicji relacji, że klient może mieć wiele zamówień, podczas gdy zamówienie ma tylko jednego klienta. - nazwę powiązanej klasy Active Record: określoną jako pierwszy argument w [[yii\db\ActiveRecord::hasMany()|hasMany()]] lub [[yii\db\ActiveRecord::hasOne()|hasOne()]]. Rekomendowany sposób uzyskania nazwy klasy to wywołanie `Xyz::className()`, dzięki czemu możemy posiłkować się wsparciem autouzupełniania IDE i wykrywaniem błędów na poziomie kompilacji. - powiązanie pomiędzy dwoma rodzajami danych: określone jako kolumna(y), poprzez którą dane nawiązują relację. Wartości tablicy są kolumnami głównych danych (reprezentowanymi przez klasę Active Record, w której deklaruje się relacje), a klucze tablicy są kolumnami danych relacyjnych. Aby łatwo opanować technikę deklarowania relacji wystarczy zapamiętać, że kolumnę należącą do relacyjnej klasy Active Record zapisuje się zaraz obok jej nazwy (jak to widać w przykładzie powyżej - `customer_id` jest właściwością `Order` a `id` jest właściwością `Customer`). ### Uzyskiwanie dostępu do danych relacji Po zadeklarowaniu relacji, możesz uzyskać dostęp do danych poprzez jej nazwę. Odbywa się to w taki sam sposób jak uzyskiwanie dostępu do [właściwości](concept-properties.md) obiektu zdefiniowanego w metodzie relacyjnej. Właśnie dlatego też nazywamy je *właściwościami relacji*. Przykład: ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 // $orders jest tablicą obiektów typu Order $orders = $customer->orders; ``` > Info: Deklarując relację o nazwie `xyz` poprzez metodę-getter `getXyz()`, uzyskasz dostęp do `xyz` jak do [właściwości obiektu](concept-properties.md). > Zwróć uwagę na to, że nazwa uwzględnia wielkość liter. Jeśli relacja jest zadeklarowana poprzez [[yii\db\ActiveRecord::hasMany()|hasMany()]], zwraca tablicę powiązanych instancji Active Record; jeśli deklaracja odbywa się poprzez [[yii\db\ActiveRecord::hasOne()|hasOne()]], zwraca pojedynczą powiązaną instancję Active Record lub wartość null, w przypadku, gdy nie znaleziono powiązanych danych. Podczas pierwszego odwołania się do właściwości relacji wykonywana jest kwerenda SQL, tak jak pokazano to w przykładzie powyżej. Odwołanie się do tej samej właściwości kolejny raz zwróci poprzedni wynik, bez wykonywanie ponownie kwerendy. Aby wymusić wykonanie kwerendy w takiej sytuacji, należy najpierw usunąć z pamięci właściwość relacyjną poprzez `unset($customer->orders)`. > Note: Pomimo podobieństwa mechanizmu relacji do [właściwości obiektu](concept-properties.md), jest tutaj znacząca różnica. > Wartości właściwości zwykłych obiektów są tego samego typu jak definiująca je metoda-getter. > Metoda relacyjna zwraca jednak instancję [[yii\db\ActiveQuery]], a właściwości relacji są instancjami [[yii\db\ActiveRecord]] lub tablicą takich obiektów. > > ```php > $customer->orders; // tablica obiektów `Order` > $customer->getOrders(); // instancja ActiveQuery > ``` > > Taka funkcjonalność jest użyteczna przy tworzeniu kwerend dostosowanych do potrzeb programisty, co opisane jest w następnej sekcji. ### Dynamiczne kwerendy relacyjne Dzięki temu, że metoda relacyjna zwraca instancję [[yii\db\ActiveQuery]], możliwe jest dalsze rozbudowanie takiej kwerendy korzystając z metod konstruowania kwerend. Dla przykładu: ```php $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `subtotal` > 200 ORDER BY `id` $orders = $customer->getOrders() ->where(['>', 'subtotal', 200]) ->orderBy('id') ->all(); ``` Inaczej niż w przypadku właściwości relacji, za każdym razem, gdy wywyłujesz dynamiczną kwerendę relacyjną poprzez metodę relacji, wykonywane jest zapytanie do bazy, nawet jeśli identyczna kwerenda została już wywołana wcześniej. Możliwe jest także sparametryzowanie deklaracji relacji, dzięki czemu można w łatwiejszy sposób wykonywać relacyjne kwerendy. Dla przykładu, możesz zadeklarować relację `bigOrders` jak to pokazano poniżej: ```php class Customer extends ActiveRecord { public function getBigOrders($threshold = 100) { return $this->hasMany(Order::className(), ['customer_id' => 'id']) ->where('subtotal > :threshold', [':threshold' => $threshold]) ->orderBy('id'); } } ``` Dzięki czemu możesz wykonać następujące relacyjne kwerendy: ```php // SELECT * FROM `order` WHERE `subtotal` > 200 ORDER BY `id` $orders = $customer->getBigOrders(200)->all(); // SELECT * FROM `order` WHERE `subtotal` > 100 ORDER BY `id` $orders = $customer->bigOrders; ``` ### Relacje za pomocą tabeli węzła W projekcie bazy danych, kiedy połączenie pomiędzy dwoma relacyjnymi tabelami jest typu wiele-do-wielu, zwykle stosuje się tzw. [tabelę węzła](https://en.wikipedia.org/wiki/Junction_table). Dla przykładu, tabela `order` i tabela `item` mogą być powiązane poprzez węzeł nazwany `order_item`. Jedno zamówienie będzie posiadało wiele produktów zamówienia (pozycji), a każdy indywidualny produkt będzie także powiązany z wieloma pozycjami zamówienia. Deklarując takie relacje, możesz wywołać zarówno metodę [[yii\db\ActiveQuery::via()|via()]] jak i [[yii\db\ActiveQuery::viaTable()|viaTable()]], aby określić tabelę węzła. Różnica pomiędzy [[yii\db\ActiveQuery::via()|via()]] i [[yii\db\ActiveQuery::viaTable()|viaTable()]] jest taka, że pierwsza metoda definiuje tabelę węzła dla istniejącej nazwy relacji, podczas gdy druga definiuje bezpośrednio węzeł. Przykład: ```php class Order extends ActiveRecord { public function getItems() { return $this->hasMany(Item::className(), ['id' => 'item_id']) ->viaTable('order_item', ['order_id' => 'id']); } } ``` lub alternatywnie, ```php class Order extends ActiveRecord { public function getOrderItems() { return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); } public function getItems() { return $this->hasMany(Item::className(), ['id' => 'item_id']) ->via('orderItems'); } } ``` Sposób użycia relacji zadeklarowanych z pomocą tabeli węzła jest taki sam jak dla zwykłych relacji. Dla przykładu: ```php // SELECT * FROM `order` WHERE `id` = 100 $order = Order::findOne(100); // SELECT * FROM `order_item` WHERE `order_id` = 100 // SELECT * FROM `item` WHERE `item_id` IN (...) // zwraca tablicę obiektów Item $items = $order->items; ``` ### Pobieranie leniwe i gorliwe W sekcji [Uzyskiwanie dostępu do danych relacji](#accessing-relational-data) wyjaśniliśmy, że można uzyskać dostęp do właściwości relacji instancji Active Record w identyczny sposób jak w przypadku zwykłych właściwości obiektu. Kwerenda SQL zostanie wykonana tylko w momencie pierwszego odwołania się do właściwości relacji. Taki sposób uzyskiwania relacyjnych danych nazywamy *pobieraniem leniwym*. Przykład: ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 $orders = $customer->orders; // bez wykonywania zapytania SQL $orders2 = $customer->orders; ``` Leniwe pobieranie jest bardzo wygodne w użyciu, może jednak powodować spadek wydajności aplikacji, kiedy konieczne jest uzyskanie dostępu do tej samej relacyjnej właściwości dla wielu instancji Active Record. Rozważmy poniższy przykład - ile zapytań SQL zostanie wykonanych? ```php // SELECT * FROM `customer` LIMIT 100 $customers = Customer::find()->limit(100)->all(); foreach ($customers as $customer) { // SELECT * FROM `order` WHERE `customer_id` = ... $orders = $customer->orders; } ``` Jak wynika z opisu powyżej, zostanie wykonanych aż 101 kwerend SQL! Dzieje się tak, ponieważ za każdym razem, gdy uzyskujemy dostęp do właściwości relacyjnej `orders` dla kolejnego obiektu `Customer` w pętli, wykonywane jest nowe zapytanie SQL. Aby rozwiązać ten wydajnościowy problem, należy użyć tak zwanego *gorliwego pobierania*, jak w przykładzie poniżej: ```php // SELECT * FROM `customer` LIMIT 100; // SELECT * FROM `orders` WHERE `customer_id` IN (...) $customers = Customer::find() ->with('orders') ->limit(100) ->all(); foreach ($customers as $customer) { // kwerenda SQL nie jest wykonywana $orders = $customer->orders; } ``` Wywołanie metody [[yii\db\ActiveQuery::with()]] powoduje pobranie zamówień dla pierwszych 100 klientów w pojedynczej kwerendzie SQL, dzięki czemu redukujemy ilość zapytań ze 101 do 2! Możliwe jest gorliwe pobranie jednej lub wielu relacji, a nawet gorliwe pobranie *zagnieżdżonych relacji*. Zagnieżdżona relacja to taka, która została zadeklarowana w relacyjnej klasie Active Record. Dla przykładu, `Customer` jest powiązany z `Order` poprzez relację `orders`, a `Order` jest powiązany z `Item` poprzez relację `items`. Ładując dane dla `Customer`, możesz gorliwie pobrać `items` używając notacji zagnieżdżonej relacji `orders.items`. Poniższy kod pokazuje różne sposoby użycia [[yii\db\ActiveQuery::with()|with()]]. Zakładamy, że klasa `Customer` posiada dwie relacje `orders` i `country`, a klasa `Order` jedną relację `items`. ```php // gorliwe pobieranie "orders" i "country" $customers = Customer::find()->with('orders', 'country')->all(); // odpowiednik powyższego w zapisie tablicowym $customers = Customer::find()->with(['orders', 'country'])->all(); // kwerenda SQL nie jest wykonywana $orders= $customers[0]->orders; // kwerenda SQL nie jest wykonywana $country = $customers[0]->country; // gorliwe pobieranie "orders" i zagnieżdżonej relacji "orders.items" $customers = Customer::find()->with('orders.items')->all(); // uzyskanie dostępu do produktów pierwszego zamówienia pierwszego klienta // kwerenda SQL nie jest wykonywana $items = $customers[0]->orders[0]->items; ``` Możesz pobrać gorliwie także głęboko zagnieżdżone relacje, jak np. `a.b.c.d`. Każda z kolejnych następujących po sobie relacji zostanie pobrana gorliwie - wywołując [[yii\db\ActiveQuery::with()|with()]] z `a.b.c.d`, pobierzesz `a`, `a.b`, `a.b.c` i `a.b.c.d`. > Info: Podsumowując, podczas gorliwego pobierania `N` relacji, pośród których `M` relacji jest zdefiniowanych za pomocą > [tabeli węzła](#junction-table), zostanie wykonanych łącznie `N+M+1` kwerend SQL. > Zwróć uwagę na to, że zagnieżdżona relacja `a.b.c.d` jest liczona jako 4 relacje. Podczas gorliwego pobierania relacji, możesz dostosować kwerendę do własnych potrzeb korzystając z funkcji anonimowej. Przykład: ```php // znajdź klientów i pobierz ich kraje zamieszkania i aktywne zamówienia // SELECT * FROM `customer` // SELECT * FROM `country` WHERE `id` IN (...) // SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1 $customers = Customer::find()->with([ 'country', 'orders' => function ($query) { $query->andWhere(['status' => Order::STATUS_ACTIVE]); }, ])->all(); ``` Dostosowując relacyjną kwerendę należy podać nazwę relacji jako klucz tablicy i użyć funkcji anonimowej jako odpowiadającej kluczowi wartości. Funkcja anonimowa otrzymuje parametr `$query`, reprezentujący obiekt [[yii\db\ActiveQuery]], służący do wykonania relacyjnej kwerendy. W powyższym przykładzie modyfikujemy relacyjną kwerendę dodając warunek ze statusem zamówienia. > Note: Wywołując [[yii\db\Query::select()|select()]] podczas gorliwego pobierania relacji, należy upewnić się, że kolumny określone w deklaracji > relacji znajdują się na liście pobieranych. W przeciwnym razie powiązany model może nie zostać poprawnie załadowany. Przykład: > > ```php > $orders = Order::find()->select(['id', 'amount'])->with('customer')->all(); > // $orders[0]->customer ma zawsze wartość null. Aby rozwiązać ten problem, należy użyć: > $orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all(); > ``` ### Przyłączanie relacji > Note: Zawartość tej sekcji odnosi się tylko do relacyjnych baz danych, takich jak MySQL, PostgreSQL, itp. Relacyjne kwerendy opisane do tej pory jedynie nawiązują do głównych kolumn tabeli podczas pobierania danych. W rzeczywistości często musimy odnieść się do kolumn w powiązanych tabelach. Przykładowo chcemy pobrać klientów, którzy złożyli przynajmniej jedno aktywne zamówienie - możemy tego dokonać za pomocą następującej przyłączającej kwerendy: ```php // SELECT `customer`.* FROM `customer` // LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` // WHERE `order`.`status` = 1 // // SELECT * FROM `order` WHERE `customer_id` IN (...) $customers = Customer::find() ->select('customer.*') ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`') ->where(['order.status' => Order::STATUS_ACTIVE]) ->with('orders') ->all(); ``` > Note: Podczas tworzenia relacyjnych kwerend zawierających instrukcję SQL JOIN koniecznym jest ujednoznacznienie nazw kolumn. > Standardową praktyką w takim wypadku jest poprzedzenie nazwy kolumny odpowiadającą jej nazwą tabeli. Jeszcze lepszym rozwiązaniem jest użycie istniejącej deklaracji relacji wywołując metodę [[yii\db\ActiveQuery::joinWith()]]: ```php $customers = Customer::find() ->joinWith('orders') ->where(['order.status' => Order::STATUS_ACTIVE]) ->all(); ``` Oba rozwiązania wykonują te same zestawy instrukcji SQL, ale ostatnie jest o wiele schludniejsze. [[yii\db\ActiveQuery::joinWith()|joinWith()]] domyślnie korzysta z `LEFT JOIN` do przyłączenia głównej tabeli z relacyjną. Możesz określić inny typ przyłączenia (np. `RIGHT JOIN`) podając trzeci parametr `$joinType`. Jeśli chcesz użyć typu przyłączenia `INNER JOIN`, możesz bezpośrednio wywołać metodę [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]]. Wywołanie [[yii\db\ActiveQuery::joinWith()|joinWith()]] domyślnie [pobierze gorliwie](#lazy-eager-loading) dane relacyjne. Jeśli nie chcesz pobierać danych w ten sposób, możesz ustawić drugi parametr `$eagerLoading` na false. Tak jak w przypadku [[yii\db\ActiveQuery::with()|with()]], możesz przyłączyć jedną lub wiele relacji na raz, dodać do nich dodatkowe warunki, przyłączyć zagnieżdżone relacje i korzystać z zarówno [[yii\db\ActiveQuery::with()|with()]] jak i [[yii\db\ActiveQuery::joinWith()|joinWith()]]. Przykładowo: ```php $customers = Customer::find()->joinWith([ 'orders' => function ($query) { $query->andWhere(['>', 'subtotal', 100]); }, ])->with('country') ->all(); ``` Czasem, przyłączając dwie tabele, musisz sprecyzować dodatkowe warunki dla części `ON` kwerendy JOIN. Można to zrobić wywołując metodę [[yii\db\ActiveQuery::onCondition()]] w poniższy sposób: ```php // SELECT `customer`.* FROM `customer` // LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 // // SELECT * FROM `order` WHERE `customer_id` IN (...) $customers = Customer::find()->joinWith([ 'orders' => function ($query) { $query->onCondition(['order.status' => Order::STATUS_ACTIVE]); }, ])->all(); ``` Powyższa kwerenda pobiera *wszystkich* klientów i dla każdego z nich pobiera wszystkie aktywne zamówienia. Zwróć uwagę na to, że ten przykład różni się od poprzedniego, gdzie pobierani byli tylko klienci posiadający przynajmniej jedno aktywne zamówienie. > Info: Jeśli [[yii\db\ActiveQuery]] zawiera warunek podany za pomocą [[yii\db\ActiveQuery::onCondition()|onCondition()]], > będzie on umieszczony w części instrukcji `ON` tylko jeśli kwerenda zawiera JOIN. W przeciwnym wypadku warunek ten będzie automatycznie > dodany do części `WHERE`. ### Odwrócone relacje Deklaracje relacji są zazwyczaj obustronne dla dwóch klas Active Record. Przykładowo `Customer` jest powiązany z `Order` poprzez relację `orders`, a `Order` jest powiązany jednocześnie z `Customer` za pomocą relacji `customer`. ```php class Customer extends ActiveRecord { public function getOrders() { return $this->hasMany(Order::className(), ['customer_id' => 'id']); } } class Order extends ActiveRecord { public function getCustomer() { return $this->hasOne(Customer::className(), ['id' => 'customer_id']); } } ``` Rozważmy teraz poniższy kod: ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 $order = $customer->orders[0]; // SELECT * FROM `customer` WHERE `id` = 123 $customer2 = $order->customer; // zwraca "różne" echo $customer2 === $customer ? 'takie same' : 'różne'; ``` Wydawałoby się, że `$customer` i `$customer2` powinny być identyczne, ale jednak nie są! W rzeczywistości zawierają takie same dane klienta, ale są różnymi obiektami. Wywołując `$order->customer` wykonywana jest dodatkowa kwerenda SQL do wypełnienia nowego obiektu `$customer2`. Aby uniknąć nadmiarowego wykonywania ostatniej kwerendy SQL w powyższym przykładzie, powinniśmy wskazać Yii, że `customer` jest *odwróconą relacją* `orders` wywołując metodę [[yii\db\ActiveQuery::inverseOf()|inverseOf()]] jak pokazano to poniżej: ```php class Customer extends ActiveRecord { public function getOrders() { return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer'); } } ``` Z tą dodatkową instrukcją w deklaracji relacji uzyskamy: ```php // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // SELECT * FROM `order` WHERE `customer_id` = 123 $order = $customer->orders[0]; // kwerenda SQL nie jest wykonywana $customer2 = $order->customer; // wyświetla "takie same" echo $customer2 === $customer ? 'takie same' : 'różne'; ``` > Note: Odwrócone relacje nie mogą być definiowane dla relacji zawierających [tabelę węzła](#junction-table), dlatego też definiując relację z użyciem > [[yii\db\ActiveQuery::via()|via()]] lub [[yii\db\ActiveQuery::viaTable()|viaTable()]] nie powinno się już wywoływać > [[yii\db\ActiveQuery::inverseOf()|inverseOf()]]. ## Zapisywanie relacji Podczas pracy z danymi relacyjnymi często konieczne jest ustalenie związku pomiędzy różnymi danymi lub też usunięcie istniejącego połączenia. Takie akcje wymagają ustalenia właściwych wartości dla kolumn definiujących relacje. Korzystając z Active Record można użyć następujących instrukcji: ```php $customer = Customer::findOne(123); $order = new Order(); $order->subtotal = 100; // ... // ustawianie wartości dla atrybutu definiującego relację "customer" dla Order $order->customer_id = $customer->id; $order->save(); ``` Active Record zawiera metodę [[yii\db\ActiveRecord::link()|link()]], która pozwala na uzyskanie powyższego w efektywniejszy sposób: ```php $customer = Customer::findOne(123); $order = new Order(); $order->subtotal = 100; // ... $order->link('customer', $customer); ``` Metoda [[yii\db\ActiveRecord::link()|link()]] wymaga podania konkretnej nazwy relacji i docelowej instancji Active Record, z którą powinna być nawiązany związek. Mechanizm ten zmodyfikuje wartości atrybutów łączących obie instancje Active Record i zapisze je w bazie danych. W powyższym przykładzie atrybut `customer_id` instancji `Order` otrzyma wartość atrybutu `id` instancji `Customer`, a następnie zostanie zapisany w bazie danych. > Note: Nie możesz łączyć w ten sposób dwóch świeżo utworzonych instancji Active Record. Zaleta używania [[yii\db\ActiveRecord::link()|link()]] jest jeszcze bardziej widoczna, jeśli relacja jest zdefiniowana poprzez [tabelę węzła](#junction-table). Przykładowo możesz użyć następującego kodu, aby połączyć instancję `Order` z instancją `Item`: ```php $order->link('items', $item); ``` Powyższy przykład automatycznie doda nowy wiersz w tabeli węzła `order_item`, aby połączyć zamówienie z produktem. > Info: Metoda [[yii\db\ActiveRecord::link()|link()]] NIE wykona automatycznie żadnego procesu walidacji danych podczas zapisywania instancji Active Record. > Na Tobie spoczywa obowiązek walidacji wszystkich danych przed wywołaniem tej metody. Odwrotną operacją do [[yii\db\ActiveRecord::link()|link()]] jest [[yii\db\ActiveRecord::unlink()|unlink()]], która usuwa istniejący związek pomiędzy dwoma instancjami Active Record. Przykładowo: ```php $customer = Customer::find()->with('orders')->all(); $customer->unlink('orders', $customer->orders[0]); ``` Domyślnie metoda [[yii\db\ActiveRecord::unlink()|unlink()]] ustawia wartość klucza(y) obcego, który definiuje istniejącą relację, na null. Można jednak zamiast tego wybrać opcję usuwania wiersza tabeli, który zawiera klucz obcy, ustawiając w metodzie parametr `$delete` na true. Jeśli w relacji użyty jest węzeł, wywołanie [[yii\db\ActiveRecord::unlink()|unlink()]] spowoduje wyczyszczenie kluczy obcych w tabeli węzła lub też usunięcie odpowiadających im wierszy, jeśli `$delete` jest ustawione na true. ## Relacje międzybazowe Active Record pozwala na deklarowanie relacji pomiędzy klasami Active Record zasilanymi przez różne bazy danych. Bazy danych mogę być różnych typów (np. MySQL i PostgreSQL lub MS SQL i MongoDB) i mogą pracować na różnych serwerach. Do wykonania relacyjnych zapytań używa się takich samych procedur, jak w przypadku relacji w obrębie jednej bazy danych. Przykład: ```php // Customer jest powiązany z tabelą "customer" w relacyjnej bazie danych (np. MySQL) class Customer extends \yii\db\ActiveRecord { public static function tableName() { return 'customer'; } public function getComments() { // klient posiada wiele komentarzy return $this->hasMany(Comment::className(), ['customer_id' => 'id']); } } // Comment jest powiązany z kolekcją "comment" w bazie danych MongoDB class Comment extends \yii\mongodb\ActiveRecord { public static function collectionName() { return 'comment'; } public function getCustomer() { // komentarz jest przypisany do jednego klienta return $this->hasOne(Customer::className(), ['id' => 'customer_id']); } } $customers = Customer::find()->with('comments')->all(); ``` Możesz używać większości funkcjonalności dostępnych dla relacyjnych kwerend opisanych w tym rozdziale. > Note: Użycie [[yii\db\ActiveQuery::joinWith()|joinWith()]] jest ograniczone do baz danych pozwalających na międzybazowe kwerendy JOIN, dlatego też > nie możesz użyć tej metody w powyższym przykładzie, ponieważ MongoDB nie wspiera instrukcji JOIN. ## Niestandardowe klasy kwerend Domyślnie wszystkie kwerendy Active Record używają klasy [[yii\db\ActiveQuery]]. Aby użyć niestandardowej klasy kwerend razem z klasą Active Record, należy nadpisać metodę [[yii\db\ActiveRecord::find()]], aby zwracała instancję żądanej klasy kwerend. Przykład: ```php namespace app\models; use yii\db\ActiveRecord; use yii\db\ActiveQuery; class Comment extends ActiveRecord { public static function find() { return new CommentQuery(get_called_class()); } } class CommentQuery extends ActiveQuery { // ... } ``` Od tego momentu, za każdym razem, gdy wykonywana będzie kwerenda (np. `find()`, `findOne()`) lub pobierana relacja (np. `hasOne()`) klasy `Comment`, praca będzie odbywać się na instancji `CommentQuery` zamiast `ActiveQuery`. > Tip: Dla dużych projektów rekomendowane jest, aby używać własnych, odpowiednio dopasowanych do potrzeb, klas kwerend, dzięki czemu klasy Active Record > pozostają przejrzyste. Możesz dopasować klasę kwerend do własnych potrzeb na wiele kreatywnych sposobów. Przykładowo, możesz zdefiniować nowe metody konstruujące zapytanie: ```php class CommentQuery extends ActiveQuery { public function active($state = true) { return $this->andWhere(['active' => $state]); } } ``` > Note: Zwykle, zamiast wywoływać metodę [[yii\db\ActiveQuery::where()|where()]], powinno się używać metody > [[yii\db\ActiveQuery::andWhere()|andWhere()]] lub [[yii\db\ActiveQuery::orWhere()|orWhere()]], aby dołączać kolejne warunki zapytania w > konstruktorze kwerend, dzięki czemu istniejące warunki nie zostaną nadpisane. Powyższy przykład pozwala na użycie następującego kodu: ```php $comments = Comment::find()->active()->all(); $inactiveComments = Comment::find()->active(false)->all(); ``` Możesz także użyć nowych metod budowania kwerend przy definiowaniu relacji z `Comment` lub wykonywaniu relacyjnych kwerend: ```php class Customer extends \yii\db\ActiveRecord { public function getActiveComments() { return $this->hasMany(Comment::className(), ['customer_id' => 'id'])->active(); } } $customers = Customer::find()->with('activeComments')->all(); // lub alternatywnie $customers = Customer::find()->with([ 'comments' => function($q) { $q->active(); } ])->all(); ``` > Info: W Yii 1.1 do tego celu służy mechanizm *podzbiorów (scope)*, nie jest on jednak bezpośrednio wspierany w Yii 2.0, a zamiast tego > powinno się używać dopasowanych do własnych potrzeb klas kwerend. ## Pobieranie dodatkowych pól W momencie, gdy instancja Active Record pobiera dane z wyniku kwerendy, wartości kolumn przypisywane są do odpowiadających im atrybutów. Możliwe jest pobranie dodatkowych kolumn lub wartości za pomocą kwerendy i przypisanie ich w Active Record. Przykładowo załóżmy, że mamy tabelę 'room', która zawiera informacje o pokojach dostępnych w hotelu. Każdy pokój przechowuje informacje na temat swojej wielkości za pomocą pól 'length', 'width' i 'height'. Teraz wyobraźmy sobie, że potrzebujemy pobrać listę wszystkich pokojów posortowaną po ich kubaturze w malejącej kolejności. Nie możemy obliczyć kubatury korzystając z PHP, ponieważ zależy nam na szybkim posortowaniu rekordów i dodatkowo chcemy wyświetlić pole 'volume' na liście. Aby osiągnąć ten cel, musimy zadeklarować dodatkowe pole w klasie 'Room' rozszerzającej Active Record, które przechowa wartość 'volume': ```php class Room extends \yii\db\ActiveRecord { public $volume; // ... } ``` Następnie należy skonstruować kwerendę, która obliczy kubaturę i wykona sortowanie: ```php $rooms = Room::find() ->select([ '{{room}}.*', // pobierz wszystkie kolumny '([[length]] * [[width]].* [[height]]) AS volume', // oblicz kubaturę ]) ->orderBy('volume DESC') // posortuj ->all(); foreach ($rooms as $room) { echo $room->volume; // zawiera wartość obliczoną przez SQL } ``` Możliwość pobrania dodatkowych pól jest szczególnie pomocna przy kwerendach agregujących. Załóżmy, że potrzebujesz wyświetlić listę klientów wraz z liczbą zamówień, których dokonali. Najpierw musisz zadeklarować klasę `Customer` wraz z relacją 'orders' i dodatkowym polem przechowującym liczbę zamówień: ```php class Customer extends \yii\db\ActiveRecord { public $ordersCount; // ... public function getOrders() { return $this->hasMany(Order::className(), ['customer_id' => 'id']); } } ``` Teraz już możesz skonstruować kwerendę, która przyłączy zamówienia i policzy ich liczbę: ```php $customers = Customer::find() ->select([ '{{customer}}.*', // pobierz wszystkie kolumny klienta 'COUNT({{order}}.id) AS ordersCount' // oblicz ilość zamówień ]) ->joinWith('orders') // przyłącz tabelę węzła ->groupBy('{{customer}}.id') // pogrupuj wyniki dla funkcji agregacyjnej ->all(); ```