0 follower

Active Record

Active Record 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:

$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:

$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 8.4 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 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 ActiveRecord.
### Deklarowanie nazwy tabeli

Domyślnie każda klasa Active Record jest powiązana ze swoją tabelą w bazie danych. Metoda tableName() zwraca nazwę tabeli konwertując nazwę klasy za pomocą yii\helpers\Inflector::camel2id(). Możesz przeciążyć tę metodę, jeśli tabela nie jest nazwana zgodnie z tą konwencją.

Identycznie zastosowany może być domyślny prefiks tabeli tablePrefix. Przykładowo, jeśli tablePrefix to tbl_, tabelą klasy Customer staje się tbl_customer, a dla OrderItem jest to tbl_order_item.

Jeśli nazwa tabeli zostanie podana jako {{%NazwaTabeli}}, znak procent % zostanie zamieniony automatycznie na prefiks tabeli. Dla przykładu, {{%post}} staje się {{tbl_post}}. Nawiasy wokół nazwy tabeli są używane dla odpowiedniego podawania nazw w kwerendach SQL.

W poniższym przykładzie deklarujemy klasę Active Record nazwaną Customer dla tabeli customer w bazie danych.

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}}';
    }
}

Aktywne rekordy nazywane są "modelami"

Instancje Active Record są traktowane jak modele. Z tego powodu zwykle dodajemy klasy Active Record do przestrzeni nazw app\models (lub innej, przeznaczonej dla klas modeli).

Dzięki temu, że ActiveRecord rozszerza Model, dziedziczy wszystkie funkcjonalności modelu, takie jak atrybuty, zasady walidacji, serializację danych itd.

Łączenie się z bazą danych

Domyślnie Active Record używa komponentu aplikacji db jako 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), komponent db można skonfigurować w pliku konfiguracyjnym aplikacji jak poniżej:

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ę getDb():

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 find();
  2. Zbudowanie obiektu kwerendy za pomocą metod konstruktora kwerend;
  3. Wywołanie metod kwerendy w celu uzyskania danych jako instancji klasy Active Record.

Jak widać, procedura jest bardzo podobna do tej używanej przy konstruktorze kwerend. Jedyna różnica jest taka, że zamiast użycia operatora new do stworzenia obiektu kwerendy, wywołujemy metodę find(), która zwraca nowy obiekt kwerendy klasy ActiveQuery.

Poniżej znajdziesz kilka przykładów pokazujących jak używać Active Query do pobierania danych:

// 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.

Informacja: Dzięki temu, że ActiveQuery rozszerza klasę Query, możesz użyć wszystkich metod dotyczących kwerend i ich budowania opisanych w sekcji Konstruktor kwerend.

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:

  • findOne(): zwraca pojedynczą instancję klasy Active Record, zawierającą dane z pierwszego pobranego odpowiadającego zapytaniu wiersza danych.
  • 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.

Poniższy kod pokazuje, jak mogą być użyte opisane metody:

// 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,
]);

Uwaga: Ani metoda findOne() ani 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 findBySql():

// 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 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:

// "id" i "email" są nazwami kolumn w tabeli "customer"
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Uwaga: 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:

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'.

Wskazówka: Powyższy przykład pokazuje podstawowy sposób transformacji danych. Podczas zwyczajowej pracy z formularzami danych można skorzystać z DateValidator i yii\jui\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ę asArray() przed wykonaniem kwerendy:

// zwraca wszystkich klientów
// każdy klient jest zwracany w postaci tablicy asocjacyjnej
$customers = Customer::find()
    ->asArray()
    ->all();

Uwaga: 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 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:

// 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 save() w celu zapisania danych w bazie.

Przykład:

// 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 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 save() spowoduje dodanie nowego wiersza. Jeśli instacja jest wynikiem użycia kwerendy, wywołanie 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 isNewRecord. Jest ona także używana przez save() w poniższy sposób:

public function save($runValidation = true, $attributeNames = null)
{
    if ($this->getIsNewRecord()) {
        return $this->insert($runValidation, $attributeNames);
    } else {
        return $this->update($runValidation, $attributeNames) !== false;
    }
}

Wskazówka: Możesz również wywołać insert() lub update() bezpośrednio, aby, odpowiednio, dodać lub uaktualnić wiersz.

Walidacja danych

Dzięki temu, że ActiveRecord rozszerza klasę Model, korzysta z tych samych mechanizmów walidacji danych. Możesz definiować zasady walidacji nadpisując metodę rules() i uruchamiać procedurę walidacji wywołując metodę validate().

