You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1368 lines
60 KiB
1368 lines
60 KiB
9 years ago
|
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 <span id="declaring-ar-classes"></span>
|
||
|
|
||
|
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 <span id="db-connection"></span>
|
||
|
|
||
|
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 <span id="querying-data"></span>
|
||
|
|
||
|
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 <span id="accessing-data"></span>
|
||
|
|
||
|
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 <span id="data-transformation"></span>
|
||
|
|
||
|
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 <span id="data-in-arrays"></span>
|
||
|
|
||
|
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 <span id="data-in-batches"></span>
|
||
|
|
||
|
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 <span id="inserting-updating-data"></span>
|
||
|
|
||
|
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 <span id="data-validation"></span>
|
||
|
|
||
|
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 <span id="massive-assignment"></span>
|
||
|
|
||
|
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 <span id="updating-counters"></span>
|
||
|
|
||
|
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 <span id="dirty-attributes"></span>
|
||
|
|
||
|
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 <span id="default-attribute-values"></span>
|
||
|
|
||
|
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 <span id="updating-multiple-rows"></span>
|
||
|
|
||
|
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 <span id="deleting-data"></span>
|
||
|
|
||
|
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 <span id="ar-life-cycles"></span>
|
||
|
|
||
|
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 <span id="new-instance-life-cycle"></span>
|
||
|
|
||
|
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 <span id="querying-data-life-cycle"></span>
|
||
|
|
||
|
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 <span id="saving-data-life-cycle"></span>
|
||
|
|
||
|
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 <span id="deleting-data-life-cycle"></span>
|
||
|
|
||
|
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 <span id="transactional-operations"></span>
|
||
|
|
||
|
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 <span id="optimistic-locks"></span>
|
||
|
|
||
|
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 <span id="relational-data"></span>
|
||
|
|
||
|
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 <span id="declaring-relations"></span>
|
||
|
|
||
|
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 <span id="accessing-relational-data"></span>
|
||
|
|
||
|
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 <span id="dynamic-relational-query"></span>
|
||
|
|
||
|
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 <span id="junction-table"></span>
|
||
|
|
||
|
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 <span id="lazy-eager-loading"></span>
|
||
|
|
||
|
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 <span id="joining-with-relations"></span>
|
||
|
|
||
|
> 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 <span id="inverse-relations"></span>
|
||
|
|
||
|
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 <span id="saving-relations"></span>
|
||
|
|
||
|
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 <span id="cross-database-relations"></span>
|
||
|
|
||
|
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 <span id="customizing-query-classes"></span>
|
||
|
|
||
|
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();
|
||
|
```
|