Wywołanie save() automatycznie wywołuje również metodę 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ść errors.

Wskazówka: 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, instancje Active Record posiadają również mechanizm masowego przypisywania. 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.

$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 updateCounters() służy do aktualizacji jednego lub wielu liczników. Przykład:

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

Uwaga: Jeśli używasz 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 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ę getDirtyAttributes(), aby otrzymać najnowszą listę brudnych atrybutów. Dodatkowo można wywołać markAttributeDirty(), aby oznaczyć konkretny atrybut jako brudny.

Jeśli chcesz sprawdzić wartość atrybutu sprzed ostatniej zmiany, możesz wywołać getOldAttributes() lub getOldAttribute().

Uwaga: 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: ['attributeName', 'filter', 'filter' => 'intval']. Działa on z wszystkimi funkcjami PHP rzutującymi typy jak intval(), floatval(), boolval, itp...

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ę loadDefaultValues(), która przypisze wszystkie domyślne wartości odpowiednim atrybutom:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz otrzyma domyślną wartość, zadeklarowaną przy definiowaniu kolumny "xyz"

Rzutowanie typów atrybutów

Po wypełnieniu rezultatem kwerendy, yii\db\ActiveRecord przeprowadza automatyczne rzutowanie typów na wartościach swoich atrybutów, używając do tego celu informacji zawartych w schemacie tabeli bazy danych. Pozwala to na prawidłowe przedstawienie danych pobranych z kolumny tabeli zadeklarowanej jako liczba całkowita, w postaci wartości typu PHP integer w instancji klasy ActiveRecord (typu boolean jako boolean itp.). Mechanizm rzutowania ma jednak kilka ograniczeń:

  • Wartości typu zmiennoprzecinkowego nie są konwertowane na float, a zamiast tego są przedstawiane jako łańcuch znaków, aby zachować dokładność ich liczbowej prezentacji.
  • Konwersja typu integer zależy od zakresu liczb całkowitych używanego systemu operacyjnego. Wartości kolumn zadeklarowanych jako 'unsigned integer' lub 'big integer' będą przekonwertowane do PHP integer tylko na systemach 64-bitowych, a na 32-bitowych będą przedstawione jako łańcuchy znaków.

Zwróć uwagę na to, że rzutowanie typów jest wykonywane tylko podczas wypełniania instancji ActiveRecord rezultatem kwerendy. Automatyczna konwersja nie jest przeprowadzana dla wartości załadowanych poprzez żądanie HTTP lub ustawionych bezpośrednio dla właściwości klasy. Schemat tabeli będzie również użyty do przygotowania instrukcji SQL przy zapisywaniu danych ActiveRecord, aby upewnić się, że wartości są przypisane w kwerendzie z prawidłowymi typami. Atrybuty instancji ActiveRecord nie będą jednak przekonwertowane w procesie zapisywania.

Wskazówka: możesz użyć yii\behaviors\AttributeTypecastBehavior, aby skonfigurować proces rzutowania typów dla wartości atrybutów w momencie ich walidacji lub zapisu.

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ę updateAll().

// 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ć updateAllCounters(), aby uaktualnić liczniki wielu wierszy w tym samym czasie.

// 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ę delete().

$customer = Customer::findOne(123);
$customer->delete();

Możesz również wywołać deleteAll(), aby usunąć kilka lub wszystkie wiersze danych. Dla przykładu:

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

Uwaga: Należy być bardzo ostrożnym przy wywoływaniu 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 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. init(): uruchamia event EVENT_INIT.

Cykl życia przy pobieraniu danych

Podczas pobierania danych za pomocą jednej z metod kwerendy, każdy świeżo wypełniony obiekt Active Record przechodzi następujący cykl:

  1. Konstruktor klasy.
  2. init(): uruchamia event EVENT_INIT.
  3. afterFind(): uruchamia event EVENT_AFTER_FIND.

Cykl życia przy zapisywaniu danych

Podczas wywołania save(), w celu dodania lub uaktualnienia danych instancji Active Record, zachodzi następujący cykl:

  1. beforeValidate(): uruchamia event EVENT_BEFORE_VALIDATE. Jeśli metoda zwróci false lub właściwość 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. afterValidate(): uruchamia event EVENT_AFTER_VALIDATE.
  4. beforeSave(): uruchamia event EVENT_BEFORE_INSERT lub EVENT_BEFORE_UPDATE. Jeśli metoda zwróci false lub właściwość isValid ma wartość false, kolejne kroki są pomijane.
  5. Proces właściwego dodawania lub aktulizowania danych.
  6. afterSave(): uruchamia event EVENT_AFTER_INSERT lub EVENT_AFTER_UPDATE.

Cykl życia przy usuwaniu danych

Podczas wywołania delete(), w celu usunięcia danych instancji Active Record, zachodzi następujący cykl:

  1. beforeDelete(): uruchamia event EVENT_BEFORE_DELETE. Jeśli metoda zwróci false lub właściwość isValid ma wartość false, kolejne kroki są pomijane.
  2. Proces właściwego usuwania danych.
  3. afterDelete(): uruchamia event EVENT_AFTER_DELETE.

Uwaga: Wywołanie poniższych metod NIE uruchomi żadnego z powyższych cykli:

Odświeżanie cyklu życia danych

Wywołanie refresh() w celu odświeżenia instancji Active Record, uruchamia event EVENT_AFTER_REFRESH, o ile odświeżenie się powiedzie i metoda zwróci true.

Praca z transakcjami

Są dwa sposoby użycia transakcji 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:

$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;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

Uwaga: w powyższym kodzie znajdują się dwa bloki catch dla kompatybilności z PHP 5.x i PHP 7.x. \Exception implementuje interfejs \Throwable od PHP 7.0, zatem można pominąć część z \Exception, jeśli Twoja aplikacja używa tylko PHP 7.0 lub wyższego.

Drugi sposób polega na utworzeniu listy operacji bazodanowych, które wymagają transakcji za pomocą metody transactions(). Dla przykładu:

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 transactions() powinna zwracać tablicę, której klucze są nazwami scenariuszy, 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:

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 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 StaleObjectException, który powstrzymuje zapis wiersza. Optymistyczne blokowanie może być użyte tylko przy aktualizacji lub usuwaniu istniejącego wiersza za pomocą odpowiednio update() lub 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ę 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 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:

// ------ 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:

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['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 hasMany() lub 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 hasMany() lub hasOne(). Rekomendowany sposób uzyskania nazwy klasy to wywołanie Xyz::class, 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 obiektu zdefiniowanego w metodzie relacyjnej. Właśnie dlatego też nazywamy je właściwościami relacji. Przykład:

// 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;

Informacja: Deklarując relację o nazwie xyz poprzez metodę-getter getXyz(), uzyskasz dostęp do xyz jak do właściwości obiektu. Zwróć uwagę na to, że nazwa uwzględnia wielkość liter.

Jeśli relacja jest zadeklarowana poprzez hasMany(), zwraca tablicę powiązanych instancji Active Record; jeśli deklaracja odbywa się poprzez 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).

Uwaga: Pomimo podobieństwa mechanizmu relacji do właściwości obiektu, 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ę ActiveQuery, a właściwości relacji są instancjami ActiveRecord lub tablicą takich obiektów.

$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ę ActiveQuery, możliwe jest dalsze rozbudowanie takiej kwerendy korzystając z metod konstruowania kwerend. Dla przykładu:

$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:

class Customer extends ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

Dzięki czemu możesz wykonać następujące relacyjne kwerendy:

// 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. 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ę via() jak i viaTable(), aby określić tabelę węzła. Różnica pomiędzy via() i 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:

class Order extends ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

lub alternatywnie,

class Order extends ActiveRecord
{
    public function getOrderItems()
    {
        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
    }

    public function getItems()
    {
        return $this->hasMany(Item::class, ['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:

// 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 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:

// 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?

// 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:

// 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 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 with(). Zakładamy, że klasa Customer posiada dwie relacje orders i country, a klasa Order jedną relację items.

// 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 with() z a.b.c.d, pobierzesz a, a.b, a.b.c i a.b.c.d.

Informacja: Podsumowując, podczas gorliwego pobierania N relacji, pośród których M relacji jest zdefiniowanych za pomocą tabeli węzła, 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:

// 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 ActiveQuery, służący do wykonania relacyjnej kwerendy. W powyższym przykładzie modyfikujemy relacyjną kwerendę dodając warunek ze statusem zamówienia.

Uwaga: Wywołując 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:

$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

Uwaga: 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:

// 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();

Uwaga: 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ę joinWith():

$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.

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ę innerJoinWith().

Wywołanie joinWith() domyślnie pobierze gorliwie dane relacyjne. Jeśli nie chcesz pobierać danych w ten sposób, możesz ustawić drugi parametr $eagerLoading na false.

Tak jak w przypadku 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 with() jak i joinWith(). Przykładowo:

$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ę onCondition() w poniższy sposób:

// 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.

Informacja: Jeśli ActiveQuery zawiera warunek podany za pomocą 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. Może zatem składać się z warunków opierających się tylko na kolumnach powiązanej tabeli.

Aliasy dołączanych tabeli

Jak już wspomniano wcześniej, używając JOIN w kwerendzie, musimy ujednoznacznić nazwy kolumn. Z tego powodu często stosuje się aliasy dla tabel. Alias dla kwerendy relacyjnej można ustawić, modyfikując ją w następujący sposób:

$query->joinWith([
    'orders' => function ($q) {
        $q->from(['o' => Order::tableName()]);
    },
])

Powyższy sposób wygląda jednak na bardzo skomplikowany i wymaga ręcznej modyfikacji w kodzie nazwy tabeli obiektu relacji lub wywołania Order::tableName(). Od wersji 2.0.7, Yii udostępnia do tego celu skróconą metodę. Możliwe jest zdefiniowanie i używanie aliasu dla tabeli relacji w poniższy sposób:

// dołącz relację 'orders' i posortuj wyniki po 'orders.id'
$query->joinWith(['orders o'])->orderBy('o.id');

Powyższy kod działa dla prostych relacji. Jeśli jednak potrzebujesz aliasu dla tabeli dołączonej w zagnieżdżonej relacji, np. $query->joinWith(['orders.product']), musisz rozwinąć wywołanie joinWith jak w poniższym przykładzie:

$query->joinWith(['orders o' => function($q) {
        $q->joinWith('product p');
    }])
    ->where('o.amount > 100');

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.

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

Rozważmy teraz poniższy kod:

// 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ę inverseOf() jak pokazano to poniżej:

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
    }
}

Z tą dodatkową instrukcją w deklaracji relacji uzyskamy:

// 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';

Uwaga: Odwrócone relacje nie mogą być definiowane dla relacji zawierających tabelę węzła, dlatego też definiując relację z użyciem via() lub viaTable() nie powinno się już wywoływać 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:

$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ę link(), która pozwala na uzyskanie powyższego w efektywniejszy sposób:

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

$order->link('customer', $customer);

Metoda 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.

Uwaga: Nie możesz łączyć w ten sposób dwóch świeżo utworzonych instancji Active Record.

Zaleta używania link() jest jeszcze bardziej widoczna, jeśli relacja jest zdefiniowana poprzez tabelę węzła. Przykładowo możesz użyć następującego kodu, aby połączyć instancję Order z instancją Item:

$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.

Informacja: Metoda 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 link() jest unlink(), która usuwa istniejący związek pomiędzy dwoma instancjami Active Record. Przykładowo:

$customer = Customer::find()->with('orders')->where(['id' => 123])->one();
$customer->unlink('orders', $customer->orders[0]);

Domyślnie metoda unlink() ustawia wartość klucza obcego (lub wielu kluczy obcych), 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 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:

// 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::class, ['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::class, ['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.

Uwaga: Użycie 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 ActiveQuery. Aby użyć niestandardowej klasy kwerend razem z klasą Active Record, należy nadpisać metodę find(), aby zwracała instancję żądanej klasy kwerend. Przykład:

// plik Comment.php
namespace app\models;

use yii\db\ActiveRecord;

class Comment extends ActiveRecord
{
    public static function find()
    {
        return new CommentQuery(get_called_class());
    }
}

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.

Teraz należy zdefiniować klasę CommentQuery, którą można dopasować do własnych kreatywnych potrzeb, dzięki czemu budowanie zapytań bazodanowych będzie o wiele bardziej ułatwione. Dla przykładu,

// plik CommentQuery.php
namespace app\models;

use yii\db\ActiveQuery;

class CommentQuery extends ActiveQuery
{
    // dodatkowe warunki relacyjnej kwerendy dołączone jako domyślne (ten krok można pominąć)
    public function init()
    {
        $this->andOnCondition(['deleted' => false]);
        parent::init();
    }

    // ... dodaj zmodyfikowane metody kwerend w tym miejscu ...

    public function active($state = true)
    {
        return $this->andOnCondition(['active' => $state]);
    }
}

Uwaga: Zwykle, zamiast wywoływać metodę onCondition(), powinno się używać metody andOnCondition() lub orOnCondition(), 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:

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

Wskazówka: 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 także użyć nowych metod budowania kwerend przy definiowaniu relacji z Comment lub wykonywaniu relacyjnych kwerend:

class Customer extends \yii\db\ActiveRecord
{
    public function getActiveComments()
    {
        return $this->hasMany(Comment::class, ['customer_id' => 'id'])->active();
    }
}

$customers = Customer::find()->joinWith('activeComments')->all();

// lub alternatywnie
class Customer extends \yii\db\ActiveRecord
{
    public function getComments()
    {
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

$customers = Customer::find()->joinWith([
    'comments' => function($q) {
        $q->active();
    }
])->all();

Informacja: 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:

class Room extends \yii\db\ActiveRecord
{
    public $volume;

    // ...
}

Następnie należy skonstruować kwerendę, która obliczy kubaturę i wykona sortowanie:

$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ń:

class Customer extends \yii\db\ActiveRecord
{
    public $ordersCount;

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

Teraz już możesz skonstruować kwerendę, która przyłączy zamówienia i policzy ich liczbę:

$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();

Wadą tej metody jest to, że jeśli informacja nie może zostać pobrana za pomocą kwerendy SQL, musi ona być obliczona oddzielnie. Zatem po pobraniu konkretnego wiersza tabeli za pomocą regularnej kwerendy bez dodatkowej instrukcji select, niemożliwym będzie zwrócenie wartości dla dodatkowych pól. Tak samo stanie się w przypadku świeżo zapisanych rekordów.

$room = new Room();
$room->length = 100;
$room->width = 50;
$room->height = 2;

$room->volume; // ta wartość będzie wynosić `null` ponieważ nie została jeszcze zadeklarowana

Używając magicznych metod __get() i __set(), możemy emulować zachowania właściwości:

class Room extends \yii\db\ActiveRecord
{
    private $_volume;
    
    public function setVolume($volume)
    {
        $this->_volume = (float) $volume;
    }
    
    public function getVolume()
    {
        if (empty($this->length) || empty($this->width) || empty($this->height)) {
            return null;
        }
        
        if ($this->_volume === null) {
            $this->setVolume(
                $this->length * $this->width * $this->height
            );
        }
        
        return $this->_volume;
    }

    // ...
}

Kiedy kwerenda nie zapewni wartości kubatury, model będzie w stanie automatycznie ją obliczyć, używając swoich atrybutów.

Możesz obliczyć sumaryczne pola rónież korzystając ze zdefiniowanych relacji:

class Customer extends \yii\db\ActiveRecord
{
    private $_ordersCount;
    
    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }
    
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // dzięki temu unikamy wywołania kwerendy szukającej głównych kluczy o wartości null
        }
        
        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // oblicz sumę na żądanie z relacji
        }

        return $this->_ordersCount;
    }

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

Dla powyższego kodu, kiedy 'ordersCount' występuje w instrukcji 'select' - Customer::ordersCount zostanie wypełnione rezultatem kwerendy, w pozostałych przypadkach zostanie obliczone na żądanie używając relacji Customer::orders.

Takie podejście może być równie dobrze użyte do stworzenia skrótów dla niektórych danych relacji, zwłąszcza tych służących do obliczania sumarycznego. Przykładowo:

class Customer extends \yii\db\ActiveRecord
{
    /**
     * Deklaracja wirtualnej właściwości tylko do odczytu dla danych sumarycznych.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // to pozwala na uniknięcie uruchamiania wyszukującej kwerendy dla pustych kluczy głównych
        }
        
        return empty($this->ordersAggregation) ? 0 : $this->ordersAggregation[0]['counted'];
    }

    /**
     * Deklaracja zwykłej relacji 'orders'.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    /**
     * Deklaracja nowej relacji bazującej na 'orders', ale zapewniającej pobranie danych sumarycznych.
     */
    public function getOrdersAggregation()
    {
        return $this->getOrders()
            ->select(['customer_id', 'counted' => 'count(*)'])
            ->groupBy('customer_id')
            ->asArray(true);
    }

    // ...
}

foreach (Customer::find()->with('ordersAggregation')->all() as $customer) {
    echo $customer->ordersCount; // wyświetla dane sumaryczne z relacji bez dodatkowej kwerendy dzięki gorliwemu pobieraniu
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // wyświetla dane sumaryczne z relacji pobranej leniwie

Found a typo or you think this page needs improvement?
Edit it on github !