Yii2 activerecord save error

Previously, I was not using $model->save() function for inserting or updating any data. I was simply using createCommand() to execute query and it was working successfully. But, my team members ...

Previously, I was not using $model->save() function for inserting or updating any data. I was simply using createCommand() to execute query and it was working successfully. But, my team members asked me to avoid createCommand() and use $model->save();

Now, I started cleaning my code and problem is $model->save(); not working for me. I don’t know where i did mistake.

UsersController.php (Controller)

<?php
namespace appmodulesuserscontrollers;
use Yii;
use yiiwebNotFoundHttpException;
use yiifiltersVerbFilter;
use yiiswiftmailerMailer;
use yiifiltersAccessControl;
use yiiwebResponse;
use yiiwidgetsActiveForm;
use appmodulesusersmodelsUsers;
use appcontrollersCommonController;

class UsersController extends CommonController 
{
    .
    .

    public function actionRegister() {
    $model = new Users();

        // For Ajax Email Exist Validation
   if(Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())){
     Yii::$app->response->format = Response::FORMAT_JSON;
     return ActiveForm::validate($model);
   } 

   else if ($model->load(Yii::$app->request->post())) {
      $post = Yii::$app->request->post('Users');
      $CheckExistingUser = $model->findOne(['email' => $post['email']]);

      // Ok. Email Doesn't Exist
      if(!$CheckExistingUser) {

        $auth_key = $model->getConfirmationLink();
        $password = md5($post['password']);
        $registration_ip = Yii::$app->getRequest()->getUserIP();
        $created_at = date('Y-m-d h:i:s');

        $model->auth_key = $auth_key;
        $model->password = $password;
        $model->registration_ip = $registration_ip;
        $model->created_at = $created_at;

        if($model->save()) {
          print_r("asd");
        }

      }

    } 
    }
    .
    .
}

Everything OK in this except $model->save(); Not printing ‘asd’ as i echoed it.

And, if i write

else if ($model->load(Yii::$app->request->post() && $model->validate()) {

}

It’s not entering to this if condition.

And, if i write

if($model->save(false)) {
    print_r("asd");
}

It insert NULL to all columns and print ‘asd’

Users.php (model)

<?php

namespace appmodulesusersmodels;

use Yii;
use yiibaseModel;
use yiidbActiveRecord;
use yiihelpersSecurity;
use yiiwebIdentityInterface;
use appmodulesusersmodelsUserType;

class Users extends ActiveRecord implements IdentityInterface 
{

  public $id;
  public $first_name;
  public $last_name;
  public $email;
  public $password;
  public $rememberMe;
  public $confirm_password;
  public $user_type;
  public $company_name;
  public $status;
  public $auth_key;
  public $confirmed_at;
  public $registration_ip;
  public $verify_code;
  public $created_at;
  public $updated_at;
  public $_user = false;

  public static function tableName() {
    return 'users';
  }

  public function rules() {
    return [
      //First Name
      'FirstNameLength' => ['first_name', 'string', 'min' => 3, 'max' => 255],
      'FirstNameTrim' => ['first_name', 'filter', 'filter' => 'trim'],
      'FirstNameRequired' => ['first_name', 'required'],
      //Last Name
      'LastNameLength' => ['last_name', 'string', 'min' => 3, 'max' => 255],
      'LastNameTrim' => ['last_name', 'filter', 'filter' => 'trim'],
      'LastNameRequired' => ['last_name', 'required'],
      //Email ID
      'emailTrim' => ['email', 'filter', 'filter' => 'trim'],
      'emailRequired' => ['email', 'required'],
      'emailPattern' => ['email', 'email'],
      'emailUnique' => ['email', 'unique', 'message' => 'Email already exists!'],
      //Password
      'passwordRequired' => ['password', 'required'],
      'passwordLength' => ['password', 'string', 'min' => 6],
      //Confirm Password
      'ConfirmPasswordRequired' => ['confirm_password', 'required'],
      'ConfirmPasswordLength' => ['confirm_password', 'string', 'min' => 6],
      ['confirm_password', 'compare', 'compareAttribute' => 'password'],
      //Admin Type
      ['user_type', 'required'],
      //company_name
      ['company_name', 'required', 'when' => function($model) {
          return ($model->user_type == 2 ? true : false);
        }, 'whenClient' => "function (attribute, value) {
          return $('input[type="radio"][name="Users[user_type]"]:checked').val() == 2;
      }"], #'enableClientValidation' => false
      //Captcha
      ['verify_code', 'captcha'],

      [['auth_key','registration_ip','created_at'],'safe'] 
    ];
  }

  public function attributeLabels() {
    return [
      'id' => 'ID',
      'first_name' => 'First Name',
      'last_name' => 'Last Name',
      'email' => 'Email',
      'password' => 'Password',
      'user_type' => 'User Type',
      'company_name' => 'Company Name',
      'status' => 'Status',
      'auth_key' => 'Auth Key',
      'confirmed_at' => 'Confirmed At',
      'registration_ip' => 'Registration Ip',
      'confirm_id' => 'Confirm ID',
      'created_at' => 'Created At',
      'updated_at' => 'Updated At',
      'verify_code' => 'Verification Code',
    ];
  }

  //custom methods
  public static function findIdentity($id) {
    return static::findOne($id);
  }

  public static function instantiate($row) {
    return new static($row);
  }

  public static function findIdentityByAccessToken($token, $type = null) {
    throw new NotSupportedException('Method "' . __CLASS__ . '::' . __METHOD__ . '" is not implemented.');
  }

  public function getId() {
    return $this->id;
  }

  public function getAuthKey() {
    return $this->auth_key;
  }

  public function validateAuthKey($authKey) {
    return $this->auth_key === $auth_key;
  }

  public function validatePassword($password) {
    return $this->password === $password;
  }

  public function getFirstName() {
    return $this->first_name;
  }

  public function getLastName() {
    return $this->last_name;
  }

  public function getEmail() {
    return $this->email;
  }

  public function getCompanyName() {
    return $this->company_name;
  }

  public function getUserType() {
    return $this->user_type;
  }

  public function getStatus() {
    return $this->status;
  }

  public function getUserTypeValue() {
    $UserType = $this->user_type;
    $UserTypeValue = UserType::find()->select(['type'])->where(['id' => $UserType])->one();
    return $UserTypeValue['type'];
  }

  public function getCreatedAtDate() {
    $CreatedAtDate = $this->created_at;
    $CreatedAtDate = date('d-m-Y h:i:s A', strtotime($CreatedAtDate));
    return $CreatedAtDate;
  }

  public function getLastUpdatedDate() {
    $UpdatedDate = $this->updated_at;
    if ($UpdatedDate != 0) {
      $UpdatedDate = date('d-m-Y h:i:s A', strtotime($UpdatedDate));
      return $UpdatedDate;
    } else {
      return '';
    }
  }

  public function register() {
    if ($this->validate()) {
      return true;
    }
    return false;
  }

  public static function findByEmailAndPassword($email, $password) {
    $password = md5($password);
    $model = Yii::$app->db->createCommand("SELECT * FROM users WHERE email ='{$email}' AND password='{$password}' AND status=1");
    $users = $model->queryOne();
    if (!empty($users)) {
      return new Users($users);
    } else {
      return false;
    }
  }

  public static function getConfirmationLink() {
    $characters = 'abcedefghijklmnopqrstuvwxyzzyxwvutsrqponmlk';
    $confirmLinkID = '';
    for ($i = 0; $i < 10; $i++) {
      $confirmLinkID .= $characters[rand(0, strlen($characters) - 1)];
    }
    return $confirmLinkID = md5($confirmLinkID);
  }

}

Any help is appreciable. Please Help me.

Active Record обеспечивает объектно-ориентированный интерфейс для доступа
и манипулирования данными, хранящимися в базах данных. Класс Active Record соответствует таблице в базе данных, объект
Active Record соответствует строке этой таблицы, а атрибут объекта Active Record представляет собой значение
отдельного столбца строки. Вместо непосредственного написания SQL-выражений вы сможете получать доступ к атрибутам
Active Record и вызывать методы Active Record для доступа и манипулирования данными, хранящимися в таблицах базы данных.

Для примера предположим, что Customer — это класс Active Record, который сопоставлен с таблицей customer, а name
столбец в таблице customer. Тогда вы можете написать следующий код для вставки новой строки в таблицу customer:

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

Вышеприведённый код аналогичен использованию следующего SQL-выражения в MySQL, которое менее интуитивно, потенциально
может вызвать ошибки и даже проблемы совместимости, если вы используете различные виды баз данных:

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii поддерживает работу с Active Record для следующих реляционных баз данных:

  • MySQL 4.1 и выше: посредством yiidbActiveRecord
  • PostgreSQL 7.3 и выше: посредством yiidbActiveRecord
  • SQLite 2 и 3: посредством yiidbActiveRecord
  • Microsoft SQL Server 2008 и выше: посредством yiidbActiveRecord
  • Oracle: посредством yiidbActiveRecord
  • CUBRID 9.3 и выше: посредством yiidbActiveRecord (Имейте в виду, что вследствие
    бага в PDO-расширении для CUBRID, заключение значений в кавычки не работает,
    поэтому необходимо использовать CUBRID версии 9.3 как на клиентской стороне, так и на сервере)
  • Sphinx: посредством yiisphinxActiveRecord, потребуется расширение yii2-sphinx
  • ElasticSearch: посредством yiielasticsearchActiveRecord, потребуется расширение yii2-elasticsearch

Кроме того Yii поддерживает использование Active Record со следующими NoSQL базами данных:

  • Redis 2.6.12 и выше: посредством yiiredisActiveRecord, потребуется расширение yii2-redis
  • MongoDB 1.3.0 и выше: посредством yiimongodbActiveRecord, потребуется расширение yii2-mongodb

В этом руководстве мы в основном будем описывать использование Active Record для реляционных баз данных. Однако большая
часть этого материала также применима при использовании Active Record с NoSQL базами данных.

Объявление классов Active Record ¶

Для начала объявите свой собственный класс, унаследовав класс yiidbActiveRecord.

Настройка имени таблицы ¶

По умолчанию каждый класс Active Record ассоциирован с таблицей в базе данных. Метод
tableName() получает имя таблицы из имени класса с помощью yiihelpersInflector::camel2id().
Если таблица не названа соответственно, вы можете переопределить данный метод.

Также может быть применён tablePrefix по умолчанию. Например, если
tablePrefix задан как tbl_, Customer преобразуется в tbl_customer, а
OrderItem в tbl_order_item.

Если имя таблицы указано в формате {{%TableName}}, символ % заменяется префиксом. Например {{%post}} становится
{{tbl_post}}. Фигуриные скобки используются для экранирования в SQL-запросах.

В нижеследующем примере мы объявляем класс Active Record с названием Customer для таблицы customer.

namespace appmodels;

use yiidbActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string название таблицы, сопоставленной с этим ActiveRecord-классом.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Классы Active record называются «моделями» ¶

Объекты Active Record являются моделями. Именно поэтому мы обычно задаём классам Active Record
пространство имён appmodels (или другое пространство имён, предназначенное для моделей).

Т.к. класс yiidbActiveRecord наследует класс yiibaseModel, он обладает всеми возможностями
моделей, такими как атрибуты, правила валидации, способы сериализации данных и т.д.

Подключение к базам данных ¶

По умолчанию Active Record для доступа и манипулирования данными БД использует
компонент приложения db в качестве компонента
DB connection. Как сказано в разделе Объекты доступа к данным (DAO), вы можете
настраивать компонент db на уровне конфигурации приложения как показано ниже:

return [
    'components' => [
        'db' => [
            'class' => 'yiidbConnection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

Если вы хотите использовать для подключения к базе данных другой компонент подключения, отличный от db, вам нужно
переопределить метод getDb():

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // использовать компонент приложения "db2"
        return Yii::$app->db2;  
    }
}

Получение данных ¶

После объявления класса Active Record вы можете использовать его для получения данных из соответствующей таблицы базы
данных. Этот процесс, как правило, состоит из следующих трёх шагов:

  1. Создать новый объект запроса вызовом метода yiidbActiveRecord::find();
  2. Настроить объект запроса вызовом методов построения запросов;
  3. Вызвать один из методов получения данных для извлечения данных в виде объектов
    Active Record.

Как вы могли заметить, эти шаги очень похожи на работу с построителем запросов. Различие лишь в
том, что для создания объекта запроса вместо оператора new используется метод yiidbActiveRecord::find(),
возвращающий новый объект запроса, являющийся представителем класса yiidbActiveQuery.

Ниже приведено несколько примеров использования Active Query для получения данных:

// возвращает покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// возвращает всех активных покупателей, сортируя их по идентификаторам
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// возвращает количество активных покупателей
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// возвращает всех покупателей массивом, индексированным их идентификаторами
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

В примерах выше $customer — это объект класса Customer, в то время как $customers — это массив таких объектов. Все
эти объекты заполнены данными таблицы customer.

Информация: Т.к. класс yiidbActiveQuery наследует yiidbQuery, вы можете использовать в нём все методы
построения запросов и все методы класса Query как описано в разделе Построитель запросов.

Т.к. извлечение данных по первичному ключу или значениям отдельных столбцов достаточно распространённая задача, Yii
предоставляет два коротких метода для её решения:

  • yiidbActiveRecord::findOne(): возвращает один объект Active Record, заполненный первой строкой результата запроса.
  • yiidbActiveRecord::findAll(): возвращает массив объектов Active Record, заполненных всеми полученными результатами запроса.

Оба метода могут принимать параметры в одном из следующих форматов:

  • скалярное значение: значение интерпретируется как первичный ключ, по которому следует искать. Yii прочитает
    информацию о структуре базы данных и автоматически определит, какой столбец таблицы содержит первичные ключи.
  • массив скалярных значений: массив интерпретируется как набор первичных ключей, по которым следует искать.
  • ассоциативный массив: ключи массива интерпретируются как названия столбцов, а значения — как содержимое столбцов,
    которое следует искать. За подробностями вы можете обратиться к разделу Hash Format

Нижеследующий код демонстрирует, каким образом эти методы могут быть использованы:

// возвращает покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// возвращает покупателей с идентификаторами 100, 101, 123 и 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// возвращает активного покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// возвращает всех неактивных покупателей
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

Внимание: Если вам нужно передать в эти методы данные, полученные от пользователя, убедитесь что передаваемое значение – это скаляр,
а если необходимо указать условия в формате массива – убедитесь, что пользовательские данные не могут изменить структуру этого массива.

// yiiwebController гарантирует, что $id будет скаляром
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// явное указание имени столбца для поиска гарантирует поиск по столбцу `id`,
// и возвращение одной записи как для массива, так и для скаляра в принятом от пользователя поле `id` 
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// НЕ используйте этот код! Пользователь может передать в параметр `id` массив
// и осуществить поиск по имени столбца, которое не должно быть использовано для поиска по логике вашего приложения.
$model = Post::findOne(Yii::$app->request->get('id'));

Примечание: Ни метод yiidbActiveRecord::findOne(), ни yiidbActiveQuery::one() не добавляет условие LIMIT 1 к
генерируемым SQL-запросам. Если ваш запрос может вернуть много строк данных, вы должны вызвать метод limit(1) явно
в целях улучшения производительности, например: Customer::find()->limit(1)->one().

Помимо использования методов построения запросов вы можете также писать запросы на «чистом» SQL для получения данных и
заполнения ими объектов Active Record. Вы можете делать это посредством метода yiidbActiveRecord::findBySql():

// возвращает всех неактивных покупателей
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Не используйте дополнительные методы построения запросов после вызова метода
findBySql(), т.к. они будут проигнорированы.

Доступ к данным ¶

Как сказано выше, получаемые из базы данные заполняют объекты Active Record и каждая строка результата запроса
соответствует одному объекту Active Record. Вы можете получить доступ к значениям столбцов с помощью атрибутов этих
объектов. Например так:

// "id" и "email" - названия столбцов в таблице "customer"
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Примечание: Атрибуты объекта Active Record названы в соответствии с названиями столбцов связной таблицы с учётом
регистра. Yii автоматически объявляет для каждого столбца связной таблицы атрибут в Active Record. Вы НЕ должны
переопределять какие-либо из этих атрибутов.

Атрибуты Active Record названы в соответствии с именами столбцов таблицы. Если столбцы вашей таблицы именуются через
нижнее подчёркивание, то может оказаться, что вам придётся писать PHP-код вроде этого: $customer->first_name — в нём
будет использоваться нижнее подчёркивание для разделения слов в названиях атрибутов. Если вы обеспокоены единообразием
стиля кодирования, вам придётся переименовать столбцы вашей таблицы соответствующим образом (например, назвать столбцы
в стиле camelCase).

Преобразование данных ¶

Часто бывает так, что данные вводятся и/или отображаются в формате, который отличается от формата их хранения в базе
данных. Например, в базе данных вы храните дни рождения покупателей в формате UNIX timestamp (что, кстати говоря, не
является хорошим дизайном), в то время как во многих случаях вы хотите манипулировать днями рождения в виде строк
формата 'ДД.ММ.ГГГГ'. Для достижения этой цели, вы можете объявить методы преобразования данных в
ActiveRecord-классе Customer как показано ниже:

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('d.m.Y', $this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

Теперь в своём PHP коде вместо доступа к $customer->birthday, вы сможете получить доступ к $customer->birthdayText,
что позволить вам вводить и отображать дни рождения покупателей в формате 'ДД.ММ.ГГГГ'.

Подсказка: Вышеприведённый пример демонстрирует общий способ преобразования данных в различные форматы. Если вы
работаете с датами и временем, вы можете использовать DateValidator и
yiijuiDatePicker, которые проще в использовании и являются более мощными инструментами.

Получение данных в виде массива ¶

Несмотря на то, что получение данных в виде Active Record объектов является удобным и гибким, этот способ не всегда
подходит при получении большого количества данных из-за больших накладных расходов памяти. В этом случае вы можете
получить данные в виде PHP-массива, используя перед выполнением запроса метод
asArray():

// возвращает всех покупателей
// каждый покупатель будет представлен в виде ассоциативного массива
$customers = Customer::find()
    ->asArray()
    ->all();

Примечание: В то время как этот способ бережёт память и улучшает производительность, он ближе к низкому слою
абстракции базы данных и вы потеряете многие возможности Active Record. Важное отличие заключается в типах данных
значений столбцов. Когда вы получаете данные в виде объектов Active Record, значения столбцов автоматически приводятся
к типам, соответствующим типам столбцов; с другой стороны, когда вы получаете данные в массивах, значения столбцов
будут строковыми (до тех пор, пока они являются результатом работы PDO-слоя без какой-либо обработки), несмотря на
настоящие типы данных соответствующих столбцов.

Пакетное получение данных ¶

В главе Построитель запросов мы объясняли, что вы можете использовать пакетную выборку для
снижения расходов памяти при получении большого количества данных из базы. Вы можете использовать такой же подход при
работе с Active Record. Например:

// получить 10 покупателей одновременно
foreach (Customer::find()->batch(10) as $customers) {
    // $customers - это массив, в котором находится 10 или меньше объектов класса Customer
}

// получить одновременно десять покупателей и перебрать их одного за другим
foreach (Customer::find()->each(10) as $customer) {
    // $customer - это объект класса Customer
}

// пакетная выборка с жадной загрузкой
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer - это объект класса Customer
}

Сохранение данных ¶

Используя Active Record, вы легко можете сохранить данные в базу данных, осуществив следующие шаги:

  1. Подготовьте объект Active Record;
  2. Присвойте новые значения атрибутам Active Record;
  3. Вызовите метод yiidbActiveRecord::save() для сохранения данных в базу данных.

Например:

// вставить новую строку данных
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// обновить имеющуюся строку данных
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

Метод save() может вставить или обновить строку данных в зависимости от состояния
Active Record объекта. Если объект создан с помощью оператора new, вызов метода save()
приведёт к вставке новой строки данных; если объект был получен с помощью запроса на получение данных, вызов
save() обновит строку таблицы, соответствующую объекту Active Record.

Вы можете различать два состояния Active Record объекта с помощью проверки значения его свойства
isNewRecord. Это свойство также используется внутри метода
save() как показано ниже:

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

Подсказка: Вы можете вызвать insert() или update()
непосредственно, чтобы вставить или обновить строку данных в таблице.

Валидация данных ¶

Т.к. класс yiidbActiveRecord наследует класс yiibaseModel, он обладает такими же возможностями
валидации данных. Вы можете объявить правила валидации переопределив метод
rules() и осуществлять валидацию данных посредством вызовов метода
validate().

Когда вы вызываете метод save(), по умолчанию он автоматически вызывает метод
validate(). Только после успешного прохождения валидации происходит сохранение
данных; в ином случае метод save() просто возвращает false, и вы можете проверить
свойство errors для получения сообщений об ошибках валидации.

Подсказка: Если вы уверены, что ваши данные не требуют валидации (например, данные пришли из доверенного источника),
вы можете вызвать save(false), чтобы пропустить валидацию.

Массовое присваивание ¶

Как и обычные модели, объекты Active Record тоже обладают
возможностью массового присваивания. Как будет показано ниже, используя эту
возможность, вы можете одним PHP выражением присвоить значения множества атрибутов Active Record объекту. Запомните
однако, что только безопасные атрибуты могут быть массово присвоены.

$values = [
    'name' => 'James',
    'email' => 'james@example.com',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

Обновление счётчиков ¶

Распространённой задачей является инкремент или декремент столбца в таблице базы данных. Назовём такие столбцы
столбцами-счётчиками. Вы можете использовать метод updateCounters() для
обновления одного или нескольких столбцов-счётчиков. Например:

$post = Post::findOne(100);

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

Примечание: Если вы используете метод yiidbActiveRecord::save() для обновления столбца-счётчика, вы можете
прийти к некорректному результату, т.к. вполне вероятно, что этот же счётчик был сохранён сразу несколькими запросами,
которые читают и записывают этот же столбец-счётчик.

Dirty-атрибуты ¶

Когда вы вызываете save() для сохранения Active Record объекта, сохраняются только
dirty-атрибуты. Атрибут считается dirty-атрибутом, если его значение было изменено после чтения из базы данных или
же он был сохранён в базу данных совсем недавно. Заметьте, что валидация данных осуществляется независимо от того,
имеются ли dirty-атрибуты в объекте Active Record или нет.

Active Record автоматически поддерживает список dirty-атрибутов. Это достигается за счёт хранения старых значений
атрибутов и сравнения их с новыми. Вы можете вызвать метод yiidbActiveRecord::getDirtyAttributes() для получения
текущего списка dirty-атрибутов. Вы также можете вызвать yiidbActiveRecord::markAttributeDirty(), чтобы явно
пометить атрибут в качестве dirty-атрибута.

Если вам нужны значения атрибутов, какими они были до их изменения, вы можете вызвать
getOldAttributes() или
getOldAttribute().

Примечание: Сравнение старых и новых значений будет осуществлено с помощью оператора ===, так что значение будет
считаться dirty-значением даже в том случае, если оно осталось таким же, но изменило свой тип. Это часто происходит,
когда модель получает пользовательский ввод из HTML-форм, где каждое значение представлено строкой. Чтобы убедиться в
корректности типа данных, например для целых значений, вы можете применить
фильтрацию данных: ['attributeName', 'filter', 'filter' => 'intval'].

Значения атрибутов по умолчанию ¶

Некоторые столбцы ваших таблиц могут иметь значения по умолчанию, объявленные в базе данных. Иногда вы можете захотеть
предварительно заполнить этими значениями вашу веб-форму, которая соответствует Active Record объекту. Чтобы избежать
повторного указания этих значений, вы можете вызвать метод
loadDefaultValues() для заполнения соответствующих Active Record атрибутов
значениями по умолчанию, объявленными в базе данных:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz получит значение по умолчанию, которое было указано при объявлении столбца "xyz"

Приведение типов атрибутов ¶

При заполнении результатами запроса yiidbActiveRecord производит автоматическое приведение типов для значений
атрибутов на основе информации из схемы базы данны. Это позволяет данным, полученным из
колонки таблицы объявленной как целое, заноситься в экземпляр ActiveRecord как значение целого типа PHP, булево как
булево и т.д.
Однако, механизм приведения типов имеет несколько ограничений:

  • Числа с плавающей точкой не будут обработаны, а будут представленны как строки, в противном случае они могут потерять точность.
  • Конвертация целых чисел зависит от разрядности используемой операционной системы. В частности: значения колонок, объявленных
    как ‘unsigned integer’ или ‘big integer’ будут приведены к целому типу PHP только на 64-х разрядных системах, в то время
    как на 32-х разрядных — они будут представленны как строки.

Имейте в виду, что преобразование типов производиться только в момент заполнения экземпляра ActiveRecord данными из результата
запроса. При заполнении данных из HTTP запроса или непосредственно через механизм доступа к полям — автоматическая конвертация
не производтся.
Схема таблицы базы данных также используется при построении SQL запроса для сохранения данных ActiveRecord, обеспечивая
соответсвие типов связываемых параметров в запросе. Однако, над атрибутами объекта ActiveRecord не будет производиться
приведение типов в процессе сохранения.

Совет: вы можете использовать поведение yiibehaviorsAttributeTypecastBehavior для того, чтобы производить
приведение типов для ActiveRecord во время валидации или сохранения.

Начиная с 2.0.14, Yii ActiveRecord поддерживает сложные типы данных, такие как JSON или многомерные массивы.

JSON в MySQL и PostgreSQL ¶

После заполнения данных, значение из столбца JSON будет автоматически декодировано из JSON в соответствии со стандартными правилами декодирования JSON.

Чтобы сохранить значение атрибута в столбец JSON, ActiveRecord автоматически создаст объект JsonExpression, который будет закодирован в строку JSON на уровне QueryBuilder.

Массивы в PostgreSQL ¶

После заполнения данных значение из столбца Array будет автоматически декодировано из нотации PgSQL в объект ArrayExpression. Он реализует интерфейс PHP ArrayAccess, так что вы можете использовать его в качестве массива, или вызвать ->getValue (), чтобы получить сам массив.

Чтобы сохранить значение атрибута в столбец массива, ActiveRecord автоматически создаст объект [[yiidbArray Expression|ArrayExpression]], который будет закодирован QueryBuilder в строковое представление массива PgSQL.

Можно также использовать условия для столбцов JSON:

$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])

Дополнительные сведения о системе построения выражений см. Query Builder – добавление пользовательских условий и выражений

Обновление нескольких строк данных ¶

Методы, представленные выше, работают с отдельными Active Record объектами, инициируя вставку или обновление данных для
отдельной строки таблицы. Вместо них для обновления нескольких строк одновременно можно использовать метод
updateAll(), который является статическим.

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

Подобным образом можно использовать метод updateAllCounters() для
обновления значений столбцов-счётчиков в нескольких строках одновременно.

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

Удаление данных ¶

Для удаления одной отдельной строки данных сначала получите Active Record объект, соответствующий этой строке, а затем
вызовите метод yiidbActiveRecord::delete().

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

Вы можете вызвать yiidbActiveRecord::deleteAll() для удаления всех или нескольких строк данных одновременно.
Например:

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

Примечание: будьте очень осторожны, используя метод deleteAll(), потому что он
может полностью удалить все данные из вашей таблицы, если вы сделаете ошибку при указании условий удаления.

Жизненные циклы Active Record ¶

Важно понимать как устроены жизненные циклы Active Record при использовании Active Record для различных целей.
В течение каждого жизненного цикла вызывается определённая последовательность методов, которые вы можете переопределять,
чтобы получить возможность тонкой настройки жизненного цикла. Для встраивания своего кода вы также можете отвечать на
конкретные события Active Record, которые срабатывают в течение жизненного цикла. Эти события особенно полезны, когда
вы разрабатываете поведения, которые требуют тонкой настройки жизненных циклов Active Record.

Ниже мы подробно опишем различные жизненные циклы Active Record и методы/события, которые участвуют в жизненных циклах.

Жизненный цикл создания нового объекта ¶

Когда создаётся новый объект Active Record с помощью оператора new, следующий жизненный цикл имеет место:

  1. Вызывается конструктор класса;
  2. Вызывается init():
    инициируется событие EVENT_INIT.

Жизненный цикл получения данных ¶

Когда происходит получение данных посредством одного из методов получения данных, каждый вновь
создаваемый объект Active Record при заполнении данными проходит следующий жизненный цикл:

  1. Вызывается конструктор класса.
  2. Вызывается init(): инициируется событие
    EVENT_INIT.
  3. Вызывается afterFind(): инициируется событие
    EVENT_AFTER_FIND.

Жизненный цикл сохранения данных ¶

Когда вызывается метод save() для вставки или обновления объекта Active Record,
следующий жизненный цикл имеет место:

  1. Вызывается beforeValidate(): инициируется событие
    EVENT_BEFORE_VALIDATE. Если метод возвращает false или свойство
    события yiibaseModelEvent::$isValid равно false, оставшиеся шаги не выполняются.
  2. Осуществляется валидация данных. Если валидация закончилась неудачей, после 3-го шага остальные шаги не выполняются.
  3. Вызывается afterValidate(): инициируется событие
    EVENT_AFTER_VALIDATE.
  4. Вызывается beforeSave(): инициируется событие
    EVENT_BEFORE_INSERT или событие
    EVENT_BEFORE_UPDATE. Если метод возвращает false или свойство события
    yiibaseModelEvent::$isValid равно false, оставшиеся шаги не выполняются.
  5. Осуществляется фактическая вставка или обновление данных в базу данных;
  6. Вызывается afterSave(): инициируется событие
    EVENT_AFTER_INSERT или событие
    EVENT_AFTER_UPDATE.

Жизненный цикл удаления данных ¶

Когда вызывается метод delete() для удаления объекта Active Record, следующий
жизненный цикл имеет место:

  1. Вызывается beforeDelete(): инициируется событие
    EVENT_BEFORE_DELETE. Если метод возвращает false или свойство события
    yiibaseModelEvent::$isValid равно false, остальные шаги не выполняются.
  2. Осуществляется фактическое удаление данных из базы данных.
  3. Вызывается afterDelete(): инициируется событие
    EVENT_AFTER_DELETE.

Примечание: Вызов следующих методов НЕ инициирует ни один из вышеприведённых жизненных циклов:

  • yiidbActiveRecord::updateAll()
  • yiidbActiveRecord::deleteAll()
  • yiidbActiveRecord::updateCounters()
  • yiidbActiveRecord::updateAllCounters()

Работа с транзакциями ¶

Есть два способа использования транзакций при работе с Active Record.

Первый способ заключается в том, чтобы явно заключить все вызовы методов Active Record в блок транзакции как показано
ниже:

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...другие операции с базой данных...
});

// или по-другому

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...другие операции с базой данных...
    $transaction->commit();
} catch(Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

Примечание: в коде выше ради совместимости с PHP 5.x и PHP 7.x использованы два блока catch.
Exception реализует интерфейс Throwable interface
начиная с PHP 7.0. Если вы используете только PHP 7 и новее, можете пропустить блок с Exception.

Второй способ заключается в том, чтобы перечислить операции с базой данных, которые требуют тразнакционного выполнения,
в методе yiidbActiveRecord::transactions(). Например:

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // вышеприведённая строка эквивалентна следующей:
            // 'api' => self::OP_ALL,
        ];
    }
}

Метод yiidbActiveRecord::transactions() должен возвращать массив, ключи которого являются именами
сценариев, а значения соответствуют операциям, которые должны быть выполнены с помощью
транзакций. Вы должны использовать следующие константы для обозначения различных операций базы данных:

  • OP_INSERT: операция вставки, осуществляемая с помощью метода
    insert();
  • OP_UPDATE: операция обновления, осуществляемая с помощью метода
    update();
  • OP_DELETE: операция удаления, осуществляемая с помощью метода
    delete().

Используйте операторы | для объединения вышеприведённых констант при обозначении множества операций. Вы можете также
использовать вспомогательную константу OP_ALL, чтобы обозначить одной константой все три
вышеприведённые операции.

Оптимистическая блокировка ¶

Оптимистическая блокировка — это способ предотвращения конфликтов, которые могут возникать, когда одна и та же строка
данных обновляется несколькими пользователями. Например, пользователь A и пользователь B одновременно редактируют одну и
ту же wiki-статью. После того, как пользователь A сохранит свои изменения, пользователь B нажимает на кнопку «Сохранить»
в попытке также сохранить свои изменения. Т.к. пользователь B работал с фактически-устаревшей версией статьи, было бы
неплохо иметь способ предотвратить сохранение его варианта статьи и показать ему некоторое сообщение с подсказкой о том,
что произошло.

Оптимистическая блокировка решает вышеприведённую проблему за счёт использования отдельного столбца для сохранения
номера версии каждой строки данных. Когда строка данных сохраняется с использованием устаревшего номера версии,
выбрасывается исключение yiidbStaleObjectException, которое предохраняет строку от сохранения. Оптимистическая
блокировка поддерживается только тогда, когда вы обновляете или удаляете существующую строку данных, используя методы
yiidbActiveRecord::update() или yiidbActiveRecord::delete() соответственно.

Для использования оптимистической блокировки:

  1. Создайте столбец в таблице базы данных, ассоциированной с классом Active Record, для сохранения номера версии каждой
    строки данных. Столбец должен быть типа big integer (в Mysql это будет BIGINT DEFAULT 0).
  2. Переопределите метод yiidbActiveRecord::optimisticLock() таким образом, чтобы он возвращал название этого
    столбца.
  3. В веб-форме, которая принимает пользовательский ввод, добавьте скрытое поле для сохранения текущей версии обновляемой
    строки. Убедитесь, что для вашего атрибута с версией объявлены правила валидации, и валидация проходит успешно.
  4. В действии контроллера, которое занимается обновлением строки данных с использованием Active Record, оберните в блок
    try…catch код и перехватывайте исключение yiidbStaleObjectException. Реализуйте необходимую бизнес-логику
    (например, возможность слияния изменений, подсказку о том, что данные устарели) для разрешения возникшего конфликта.

Например, предположим, что столбец с версией называется version. Вы можете реализовать оптимистическую блокировку с
помощью подобного кода:

// ------ код представления -------

use yiihelpersHtml;

// ...другие поля ввода
echo Html::activeHiddenInput($model, 'version');


// ------ код контроллера -------

use yiidbStaleObjectException;

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) {
        // логика разрешения конфликта версий
    }
}

Работа со связными данными ¶

Помимо работы с отдельными таблицами баз данных, Active Record также имеет возможность объединять связные данные, что
делает их легко-доступными для получения через основные объекты данных. Например, данные покупателя связаны с данными
заказов, потому что один покупатель может осуществить один или несколько заказов. С помощью объявления этой связи вы
можете получить возможность доступа к информации о заказе покупателя с помощью выражения $customer->orders, которое
возвращает информацию о заказе покупателя в виде массива объектов класса Order, которые являются Active Record
объектами.

Объявление связей ¶

Для работы со связными данными посредством Active Record вы прежде всего должны объявить связи в классе Active Record.
Эта задача решается простым объявлением методов получения связных данных для каждой интересующей вас связи как
показано ниже:

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

В вышеприведённом коде мы объявили связь orders для класса Customer и связь customer для класса Order.

Каждый метод получения связных данных должен быть назван в формате getXyz. Мы называем xyz (первая буква в нижнем
регистре) именем связи. Помните, что имена связей чувствительны к регистру.

При объявлении связи, вы должны указать следующую информацию:

  • кратность связи: указывается с помощью вызова метода hasMany() или метода
    hasOne(). В вышеприведённом примере вы можете легко увидеть в объявлениях связей,
    что покупатель может иметь много заказов в то время, как заказ может быть сделан лишь одним покупателем.
  • название связного Active Record класса: указывается в качестве первого параметра для метода
    hasMany() или для метода hasOne(). Рекомендуется
    использовать код Xyz::class, чтобы получить строку с именем класса, при этом вы сможете воспользоваться
    возможностями авто-дополнения кода, встроенного в IDE, а также получите обработку ошибок на этапе компиляции.
  • связь между двумя типами данных: указываются столбцы с помощью которых два типа данных связаны. Значения массива — это
    столбцы основного объекта данных (представлен классом Active Record, в котором объявляется связь), в то время как
    ключи массива — столбцы связанных данных.

    Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record
    указывается сразу после указания самого класса Active Record. Вы видите, что customer_id — это свойство класса
    Order, а id — свойство класса Customer.

Внимание: Имя связи relation зарезервировано. Его использование приведёт к ошибке ArgumentCountError.

Доступ к связным данным ¶

После объявления связей вы можете получать доступ к связным данным с помощью имён связей. Это происходит таким же
образом, каким осуществляется доступ к свойству объекта объявленному с помощью метода получения
связных данных. По этой причине, мы называем его свойством связи. Например:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders - это массив объектов Order
$orders = $customer->orders;

Информация: когда вы объявляете связь с названием xyz посредством геттера getXyz(), у вас появляется возможность
доступа к свойству xyz подобно свойству объекта. Помните, что название связи чувствительно
к регистру.

Если связь объявлена с помощью метода hasMany(), доступ к свойству связи вернёт
массив связных объектов Active Record; если связь объявлена с помощью метода hasOne(),
доступ к свойству связи вернёт связный Active Record объект или null, если связные данные не найдены.

Когда вы запрашиваете свойство связи в первый раз, выполняется SQL-выражение как показано в примере выше. Если то же
самое свойство запрашивается вновь, будет возвращён результат предыдущего SQL-запроса без повторного выполнения
SQL-выражения. Для принудительного повторного выполнения SQL-запроса, вы можете удалить свойство связи с помощью
операции: unset($customer->orders).

Примечание: Несмотря на то, что эта концепция выглядит похожей на концепцию свойств объектов,
между ними есть важное различие. Для обычных свойств объектов значения свойств имеют тот же тип, который возвращает
геттер. Однако метод получения связных данных возвращает объект yiidbActiveQuery, в то время как доступ к
свойству связи возвращает объект yiidbActiveRecord или массив таких объектов.
`php
$customer->orders; // массив объектов Order
$customer->getOrders(); // объект ActiveQuery
`
Это полезно при тонкой настройке запросов к связным данным, что будет описано в следующем разделе.

Динамические запросы связных данных ¶

Т.к. метод получения связных данных возвращает объект запроса yiidbActiveQuery, вы можете в дальнейшем перед его
отправкой в базу данных настроить этот запрос, используя методы построения запросов. Например:

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

В отличие от доступа к данным с помощью свойства связи, каждый раз при выполнении такого динамического запроса
посредством метода получения связных данных будет выполняться SQL-запрос, даже если тот же самый динамический запрос был
отправлен ранее.

Иногда вы можете даже захотеть настроить объявление связи таким образом, чтобы вы могли более просто осуществлять
динамические запросы связных данных. Например, вы можете объявить связь bigOrders как показано ниже:

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

После этого вы сможете выполнять следующие запросы связных данных:

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

Связывание посредством промежуточной таблицы ¶

При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится
промежуточная таблица. Например, таблицы order и item могут быть
связаны посредством промежуточной таблицы с названием order_item. Один заказ будет соотноситься с несколькими товарами,
в то время как один товар будет также соотноситься с несколькими заказами.

При объявлении подобных связей вы можете пользоваться методом via() или методом
viaTable() для указания промежуточной таблицы. Разница между методами
via() и viaTable() заключается в том, что первый
метод указывает промежуточную таблицу с помощью названия связи, в то время как второй метод непосредственно указывает
промежуточную таблицу. Например:

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

или по-другому:

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

Использовать связи, объявленные с помощью промежуточных таблиц, можно точно также, как и обычные связи. Например:

// 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 (...)
// возвращает массив объектов Item
$items = $order->items;

Отложенная и жадная загрузка ¶

В разделе Доступ к связным данным, мы показывали, что вы можете получать доступ к свойству
связи объекта Active Record точно также, как получаете доступ к свойству обычного объекта. SQL-запрос будет выполнен
только во время первого доступа к свойству связи. Мы называем подобный способ получения связных данных отложенной
загрузкой
. Например:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// SQL-запрос не выполняется
$orders2 = $customer->orders;

Отложенная загрузка очень удобна в использовании. Однако этот метод может вызвать проблемы производительности, когда вам
понадобится получить доступ к тем же самым свойствам связей для нескольких объектов Active Record. Рассмотрите
следующий пример кода. Сколько SQL-запросов будет выполнено?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

Как вы могли заметить по вышеприведённым комментариям кода, будет выполнен 101 SQL-запрос! Это произойдёт из-за того,
что каждый раз внутри цикла будет выполняться SQL-запрос при получении доступа к свойству связи orders каждого
отдельного объекта Customer.

Для решения этой проблемы производительности вы можете, как показано ниже, использовать подход, который называется
жадная загрузка:

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // SQL-запрос не выполняется
    $orders = $customer->orders;
}

Посредством вызова метода yiidbActiveQuery::with(), вы указываете объекту Active Record вернуть заказы первых 100
покупателей с помощью одного SQL-запроса. В результате снижаете количество выполняемых SQL-запросов от 101 до 2!

Вы можете жадно загружать одну или несколько связей. Вы можете даже жадно загружать вложенные связи. Вложенная связь —
это связь, которая объявлена внутри связного Active Record класса. Например, Customer связан с Order посредством
связи orders, а Order связан с Item посредством связи items. При формировании запроса для Customer, вы можете
жадно загрузить items, используя нотацию вложенной связи orders.items.

Ниже представлен код, который показывает различные способы использования метода with().
Мы полагаем, что класс Customer имеет две связи: orders и country — в то время как класс Order имеет лишь одну
связь items.

// жадная загрузка "orders" и "country" одновременно
$customers = Customer::find()->with('orders', 'country')->all();
// аналог с использованием синтаксиса массива
$customers = Customer::find()->with(['orders', 'country'])->all();
// SQL-запрос не выполняется
$orders= $customers[0]->orders;
// SQL-запрос не выполняется
$country = $customers[0]->country;

// жадная загрузка связи "orders" и вложенной связи "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// доступ к деталям первого заказа первого покупателя 
// SQL-запрос не выполняется
$items = $customers[0]->orders[0]->items;

Вы можете жадно загрузить более глубокие вложенные связи, такие как a.b.c.d. Все родительские связи будут жадно
загружены. Таким образом, когда вы вызываете метод with() с параметром a.b.c.d, вы
жадно загрузите связи a, a.b, a.b.c и a.b.c.d.

Информация: В целом, когда жадно загружается N связей, среди которых M связей объявлено с помощью
промежуточной таблицы, суммарное количество выполняемых SQL-запросов будет равно N+M+1. Заметьте,
что вложенная связь a.b.c.d насчитывает 4 связи.

Когда связь жадно загружается, вы можете настроить соответствующий запрос получения связных данных с использованием
анонимной функции. Например:

// найти покупателей и получить их вместе с их странами и активными заказами
// 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();

Когда настраивается запрос на получение связных данных для какой-либо связи, вы можете указать название связи в виде
ключа массива и использовать анонимную функцию в качестве соответствующего значения этого массива. Анонимная функция
получит параметр $query, который представляет собой объект yiidbActiveQuery, используемый для выполнения запроса
на получение связных данных для данной связи. В вышеприведённом примере кода мы изменили запрос на получение связных
данных, наложив на него дополнительное условие выборки статуса заказов.

Примечание: Если вы вызываете метод select() в процессе жадной загрузки связей, вы должны
убедиться, что будут выбраны столбцы, участвующие в объявлении связей. Иначе связные модели будут загружены
неправильно. Например:

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer всегда равно null. Для исправления проблемы вы должны сделать следующее:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

Использование JOIN со связями ¶

Примечание: Материал этого раздела применим только к реляционным базам данных, таким как MySQL, PostgreSQL, и т.д.

Запросы на получение связных данных, которые мы рассмотрели выше, ссылаются только на столбцы основной таблицы при
извлечении основной информации. На самом же деле нам часто нужно ссылаться в запросах на столбцы связных таблиц.
Например, мы можем захотеть получить покупателей, для которых имеется хотя бы один активный заказ. Для решения этой
проблемы мы можем построить запрос с использованием JOIN как показано ниже:

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

Примечание: Важно однозначно указывать в SQL-выражениях имена столбцов при построении запросов на получение связных
данных с участием оператора JOIN. Наиболее распространённая практика — предварять названия столбцов с помощью имён
соответствующих им таблиц.

Однако лучшим подходом является использование имеющихся объявлений связей с помощью вызова метода
yiidbActiveQuery::joinWith():

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

Оба подхода выполняют одинаковый набор SQL-запросов. Однако второй подход более прозрачен и прост.

По умолчанию, метод joinWith() будет использовать конструкцию LEFT JOIN для
объединения основной таблицы со связной. Вы можете указать другой тип операции JOIN (например, RIGHT JOIN) с помощью
третьего параметра этого метода — $joinType. Если вам нужен INNER JOIN, вы можете вместо этого просто вызвать
метод innerJoinWith().

Вызов метода joinWith() будет жадно загружать связные данные
по умолчанию. Если вы не хотите получать связные данные, вы можете передать во втором параметре $eagerLoading значение
false.

Подобно методу with() вы можете объединять данные с одной или несколькими связями; вы
можете настроить запрос на получение связных данных «на лету»; вы можете объединять данные с вложенными связями; вы
можете смешивать использование метода with() и метода
joinWith(). Например:

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

Иногда во время объединения двух таблиц вам может потребоваться указать некоторые дополнительные условия рядом с
оператором ON во время выполнения JOIN-запроса. Это можно сделать с помощью вызова метода
yiidbActiveQuery::onCondition() как показано ниже:

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

Вышеприведённый запрос вернёт всех покупателей и для каждого покупателя вернёт все активные заказы. Заметьте, что это
поведение отличается от нашего предыдущего примера, в котором возвращались только покупатели, у которых был как минимум
один активный заказ.

Информация: Когда в объекте yiidbActiveQuery указано условие выборки с помощью метода
onCondition(), это условие будет размещено в конструкции ON, если запрос
содержит оператор JOIN. Если же запрос не содержит оператор JOIN, такое условие будет автоматически размещено в
конструкции WHERE.

Псевдонимы связанных таблиц ¶

Как уже было отмечено, при использовании в запросе JOIN-ов, приходится явно решать конфликты имён. Поэтому часто таблицам
дают псевдонимы. Задать псевдоним для реляционного запроса можно следующим образом:

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

Выглядит это довольно сложно. Либо приходится задавать явно имена таблиц, либо вызывать Order::tableName().
Начиная с версии 2.0.7 вы можете задать и использовать псевдоним для связанной таблицы следующим образом:

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

Этот синтаксис работает для простых связей. Если необходимо использовать связующую таблицу, например
$query->joinWith(['orders.product']), то вызовы joinWith вкладываются друг в друга:

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

Обратные связи ¶

Объявления связей часто взаимны между двумя Active Record классами. Например, Customer связан с Order посредством
связи orders, а Order взаимно связан с Customer посредством связи 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']);
    }
}

Теперь рассмотрим следующий участок кода:

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

// выведет "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

Мы думали, что $customer и $customer2 эквивалентны, но оказалось, что нет! Фактически они содержат одинаковые
данные, но являются разными объектами. Когда мы получаем доступ к данным посредством $order->customer, выполняется
дополнительный SQL-запрос для заполнения нового объекта $customer2.

Чтобы избежать избыточного выполнения последнего SQL-запроса в вышеприведённом примере, мы должны подсказать Yii, что
customerобратная связь относительно orders, и сделаем это с помощью вызова метода
inverseOf() как показано ниже:

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

Теперь, после этих изменений в объявлении связи, получим:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SQL-запрос не выполняется
$customer2 = $order->customer;

// выведет "same"
echo $customer2 === $customer ? 'same' : 'not the same';

Примечание: обратные связи не могут быть объявлены для связей, использующих промежуточную таблицу.
То есть, если связь объявлена с помощью методов via() или
viaTable(), вы не должны вызывать после этого метод
inverseOf().

Сохранение связных данных ¶

Во время работы со связными данными вам часто требуется установить связи между двумя разными видами данных или удалить
существующие связи. Это требует установки правильных значений для столбцов, с помощью которых заданы связи. При
использовании Active Record вам может понадобится завершить участок кода следующим образом:

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

// установка атрибута, которой задаёт связь "customer" в объекте Order
$order->customer_id = $customer->id;
$order->save();

Active Record предоставляет метод link(), который позволяет выполнить эту задачу
более красивым способом:

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

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

Метод link() требует указать название связи и целевой объект Active Record, с которым
должна быть установлена связь. Метод изменит значения атрибутов, которые связывают два объекта Active Record, и сохранит
их в базу данных. В вышеприведённом примере, метод присвоит атрибуту customer_id объекта Order значение атрибута
id объекта Customer и затем сохранит его в базу данных.

Примечание: Невозможно связать два свежесозданных объекта Active Record.

Преимущество метода link() становится ещё более очевидным, когда связь объявлена
посредством промежуточной таблицы. Например, вы можете использовать следующий код, чтобы связать
объект Order с объектом Item:

$order->link('items', $item);

Вышеприведённый код автоматически вставит строку данных в промежуточную таблицу order_item, чтобы связать объект
order с объектом item.

Информация: Метод link() не осуществляет какую-либо валидацию данных во время
сохранения целевого объекта Active Record. На вас лежит ответственность за валидацию любых введённых данных перед
вызовом этого метода.

Существует противоположная операция для link() — это операция
unlink(), она снимает существующую связь с двух объектов Active Record. Например:

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

По умолчанию метод unlink() задаст вторичному ключу (или ключам), который определяет
существующую связь, значение null. Однако вы можете запросить удаление строки таблицы, которая содержит значение
вторичного ключа, передав значение true в параметре $delete для этого метода.

Если связь построена на основе промежуточной таблицы, вызов метода unlink() инициирует
очистку вторичных ключей в промежуточной таблице, или же удаление соответствующей строки данных в промежуточной таблице,
если параметр $delete равен true.

Связывание объектов из разных баз данных ¶

Active Record позволяет вам объявить связи между классами Active Record, которые относятся к разным базам данных. Базы
данных могут быть разных типов (например, MySQL и PostgreSQL или MS SQL и MongoDB), и они могут быть запущены на разных
серверах. Вы можете использовать тот же самый синтаксис для осуществления запросов выборки связных данных. Например:

// Объект Customer соответствует таблице "customer" в реляционной базе данных (например MySQL)
class Customer extends yiidbActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // у покупателя может быть много комментариев
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

// Объект Comment соответствует коллекции "comment" в базе данных MongoDB
class Comment extends yiimongodbActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // комментарий принадлежит одному покупателю
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

Вы можете использовать большую часть возможностей запросов получения связных данных, которые были описаны в этой главе.

Примечание: Применимость метода joinWith() ограничена базами данных, которые
позволяют выполнять запросы между разными базами с использованием оператора JOIN. По этой причине вы не можете
использовать этот метод в вышеприведённом примере, т.к. MongoDB не поддерживает операцию JOIN.

Тонкая настройка классов Query ¶

По умолчанию все запросы данных для Active Record поддерживаются с помощью класса yiidbActiveQuery. Для
использования собственного класса запроса вам необходимо переопределить метод yiidbActiveRecord::find() и
возвращать из него объект вашего собственного класса запроса. Например:

namespace appmodels;

use yiidbActiveRecord;
use yiidbActiveQuery;

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

class CommentQuery extends ActiveQuery
{
    // ...
}

Теперь, когда вы будете осуществлять получение данных (например, выполните find(), findOne()) или объявите связь
(например, hasOne()) с объектом Comment, вы будете работать с объектом класса CommentQuery вместо ActiveQuery.

Подсказка: В больших проектах рекомендуется использовать собственные классы запросов, которые будут содержать в себе
большую часть кода, связанного с настройкой запросов, таким образом классы Active Record удастся сохранить более
чистыми.

Вы можете настроить класс запроса большим количеством различных способов для улучшения методик построения запросов.
Например, можете объявить новые методы построения запросов в собственном классе запросов:

class CommentQuery extends ActiveQuery
{
    public function active($state = true)
    {
        return $this->andWhere(['active' => $state]);
    }
}

Примечание: Вместо вызова метода where() старайтесь во время объявления новых методов
построения запросов использовать andWhere() или
orWhere() для добавления дополнительных условий, в этом случае уже заданные условия
выборок не будут перезаписаны.

Это позволит вам писать код построения запросов как показано ниже:

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

Вы также можете использовать новые методы построения запросов, когда объявляете связи для класса Comment или
осуществляете запрос для выборки связных данных:

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

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

// или по-другому:
 
$customers = Customer::find()->with([
    'comments' => function($q) {
        $q->active();
    }
])->all();

Информация: В Yii версии 1.1 была концепция с названием scope. Она больше не поддерживается в Yii версии 2.0, и вы
можете использовать собственные классы запросов и собственные методы построения запросов, чтобы добиться той же самой
цели.

Получение дополнительных атрибутов ¶

Когда объект Active Record заполнен результатами запроса, его атрибуты заполнены значениями соответствующих столбцов
из полученного набора данных.

Вы можете получить дополнительные столбцы или значения с помощью запроса и сохранить их внутри объекта Active Record.
Например, предположим, что у нас есть таблица ‘room’, которая содержит информацию о доступных в отеле комнатах. Каждая
комната хранит информацию о её геометрических размерах с помощью атрибутов ‘length’, ‘width’, ‘height’. Представьте, что
вам требуется получить список всех доступных комнат, отсортированных по их объёму в порядке убывания. В этом случае вы
не можете вычислять объём с помощью PHP, потому что нам требуется сортировать записи по объёму, но вы также хотите
отображать объем в списке. Для достижения этой цели, вам необходимо объявить дополнительный атрибут в вашем Active
Record классе ‘Room’, который будет хранить значение ‘volume’:

class Room extends yiidbActiveRecord
{
    public $volume;

    // ...
}

Далее вам необходимо составить запрос, который вычисляет объём комнаты и выполняет сортировку:

$rooms = Room::find()
    ->select([
        '{{room}}.*', // получить все столбцы
        '([[length]] * [[width]] * [[height]]) AS volume', // вычислить объём
    ])
    ->orderBy('volume DESC') // отсортировать
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // содержит значение, вычисленное с помощью SQL-запроса
}

Возможность выбирать дополнительные атрибуты может быть особенно полезной для агрегирующих запросов. Представьте, что
вам необходимо отображать список покупателей с количеством их заказов. Прежде всего вам потребуется объявить класс
Customer со связью ‘orders’ и дополнительным атрибутом для хранения расчётов:

class Customer extends yiidbActiveRecord
{
    public $ordersCount;

    // ...

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

После этого вы сможете составить запрос, который объединяет заказы и вычисляет их количество:

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // получить все атрибуты покупателя
        'COUNT({{order}}.id) AS ordersCount' // вычислить количество заказов
    ])
    ->joinWith('orders') // обеспечить построение промежуточной таблицы
    ->groupBy('{{customer}}.id') // сгруппировать результаты, чтобы заставить агрегацию работать
    ->all();

Недостаток этого подхода заключается в том, что если данные для поля не загружены по результатам SQL запроса, то они
должны быть вычисленны отдельно. Это означает, что запись, полученная посредством обычного запроса без дополнительных полей в
разделе ‘select’, не может вернуть реальное значения для дополнительного поля. Это же касается и только что сохранненой
записи.

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

$room->volume; // значение будет равно `null`, т.к. поле не было заполнено

Использование магических методов __get() и __set()
позволяет эмулировать поведение обычного поля:

class Room extends yiidbActiveRecord
{
    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;
    }

    // ...
}

Если результат запроса на выборку данных не содержит поле ‘volume’, то модель сможет расчитать его автоматически
используя имеющиеся атрибуты.

Вы также можете вычислять агрегируемые поля используя объявленные отношения:

class Customer extends yiidbActiveRecord
{
    private $_ordersCount;

    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // нет смысла выполнять запрос на поиск по пустым ключам
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // вычисляем агрегацию по требованию из отношения
        }

        return $this->_ordersCount;
    }

    // ...

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

При такой реализации, в случае когда ‘ordersCount’ присутсвует в разделе ‘select’ — значение ‘Customer::ordersCount’ будет
заполнено из результатов запроса, в противном случае — оно будет вычислено по первому требованию на основании отношения Customer::orders.

Этот подход также можно использовать для быстрого доступа к некоторым данным отношений, в особенности для агрегации.
Например:

class Customer extends yiidbActiveRecord
{
    /**
     * Объявляет виртуальное свойство для агрегируемых данных, доступное только на чтение.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // нет смысла выполнять запрос на поиск по пустым ключам
        }

        return $this->ordersAggregation[0]['counted'];
    }

    /**
     * Объявляет обычное отношение 'orders'.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    /**
     * Объявляет новое отношение, основанное на 'orders', которое предоставляет агрегацию.
     */
    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; // выводит агрегируемые данные из отношения без дополнительного запроса благодаря жадной загрузке
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // выводит агрегируемые данные отношения через ленивую загрузку

Active Record обеспечивает объектно-ориентированный интерфейс для доступа
и манипулирования данными, хранящимися в базах данных. Класс Active Record соответствует таблице в базе данных, объект
Active Record соответствует строке этой таблицы, а атрибут объекта Active Record представляет собой значение
отдельного столбца строки. Вместо непосредственного написания SQL-выражений вы сможете получать доступ к атрибутам
Active Record и вызывать методы Active Record для доступа и манипулирования данными, хранящимися в таблицах базы данных.

Для примера предположим, что Customer — это класс Active Record, который сопоставлен с таблицей customer, а name
столбец в таблице customer. Тогда вы можете написать следующий код для вставки новой строки в таблицу customer:

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

Вышеприведённый код аналогичен использованию следующего SQL-выражения в MySQL, которое менее интуитивно, потенциально
может вызвать ошибки и даже проблемы совместимости, если вы используете различные виды баз данных:

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii поддерживает работу с Active Record для следующих реляционных баз данных:

  • MySQL 4.1 и выше: посредством [[yiidbActiveRecord]]
  • PostgreSQL 7.3 и выше: посредством [[yiidbActiveRecord]]
  • SQLite 2 и 3: посредством [[yiidbActiveRecord]]
  • Microsoft SQL Server 2008 и выше: посредством [[yiidbActiveRecord]]
  • Oracle: посредством [[yiidbActiveRecord]]
  • CUBRID 9.3 и выше: посредством [[yiidbActiveRecord]] (Имейте в виду, что вследствие
    бага в PDO-расширении для CUBRID, заключение значений в кавычки не работает,
    поэтому необходимо использовать CUBRID версии 9.3 как на клиентской стороне, так и на сервере)
  • Sphinx: посредством [[yiisphinxActiveRecord]], потребуется расширение yii2-sphinx
  • ElasticSearch: посредством [[yiielasticsearchActiveRecord]], потребуется расширение yii2-elasticsearch

Кроме того Yii поддерживает использование Active Record со следующими NoSQL базами данных:

  • Redis 2.6.12 и выше: посредством [[yiiredisActiveRecord]], потребуется расширение yii2-redis
  • MongoDB 1.3.0 и выше: посредством [[yiimongodbActiveRecord]], потребуется расширение yii2-mongodb

В этом руководстве мы в основном будем описывать использование Active Record для реляционных баз данных. Однако большая
часть этого материала также применима при использовании Active Record с NoSQL базами данных.

Объявление классов Active Record

Для начала объявите свой собственный класс, унаследовав класс [[yiidbActiveRecord]].

Настройка имени таблицы

По умолчанию каждый класс Active Record ассоциирован с таблицей в базе данных. Метод
[[yiidbActiveRecord::tableName()|tableName()]] получает имя таблицы из имени класса с помощью [[yiihelpersInflector::camel2id()]].
Если таблица не названа соответственно, вы можете переопределить данный метод.

Также может быть применён [[yiidbConnection::$tablePrefix|tablePrefix]] по умолчанию. Например, если
[[yiidbConnection::$tablePrefix|tablePrefix]] задан как tbl_, Customer преобразуется в tbl_customer, а
OrderItem в tbl_order_item.

Если имя таблицы указано в формате {{%TableName}}, символ % заменяется префиксом. Например {{%post}} становится
{{tbl_post}}. Фигуриные скобки используются для экранирования в SQL-запросах.

В нижеследующем примере мы объявляем класс Active Record с названием Customer для таблицы customer.

namespace appmodels;

use yiidbActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string название таблицы, сопоставленной с этим ActiveRecord-классом.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Классы Active record называются «моделями»

Объекты Active Record являются моделями. Именно поэтому мы обычно задаём классам Active Record
пространство имён appmodels (или другое пространство имён, предназначенное для моделей).

Т.к. класс [[yiidbActiveRecord]] наследует класс [[yiibaseModel]], он обладает всеми возможностями
моделей, такими как атрибуты, правила валидации, способы сериализации данных и т.д.

Подключение к базам данных

По умолчанию Active Record для доступа и манипулирования данными БД использует
компонент приложения db в качестве компонента
[[yiidbConnection|DB connection]]. Как сказано в разделе Объекты доступа к данным (DAO), вы можете
настраивать компонент db на уровне конфигурации приложения как показано ниже:

return [
    'components' => [
        'db' => [
            'class' => 'yiidbConnection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

Если вы хотите использовать для подключения к базе данных другой компонент подключения, отличный от db, вам нужно
переопределить метод [[yiidbActiveRecord::getDb()|getDb()]]:

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // использовать компонент приложения "db2"
        return Yii::$app->db2;  
    }
}

Получение данных

После объявления класса Active Record вы можете использовать его для получения данных из соответствующей таблицы базы
данных. Этот процесс, как правило, состоит из следующих трёх шагов:

  1. Создать новый объект запроса вызовом метода [[yiidbActiveRecord::find()]];
  2. Настроить объект запроса вызовом методов построения запросов;
  3. Вызвать один из методов получения данных для извлечения данных в виде объектов
    Active Record.

Как вы могли заметить, эти шаги очень похожи на работу с построителем запросов. Различие лишь в
том, что для создания объекта запроса вместо оператора new используется метод [[yiidbActiveRecord::find()]],
возвращающий новый объект запроса, являющийся представителем класса [[yiidbActiveQuery]].

Ниже приведено несколько примеров использования Active Query для получения данных:

// возвращает покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// возвращает всех активных покупателей, сортируя их по идентификаторам
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// возвращает количество активных покупателей
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// возвращает всех покупателей массивом, индексированным их идентификаторами
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

В примерах выше $customer — это объект класса Customer, в то время как $customers — это массив таких объектов. Все
эти объекты заполнены данными таблицы customer.

Info: Т.к. класс [[yiidbActiveQuery]] наследует [[yiidbQuery]], вы можете использовать в нём все методы
построения запросов и все методы класса Query как описано в разделе Построитель запросов.

Т.к. извлечение данных по первичному ключу или значениям отдельных столбцов достаточно распространённая задача, Yii
предоставляет два коротких метода для её решения:

  • [[yiidbActiveRecord::findOne()]]: возвращает один объект Active Record, заполненный первой строкой результата запроса.
  • [[yiidbActiveRecord::findAll()]]: возвращает массив объектов Active Record, заполненных всеми полученными результатами запроса.

Оба метода могут принимать параметры в одном из следующих форматов:

  • скалярное значение: значение интерпретируется как первичный ключ, по которому следует искать. Yii прочитает
    информацию о структуре базы данных и автоматически определит, какой столбец таблицы содержит первичные ключи.
  • массив скалярных значений: массив интерпретируется как набор первичных ключей, по которым следует искать.
  • ассоциативный массив: ключи массива интерпретируются как названия столбцов, а значения — как содержимое столбцов,
    которое следует искать. За подробностями вы можете обратиться к разделу Hash Format

Нижеследующий код демонстрирует, каким образом эти методы могут быть использованы:

// возвращает покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// возвращает покупателей с идентификаторами 100, 101, 123 и 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// возвращает активного покупателя с идентификатором 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// возвращает всех неактивных покупателей
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

Warning: Если вам нужно передать в эти методы данные, полученные от пользователя, убедитесь что передаваемое значение – это скаляр,
а если необходимо указать условия в формате массива – убедитесь, что пользовательские данные не могут изменить структуру этого массива.

// yiiwebController гарантирует, что $id будет скаляром
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// явное указание имени столбца для поиска гарантирует поиск по столбцу `id`,
// и возвращение одной записи как для массива, так и для скаляра в принятом от пользователя поле `id` 
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// НЕ используйте этот код! Пользователь может передать в параметр `id` массив
// и осуществить поиск по имени столбца, которое не должно быть использовано для поиска по логике вашего приложения.
$model = Post::findOne(Yii::$app->request->get('id'));

Note: Ни метод [[yiidbActiveRecord::findOne()]], ни [[yiidbActiveQuery::one()]] не добавляет условие LIMIT 1 к
генерируемым SQL-запросам. Если ваш запрос может вернуть много строк данных, вы должны вызвать метод limit(1) явно
в целях улучшения производительности, например: Customer::find()->limit(1)->one().

Помимо использования методов построения запросов вы можете также писать запросы на «чистом» SQL для получения данных и
заполнения ими объектов Active Record. Вы можете делать это посредством метода [[yiidbActiveRecord::findBySql()]]:

// возвращает всех неактивных покупателей
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Не используйте дополнительные методы построения запросов после вызова метода
[[yiidbActiveRecord::findBySql()|findBySql()]], т.к. они будут проигнорированы.

Доступ к данным

Как сказано выше, получаемые из базы данные заполняют объекты Active Record и каждая строка результата запроса
соответствует одному объекту Active Record. Вы можете получить доступ к значениям столбцов с помощью атрибутов этих
объектов. Например так:

// "id" и "email" - названия столбцов в таблице "customer"
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Note: Атрибуты объекта Active Record названы в соответствии с названиями столбцов связной таблицы с учётом
регистра. Yii автоматически объявляет для каждого столбца связной таблицы атрибут в Active Record. Вы НЕ должны
переопределять какие-либо из этих атрибутов.

Атрибуты Active Record названы в соответствии с именами столбцов таблицы. Если столбцы вашей таблицы именуются через
нижнее подчёркивание, то может оказаться, что вам придётся писать PHP-код вроде этого: $customer->first_name — в нём
будет использоваться нижнее подчёркивание для разделения слов в названиях атрибутов. Если вы обеспокоены единообразием
стиля кодирования, вам придётся переименовать столбцы вашей таблицы соответствующим образом (например, назвать столбцы
в стиле camelCase).

Преобразование данных

Часто бывает так, что данные вводятся и/или отображаются в формате, который отличается от формата их хранения в базе
данных. Например, в базе данных вы храните дни рождения покупателей в формате UNIX timestamp (что, кстати говоря, не
является хорошим дизайном), в то время как во многих случаях вы хотите манипулировать днями рождения в виде строк
формата 'ДД.ММ.ГГГГ'. Для достижения этой цели, вы можете объявить методы преобразования данных в
ActiveRecord-классе Customer как показано ниже:

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('d.m.Y', $this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

Теперь в своём PHP коде вместо доступа к $customer->birthday, вы сможете получить доступ к $customer->birthdayText,
что позволить вам вводить и отображать дни рождения покупателей в формате 'ДД.ММ.ГГГГ'.

Tip: Вышеприведённый пример демонстрирует общий способ преобразования данных в различные форматы. Если вы
работаете с датами и временем, вы можете использовать DateValidator и
[[yiijuiDatePicker|DatePicker]], которые проще в использовании и являются более мощными инструментами.

Получение данных в виде массива

Несмотря на то, что получение данных в виде Active Record объектов является удобным и гибким, этот способ не всегда
подходит при получении большого количества данных из-за больших накладных расходов памяти. В этом случае вы можете
получить данные в виде PHP-массива, используя перед выполнением запроса метод
[[yiidbActiveQuery::asArray()|asArray()]]:

// возвращает всех покупателей
// каждый покупатель будет представлен в виде ассоциативного массива
$customers = Customer::find()
    ->asArray()
    ->all();

Note: В то время как этот способ бережёт память и улучшает производительность, он ближе к низкому слою
абстракции базы данных и вы потеряете многие возможности Active Record. Важное отличие заключается в типах данных
значений столбцов. Когда вы получаете данные в виде объектов Active Record, значения столбцов автоматически приводятся
к типам, соответствующим типам столбцов; с другой стороны, когда вы получаете данные в массивах, значения столбцов
будут строковыми (до тех пор, пока они являются результатом работы PDO-слоя без какой-либо обработки), несмотря на
настоящие типы данных соответствующих столбцов.

Пакетное получение данных

В главе Построитель запросов мы объясняли, что вы можете использовать пакетную выборку для
снижения расходов памяти при получении большого количества данных из базы. Вы можете использовать такой же подход при
работе с Active Record. Например:

// получить 10 покупателей одновременно
foreach (Customer::find()->batch(10) as $customers) {
    // $customers - это массив, в котором находится 10 или меньше объектов класса Customer
}

// получить одновременно десять покупателей и перебрать их одного за другим
foreach (Customer::find()->each(10) as $customer) {
    // $customer - это объект класса Customer
}

// пакетная выборка с жадной загрузкой
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer - это объект класса Customer
}

Сохранение данных

Используя Active Record, вы легко можете сохранить данные в базу данных, осуществив следующие шаги:

  1. Подготовьте объект Active Record;
  2. Присвойте новые значения атрибутам Active Record;
  3. Вызовите метод [[yiidbActiveRecord::save()]] для сохранения данных в базу данных.

Например:

// вставить новую строку данных
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// обновить имеющуюся строку данных
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

Метод [[yiidbActiveRecord::save()|save()]] может вставить или обновить строку данных в зависимости от состояния
Active Record объекта. Если объект создан с помощью оператора new, вызов метода [[yiidbActiveRecord::save()|save()]]
приведёт к вставке новой строки данных; если объект был получен с помощью запроса на получение данных, вызов
[[yiidbActiveRecord::save()|save()]] обновит строку таблицы, соответствующую объекту Active Record.

Вы можете различать два состояния Active Record объекта с помощью проверки значения его свойства
[[yiidbActiveRecord::isNewRecord|isNewRecord]]. Это свойство также используется внутри метода
[[yiidbActiveRecord::save()|save()]] как показано ниже:

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

Tip: Вы можете вызвать [[yiidbActiveRecord::insert()|insert()]] или [[yiidbActiveRecord::update()|update()]]
непосредственно, чтобы вставить или обновить строку данных в таблице.

Валидация данных

Т.к. класс [[yiidbActiveRecord]] наследует класс [[yiibaseModel]], он обладает такими же возможностями
валидации данных. Вы можете объявить правила валидации переопределив метод
[[yiidbActiveRecord::rules()|rules()]] и осуществлять валидацию данных посредством вызовов метода
[[yiidbActiveRecord::validate()|validate()]].

Когда вы вызываете метод [[yiidbActiveRecord::save()|save()]], по умолчанию он автоматически вызывает метод
[[yiidbActiveRecord::validate()|validate()]]. Только после успешного прохождения валидации происходит сохранение
данных; в ином случае метод [[yiidbActiveRecord::save()|save()]] просто возвращает false, и вы можете проверить
свойство [[yiidbActiveRecord::errors|errors]] для получения сообщений об ошибках валидации.

Tip: Если вы уверены, что ваши данные не требуют валидации (например, данные пришли из доверенного источника),
вы можете вызвать save(false), чтобы пропустить валидацию.

Массовое присваивание

Как и обычные модели, объекты Active Record тоже обладают
возможностью массового присваивания. Как будет показано ниже, используя эту
возможность, вы можете одним PHP выражением присвоить значения множества атрибутов Active Record объекту. Запомните
однако, что только безопасные атрибуты могут быть массово присвоены.

$values = [
    'name' => 'James',
    'email' => 'james@example.com',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

Обновление счётчиков

Распространённой задачей является инкремент или декремент столбца в таблице базы данных. Назовём такие столбцы
столбцами-счётчиками. Вы можете использовать метод [[yiidbActiveRecord::updateCounters()|updateCounters()]] для
обновления одного или нескольких столбцов-счётчиков. Например:

$post = Post::findOne(100);

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

Note: Если вы используете метод [[yiidbActiveRecord::save()]] для обновления столбца-счётчика, вы можете
прийти к некорректному результату, т.к. вполне вероятно, что этот же счётчик был сохранён сразу несколькими запросами,
которые читают и записывают этот же столбец-счётчик.

Dirty-атрибуты

Когда вы вызываете [[yiidbActiveRecord::save()|save()]] для сохранения Active Record объекта, сохраняются только
dirty-атрибуты. Атрибут считается dirty-атрибутом, если его значение было изменено после чтения из базы данных или
же он был сохранён в базу данных совсем недавно. Заметьте, что валидация данных осуществляется независимо от того,
имеются ли dirty-атрибуты в объекте Active Record или нет.

Active Record автоматически поддерживает список dirty-атрибутов. Это достигается за счёт хранения старых значений
атрибутов и сравнения их с новыми. Вы можете вызвать метод [[yiidbActiveRecord::getDirtyAttributes()]] для получения
текущего списка dirty-атрибутов. Вы также можете вызвать [[yiidbActiveRecord::markAttributeDirty()]], чтобы явно
пометить атрибут в качестве dirty-атрибута.

Если вам нужны значения атрибутов, какими они были до их изменения, вы можете вызвать
[[yiidbActiveRecord::getOldAttributes()|getOldAttributes()]] или
[[yiidbActiveRecord::getOldAttribute()|getOldAttribute()]].

Note: Сравнение старых и новых значений будет осуществлено с помощью оператора ===, так что значение будет
считаться dirty-значением даже в том случае, если оно осталось таким же, но изменило свой тип. Это часто происходит,
когда модель получает пользовательский ввод из HTML-форм, где каждое значение представлено строкой. Чтобы убедиться в
корректности типа данных, например для целых значений, вы можете применить
фильтрацию данных: ['attributeName', 'filter', 'filter' => 'intval'].

Значения атрибутов по умолчанию

Некоторые столбцы ваших таблиц могут иметь значения по умолчанию, объявленные в базе данных. Иногда вы можете захотеть
предварительно заполнить этими значениями вашу веб-форму, которая соответствует Active Record объекту. Чтобы избежать
повторного указания этих значений, вы можете вызвать метод
[[yiidbActiveRecord::loadDefaultValues()|loadDefaultValues()]] для заполнения соответствующих Active Record атрибутов
значениями по умолчанию, объявленными в базе данных:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz получит значение по умолчанию, которое было указано при объявлении столбца "xyz"

Приведение типов атрибутов

При заполнении результатами запроса [[yiidbActiveRecord]] производит автоматическое приведение типов для значений
атрибутов на основе информации из схемы базы данны. Это позволяет данным, полученным из
колонки таблицы объявленной как целое, заноситься в экземпляр ActiveRecord как значение целого типа PHP, булево как
булево и т.д.
Однако, механизм приведения типов имеет несколько ограничений:

  • Числа с плавающей точкой не будут обработаны, а будут представленны как строки, в противном случае они могут потерять точность.
  • Конвертация целых чисел зависит от разрядности используемой операционной системы. В частности: значения колонок, объявленных
    как ‘unsigned integer’ или ‘big integer’ будут приведены к целому типу PHP только на 64-х разрядных системах, в то время
    как на 32-х разрядных — они будут представленны как строки.

Имейте в виду, что преобразование типов производиться только в момент заполнения экземпляра ActiveRecord данными из результата
запроса. При заполнении данных из HTTP запроса или непосредственно через механизм доступа к полям — автоматическая конвертация
не производтся.
Схема таблицы базы данных также используется при построении SQL запроса для сохранения данных ActiveRecord, обеспечивая
соответсвие типов связываемых параметров в запросе. Однако, над атрибутами объекта ActiveRecord не будет производиться
приведение типов в процессе сохранения.

Совет: вы можете использовать поведение [[yiibehaviorsAttributeTypecastBehavior]] для того, чтобы производить
приведение типов для ActiveRecord во время валидации или сохранения.

Начиная с 2.0.14, Yii ActiveRecord поддерживает сложные типы данных, такие как JSON или многомерные массивы.

JSON в MySQL и PostgreSQL

После заполнения данных, значение из столбца JSON будет автоматически декодировано из JSON в соответствии со стандартными правилами декодирования JSON.

Чтобы сохранить значение атрибута в столбец JSON, ActiveRecord автоматически создаст объект [[yiidbJsonExpression|JsonExpression]], который будет закодирован в строку JSON на уровне QueryBuilder.

Массивы в PostgreSQL

После заполнения данных значение из столбца Array будет автоматически декодировано из нотации PgSQL в объект [[yiidbArrayExpression|ArrayExpression]]. Он реализует интерфейс PHP ArrayAccess, так что вы можете использовать его в качестве массива, или вызвать ->getValue (), чтобы получить сам массив.

Чтобы сохранить значение атрибута в столбец массива, ActiveRecord автоматически создаст объект [[yiidbArray Expression|ArrayExpression]], который будет закодирован QueryBuilder в строковое представление массива PgSQL.

Можно также использовать условия для столбцов JSON:

$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])

Дополнительные сведения о системе построения выражений см. Query Builder – добавление пользовательских условий и выражений

Обновление нескольких строк данных

Методы, представленные выше, работают с отдельными Active Record объектами, инициируя вставку или обновление данных для
отдельной строки таблицы. Вместо них для обновления нескольких строк одновременно можно использовать метод
[[yiidbActiveRecord::updateAll()|updateAll()]], который является статическим.

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

Подобным образом можно использовать метод [[yiidbActiveRecord::updateAllCounters()|updateAllCounters()]] для
обновления значений столбцов-счётчиков в нескольких строках одновременно.

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

Удаление данных

Для удаления одной отдельной строки данных сначала получите Active Record объект, соответствующий этой строке, а затем
вызовите метод [[yiidbActiveRecord::delete()]].

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

Вы можете вызвать [[yiidbActiveRecord::deleteAll()]] для удаления всех или нескольких строк данных одновременно.
Например:

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

Note: будьте очень осторожны, используя метод [[yiidbActiveRecord::deleteAll()|deleteAll()]], потому что он
может полностью удалить все данные из вашей таблицы, если вы сделаете ошибку при указании условий удаления.

Жизненные циклы Active Record

Важно понимать как устроены жизненные циклы Active Record при использовании Active Record для различных целей.
В течение каждого жизненного цикла вызывается определённая последовательность методов, которые вы можете переопределять,
чтобы получить возможность тонкой настройки жизненного цикла. Для встраивания своего кода вы также можете отвечать на
конкретные события Active Record, которые срабатывают в течение жизненного цикла. Эти события особенно полезны, когда
вы разрабатываете поведения, которые требуют тонкой настройки жизненных циклов Active Record.

Ниже мы подробно опишем различные жизненные циклы Active Record и методы/события, которые участвуют в жизненных циклах.

Жизненный цикл создания нового объекта

Когда создаётся новый объект Active Record с помощью оператора new, следующий жизненный цикл имеет место:

  1. Вызывается конструктор класса;
  2. Вызывается [[yiidbActiveRecord::init()|init()]]:
    инициируется событие [[yiidbActiveRecord::EVENT_INIT|EVENT_INIT]].

Жизненный цикл получения данных

Когда происходит получение данных посредством одного из методов получения данных, каждый вновь
создаваемый объект Active Record при заполнении данными проходит следующий жизненный цикл:

  1. Вызывается конструктор класса.
  2. Вызывается [[yiidbActiveRecord::init()|init()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_INIT|EVENT_INIT]].
  3. Вызывается [[yiidbActiveRecord::afterFind()|afterFind()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_AFTER_FIND|EVENT_AFTER_FIND]].

Жизненный цикл сохранения данных

Когда вызывается метод [[yiidbActiveRecord::save()|save()]] для вставки или обновления объекта Active Record,
следующий жизненный цикл имеет место:

  1. Вызывается [[yiidbActiveRecord::beforeValidate()|beforeValidate()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]]. Если метод возвращает false или свойство
    события [[yiibaseModelEvent::isValid]] равно false, оставшиеся шаги не выполняются.
  2. Осуществляется валидация данных. Если валидация закончилась неудачей, после 3-го шага остальные шаги не выполняются.
  3. Вызывается [[yiidbActiveRecord::afterValidate()|afterValidate()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_AFTER_VALIDATE|EVENT_AFTER_VALIDATE]].
  4. Вызывается [[yiidbActiveRecord::beforeSave()|beforeSave()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_BEFORE_INSERT|EVENT_BEFORE_INSERT]] или событие
    [[yiidbActiveRecord::EVENT_BEFORE_UPDATE|EVENT_BEFORE_UPDATE]]. Если метод возвращает false или свойство события
    [[yiibaseModelEvent::isValid]] равно false, оставшиеся шаги не выполняются.
  5. Осуществляется фактическая вставка или обновление данных в базу данных;
  6. Вызывается [[yiidbActiveRecord::afterSave()|afterSave()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] или событие
    [[yiidbActiveRecord::EVENT_AFTER_UPDATE|EVENT_AFTER_UPDATE]].

Жизненный цикл удаления данных

Когда вызывается метод [[yiidbActiveRecord::delete()|delete()]] для удаления объекта Active Record, следующий
жизненный цикл имеет место:

  1. Вызывается [[yiidbActiveRecord::beforeDelete()|beforeDelete()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_BEFORE_DELETE|EVENT_BEFORE_DELETE]]. Если метод возвращает false или свойство события
    [[yiibaseModelEvent::isValid]] равно false, остальные шаги не выполняются.
  2. Осуществляется фактическое удаление данных из базы данных.
  3. Вызывается [[yiidbActiveRecord::afterDelete()|afterDelete()]]: инициируется событие
    [[yiidbActiveRecord::EVENT_AFTER_DELETE|EVENT_AFTER_DELETE]].

Note: Вызов следующих методов НЕ инициирует ни один из вышеприведённых жизненных циклов:

  • [[yiidbActiveRecord::updateAll()]]
  • [[yiidbActiveRecord::deleteAll()]]
  • [[yiidbActiveRecord::updateCounters()]]
  • [[yiidbActiveRecord::updateAllCounters()]]

Работа с транзакциями

Есть два способа использования транзакций при работе с Active Record.

Первый способ заключается в том, чтобы явно заключить все вызовы методов Active Record в блок транзакции как показано
ниже:

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...другие операции с базой данных...
});

// или по-другому

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...другие операции с базой данных...
    $transaction->commit();
} catch(Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

Note: в коде выше ради совместимости с PHP 5.x и PHP 7.x использованы два блока catch.
Exception реализует интерфейс Throwable interface
начиная с PHP 7.0. Если вы используете только PHP 7 и новее, можете пропустить блок с Exception.

Второй способ заключается в том, чтобы перечислить операции с базой данных, которые требуют тразнакционного выполнения,
в методе [[yiidbActiveRecord::transactions()]]. Например:

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // вышеприведённая строка эквивалентна следующей:
            // 'api' => self::OP_ALL,
        ];
    }
}

Метод [[yiidbActiveRecord::transactions()]] должен возвращать массив, ключи которого являются именами
сценариев, а значения соответствуют операциям, которые должны быть выполнены с помощью
транзакций. Вы должны использовать следующие константы для обозначения различных операций базы данных:

  • [[yiidbActiveRecord::OP_INSERT|OP_INSERT]]: операция вставки, осуществляемая с помощью метода
    [[yiidbActiveRecord::insert()|insert()]];
  • [[yiidbActiveRecord::OP_UPDATE|OP_UPDATE]]: операция обновления, осуществляемая с помощью метода
    [[yiidbActiveRecord::update()|update()]];
  • [[yiidbActiveRecord::OP_DELETE|OP_DELETE]]: операция удаления, осуществляемая с помощью метода
    [[yiidbActiveRecord::delete()|delete()]].

Используйте операторы | для объединения вышеприведённых констант при обозначении множества операций. Вы можете также
использовать вспомогательную константу [[yiidbActiveRecord::OP_ALL|OP_ALL]], чтобы обозначить одной константой все три
вышеприведённые операции.

Оптимистическая блокировка

Оптимистическая блокировка — это способ предотвращения конфликтов, которые могут возникать, когда одна и та же строка
данных обновляется несколькими пользователями. Например, пользователь A и пользователь B одновременно редактируют одну и
ту же wiki-статью. После того, как пользователь A сохранит свои изменения, пользователь B нажимает на кнопку «Сохранить»
в попытке также сохранить свои изменения. Т.к. пользователь B работал с фактически-устаревшей версией статьи, было бы
неплохо иметь способ предотвратить сохранение его варианта статьи и показать ему некоторое сообщение с подсказкой о том,
что произошло.

Оптимистическая блокировка решает вышеприведённую проблему за счёт использования отдельного столбца для сохранения
номера версии каждой строки данных. Когда строка данных сохраняется с использованием устаревшего номера версии,
выбрасывается исключение [[yiidbStaleObjectException]], которое предохраняет строку от сохранения. Оптимистическая
блокировка поддерживается только тогда, когда вы обновляете или удаляете существующую строку данных, используя методы
[[yiidbActiveRecord::update()]] или [[yiidbActiveRecord::delete()]] соответственно.

Для использования оптимистической блокировки:

  1. Создайте столбец в таблице базы данных, ассоциированной с классом Active Record, для сохранения номера версии каждой
    строки данных. Столбец должен быть типа big integer (в Mysql это будет BIGINT DEFAULT 0).
  2. Переопределите метод [[yiidbActiveRecord::optimisticLock()]] таким образом, чтобы он возвращал название этого
    столбца.
  3. В веб-форме, которая принимает пользовательский ввод, добавьте скрытое поле для сохранения текущей версии обновляемой
    строки. Убедитесь, что для вашего атрибута с версией объявлены правила валидации, и валидация проходит успешно.
  4. В действии контроллера, которое занимается обновлением строки данных с использованием Active Record, оберните в блок
    try…catch код и перехватывайте исключение [[yiidbStaleObjectException]]. Реализуйте необходимую бизнес-логику
    (например, возможность слияния изменений, подсказку о том, что данные устарели) для разрешения возникшего конфликта.

Например, предположим, что столбец с версией называется version. Вы можете реализовать оптимистическую блокировку с
помощью подобного кода:

// ------ код представления -------

use yiihelpersHtml;

// ...другие поля ввода
echo Html::activeHiddenInput($model, 'version');


// ------ код контроллера -------

use yiidbStaleObjectException;

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) {
        // логика разрешения конфликта версий
    }
}

Работа со связными данными

Помимо работы с отдельными таблицами баз данных, Active Record также имеет возможность объединять связные данные, что
делает их легко-доступными для получения через основные объекты данных. Например, данные покупателя связаны с данными
заказов, потому что один покупатель может осуществить один или несколько заказов. С помощью объявления этой связи вы
можете получить возможность доступа к информации о заказе покупателя с помощью выражения $customer->orders, которое
возвращает информацию о заказе покупателя в виде массива объектов класса Order, которые являются Active Record
объектами.

Объявление связей

Для работы со связными данными посредством Active Record вы прежде всего должны объявить связи в классе Active Record.
Эта задача решается простым объявлением методов получения связных данных для каждой интересующей вас связи как
показано ниже:

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

В вышеприведённом коде мы объявили связь orders для класса Customer и связь customer для класса Order.

Каждый метод получения связных данных должен быть назван в формате getXyz. Мы называем xyz (первая буква в нижнем
регистре) именем связи. Помните, что имена связей чувствительны к регистру.

При объявлении связи, вы должны указать следующую информацию:

  • кратность связи: указывается с помощью вызова метода [[yiidbActiveRecord::hasMany()|hasMany()]] или метода
    [[yiidbActiveRecord::hasOne()|hasOne()]]. В вышеприведённом примере вы можете легко увидеть в объявлениях связей,
    что покупатель может иметь много заказов в то время, как заказ может быть сделан лишь одним покупателем.

  • название связного Active Record класса: указывается в качестве первого параметра для метода
    [[yiidbActiveRecord::hasMany()|hasMany()]] или для метода [[yiidbActiveRecord::hasOne()|hasOne()]]. Рекомендуется
    использовать код Xyz::class, чтобы получить строку с именем класса, при этом вы сможете воспользоваться
    возможностями авто-дополнения кода, встроенного в IDE, а также получите обработку ошибок на этапе компиляции.

  • связь между двумя типами данных: указываются столбцы с помощью которых два типа данных связаны. Значения массива — это
    столбцы основного объекта данных (представлен классом Active Record, в котором объявляется связь), в то время как
    ключи массива — столбцы связанных данных.

    Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record
    указывается сразу после указания самого класса Active Record. Вы видите, что customer_id — это свойство класса
    Order, а id — свойство класса Customer.

Warning: Имя связи relation зарезервировано. Его использование приведёт к ошибке ArgumentCountError.

Доступ к связным данным

После объявления связей вы можете получать доступ к связным данным с помощью имён связей. Это происходит таким же
образом, каким осуществляется доступ к свойству объекта объявленному с помощью метода получения
связных данных. По этой причине, мы называем его свойством связи. Например:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders - это массив объектов Order
$orders = $customer->orders;

Info: когда вы объявляете связь с названием xyz посредством геттера getXyz(), у вас появляется возможность
доступа к свойству xyz подобно свойству объекта. Помните, что название связи чувствительно
к регистру.

Если связь объявлена с помощью метода [[yiidbActiveRecord::hasMany()|hasMany()]], доступ к свойству связи вернёт
массив связных объектов Active Record; если связь объявлена с помощью метода [[yiidbActiveRecord::hasOne()|hasOne()]],
доступ к свойству связи вернёт связный Active Record объект или null, если связные данные не найдены.

Когда вы запрашиваете свойство связи в первый раз, выполняется SQL-выражение как показано в примере выше. Если то же
самое свойство запрашивается вновь, будет возвращён результат предыдущего SQL-запроса без повторного выполнения
SQL-выражения. Для принудительного повторного выполнения SQL-запроса, вы можете удалить свойство связи с помощью
операции: unset($customer->orders).

Note: Несмотря на то, что эта концепция выглядит похожей на концепцию свойств объектов,
между ними есть важное различие. Для обычных свойств объектов значения свойств имеют тот же тип, который возвращает
геттер. Однако метод получения связных данных возвращает объект [[yiidbActiveQuery]], в то время как доступ к
свойству связи возвращает объект [[yiidbActiveRecord]] или массив таких объектов.

$customer->orders; // массив объектов `Order`
$customer->getOrders(); // объект ActiveQuery

Это полезно при тонкой настройке запросов к связным данным, что будет описано в следующем разделе.

Динамические запросы связных данных

Т.к. метод получения связных данных возвращает объект запроса [[yiidbActiveQuery]], вы можете в дальнейшем перед его
отправкой в базу данных настроить этот запрос, используя методы построения запросов. Например:

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

В отличие от доступа к данным с помощью свойства связи, каждый раз при выполнении такого динамического запроса
посредством метода получения связных данных будет выполняться SQL-запрос, даже если тот же самый динамический запрос был
отправлен ранее.

Иногда вы можете даже захотеть настроить объявление связи таким образом, чтобы вы могли более просто осуществлять
динамические запросы связных данных. Например, вы можете объявить связь bigOrders как показано ниже:

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

После этого вы сможете выполнять следующие запросы связных данных:

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

Связывание посредством промежуточной таблицы

При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится
промежуточная таблица. Например, таблицы order и item могут быть
связаны посредством промежуточной таблицы с названием order_item. Один заказ будет соотноситься с несколькими товарами,
в то время как один товар будет также соотноситься с несколькими заказами.

При объявлении подобных связей вы можете пользоваться методом [[yiidbActiveQuery::via()|via()]] или методом
[[yiidbActiveQuery::viaTable()|viaTable()]] для указания промежуточной таблицы. Разница между методами
[[yiidbActiveQuery::via()|via()]] и [[yiidbActiveQuery::viaTable()|viaTable()]] заключается в том, что первый
метод указывает промежуточную таблицу с помощью названия связи, в то время как второй метод непосредственно указывает
промежуточную таблицу. Например:

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

или по-другому:

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

Использовать связи, объявленные с помощью промежуточных таблиц, можно точно также, как и обычные связи. Например:

// 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 (...)
// возвращает массив объектов Item
$items = $order->items;

Отложенная и жадная загрузка

В разделе Доступ к связным данным, мы показывали, что вы можете получать доступ к свойству
связи объекта Active Record точно также, как получаете доступ к свойству обычного объекта. SQL-запрос будет выполнен
только во время первого доступа к свойству связи. Мы называем подобный способ получения связных данных отложенной
загрузкой
. Например:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// SQL-запрос не выполняется
$orders2 = $customer->orders;

Отложенная загрузка очень удобна в использовании. Однако этот метод может вызвать проблемы производительности, когда вам
понадобится получить доступ к тем же самым свойствам связей для нескольких объектов Active Record. Рассмотрите
следующий пример кода. Сколько SQL-запросов будет выполнено?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

Как вы могли заметить по вышеприведённым комментариям кода, будет выполнен 101 SQL-запрос! Это произойдёт из-за того,
что каждый раз внутри цикла будет выполняться SQL-запрос при получении доступа к свойству связи orders каждого
отдельного объекта Customer.

Для решения этой проблемы производительности вы можете, как показано ниже, использовать подход, который называется
жадная загрузка:

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // SQL-запрос не выполняется
    $orders = $customer->orders;
}

Посредством вызова метода [[yiidbActiveQuery::with()]], вы указываете объекту Active Record вернуть заказы первых 100
покупателей с помощью одного SQL-запроса. В результате снижаете количество выполняемых SQL-запросов от 101 до 2!

Вы можете жадно загружать одну или несколько связей. Вы можете даже жадно загружать вложенные связи. Вложенная связь —
это связь, которая объявлена внутри связного Active Record класса. Например, Customer связан с Order посредством
связи orders, а Order связан с Item посредством связи items. При формировании запроса для Customer, вы можете
жадно загрузить items, используя нотацию вложенной связи orders.items.

Ниже представлен код, который показывает различные способы использования метода [[yiidbActiveQuery::with()|with()]].
Мы полагаем, что класс Customer имеет две связи: orders и country — в то время как класс Order имеет лишь одну
связь items.

// жадная загрузка "orders" и "country" одновременно
$customers = Customer::find()->with('orders', 'country')->all();
// аналог с использованием синтаксиса массива
$customers = Customer::find()->with(['orders', 'country'])->all();
// SQL-запрос не выполняется
$orders= $customers[0]->orders;
// SQL-запрос не выполняется
$country = $customers[0]->country;

// жадная загрузка связи "orders" и вложенной связи "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// доступ к деталям первого заказа первого покупателя 
// SQL-запрос не выполняется
$items = $customers[0]->orders[0]->items;

Вы можете жадно загрузить более глубокие вложенные связи, такие как a.b.c.d. Все родительские связи будут жадно
загружены. Таким образом, когда вы вызываете метод [[yiidbActiveQuery::with()|with()]] с параметром a.b.c.d, вы
жадно загрузите связи a, a.b, a.b.c и a.b.c.d.

Info: В целом, когда жадно загружается N связей, среди которых M связей объявлено с помощью
промежуточной таблицы, суммарное количество выполняемых SQL-запросов будет равно N+M+1. Заметьте,
что вложенная связь a.b.c.d насчитывает 4 связи.

Когда связь жадно загружается, вы можете настроить соответствующий запрос получения связных данных с использованием
анонимной функции. Например:

// найти покупателей и получить их вместе с их странами и активными заказами
// 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();

Когда настраивается запрос на получение связных данных для какой-либо связи, вы можете указать название связи в виде
ключа массива и использовать анонимную функцию в качестве соответствующего значения этого массива. Анонимная функция
получит параметр $query, который представляет собой объект [[yiidbActiveQuery]], используемый для выполнения запроса
на получение связных данных для данной связи. В вышеприведённом примере кода мы изменили запрос на получение связных
данных, наложив на него дополнительное условие выборки статуса заказов.

Note: Если вы вызываете метод [[yiidbQuery::select()|select()]] в процессе жадной загрузки связей, вы должны
убедиться, что будут выбраны столбцы, участвующие в объявлении связей. Иначе связные модели будут загружены
неправильно. Например:

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer всегда равно null. Для исправления проблемы вы должны сделать следующее:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

Использование JOIN со связями

Note: Материал этого раздела применим только к реляционным базам данных, таким как MySQL, PostgreSQL, и т.д.

Запросы на получение связных данных, которые мы рассмотрели выше, ссылаются только на столбцы основной таблицы при
извлечении основной информации. На самом же деле нам часто нужно ссылаться в запросах на столбцы связных таблиц.
Например, мы можем захотеть получить покупателей, для которых имеется хотя бы один активный заказ. Для решения этой
проблемы мы можем построить запрос с использованием JOIN как показано ниже:

// 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: Важно однозначно указывать в SQL-выражениях имена столбцов при построении запросов на получение связных
данных с участием оператора JOIN. Наиболее распространённая практика — предварять названия столбцов с помощью имён
соответствующих им таблиц.

Однако лучшим подходом является использование имеющихся объявлений связей с помощью вызова метода
[[yiidbActiveQuery::joinWith()]]:

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

Оба подхода выполняют одинаковый набор SQL-запросов. Однако второй подход более прозрачен и прост.

По умолчанию, метод [[yiidbActiveQuery::joinWith()|joinWith()]] будет использовать конструкцию LEFT JOIN для
объединения основной таблицы со связной. Вы можете указать другой тип операции JOIN (например, RIGHT JOIN) с помощью
третьего параметра этого метода — $joinType. Если вам нужен INNER JOIN, вы можете вместо этого просто вызвать
метод [[yiidbActiveQuery::innerJoinWith()|innerJoinWith()]].

Вызов метода [[yiidbActiveQuery::joinWith()|joinWith()]] будет жадно загружать связные данные
по умолчанию. Если вы не хотите получать связные данные, вы можете передать во втором параметре $eagerLoading значение
false.

Подобно методу [[yiidbActiveQuery::with()|with()]] вы можете объединять данные с одной или несколькими связями; вы
можете настроить запрос на получение связных данных «на лету»; вы можете объединять данные с вложенными связями; вы
можете смешивать использование метода [[yiidbActiveQuery::with()|with()]] и метода
[[yiidbActiveQuery::joinWith()|joinWith()]]. Например:

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

Иногда во время объединения двух таблиц вам может потребоваться указать некоторые дополнительные условия рядом с
оператором ON во время выполнения JOIN-запроса. Это можно сделать с помощью вызова метода
[[yiidbActiveQuery::onCondition()]] как показано ниже:

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

Вышеприведённый запрос вернёт всех покупателей и для каждого покупателя вернёт все активные заказы. Заметьте, что это
поведение отличается от нашего предыдущего примера, в котором возвращались только покупатели, у которых был как минимум
один активный заказ.

Info: Когда в объекте [[yiidbActiveQuery]] указано условие выборки с помощью метода
[[yiidbActiveQuery::onCondition()|onCondition()]], это условие будет размещено в конструкции ON, если запрос
содержит оператор JOIN. Если же запрос не содержит оператор JOIN, такое условие будет автоматически размещено в
конструкции WHERE.

Псевдонимы связанных таблиц

Как уже было отмечено, при использовании в запросе JOIN-ов, приходится явно решать конфликты имён. Поэтому часто таблицам
дают псевдонимы. Задать псевдоним для реляционного запроса можно следующим образом:

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

Выглядит это довольно сложно. Либо приходится задавать явно имена таблиц, либо вызывать Order::tableName().
Начиная с версии 2.0.7 вы можете задать и использовать псевдоним для связанной таблицы следующим образом:

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

Этот синтаксис работает для простых связей. Если необходимо использовать связующую таблицу, например
$query->joinWith(['orders.product']), то вызовы joinWith вкладываются друг в друга:

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

Обратные связи

Объявления связей часто взаимны между двумя Active Record классами. Например, Customer связан с Order посредством
связи orders, а Order взаимно связан с Customer посредством связи 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']);
    }
}

Теперь рассмотрим следующий участок кода:

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

// выведет "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

Мы думали, что $customer и $customer2 эквивалентны, но оказалось, что нет! Фактически они содержат одинаковые
данные, но являются разными объектами. Когда мы получаем доступ к данным посредством $order->customer, выполняется
дополнительный SQL-запрос для заполнения нового объекта $customer2.

Чтобы избежать избыточного выполнения последнего SQL-запроса в вышеприведённом примере, мы должны подсказать Yii, что
customerобратная связь относительно orders, и сделаем это с помощью вызова метода
[[yiidbActiveQuery::inverseOf()|inverseOf()]] как показано ниже:

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

Теперь, после этих изменений в объявлении связи, получим:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SQL-запрос не выполняется
$customer2 = $order->customer;

// выведет "same"
echo $customer2 === $customer ? 'same' : 'not the same';

Note: обратные связи не могут быть объявлены для связей, использующих промежуточную таблицу.
То есть, если связь объявлена с помощью методов [[yiidbActiveQuery::via()|via()]] или
[[yiidbActiveQuery::viaTable()|viaTable()]], вы не должны вызывать после этого метод
[[yiidbActiveQuery::inverseOf()|inverseOf()]].

Сохранение связных данных

Во время работы со связными данными вам часто требуется установить связи между двумя разными видами данных или удалить
существующие связи. Это требует установки правильных значений для столбцов, с помощью которых заданы связи. При
использовании Active Record вам может понадобится завершить участок кода следующим образом:

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

// установка атрибута, которой задаёт связь "customer" в объекте Order
$order->customer_id = $customer->id;
$order->save();

Active Record предоставляет метод [[yiidbActiveRecord::link()|link()]], который позволяет выполнить эту задачу
более красивым способом:

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

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

Метод [[yiidbActiveRecord::link()|link()]] требует указать название связи и целевой объект Active Record, с которым
должна быть установлена связь. Метод изменит значения атрибутов, которые связывают два объекта Active Record, и сохранит
их в базу данных. В вышеприведённом примере, метод присвоит атрибуту customer_id объекта Order значение атрибута
id объекта Customer и затем сохранит его в базу данных.

Note: Невозможно связать два свежесозданных объекта Active Record.

Преимущество метода [[yiidbActiveRecord::link()|link()]] становится ещё более очевидным, когда связь объявлена
посредством промежуточной таблицы. Например, вы можете использовать следующий код, чтобы связать
объект Order с объектом Item:

$order->link('items', $item);

Вышеприведённый код автоматически вставит строку данных в промежуточную таблицу order_item, чтобы связать объект
order с объектом item.

Info: Метод [[yiidbActiveRecord::link()|link()]] не осуществляет какую-либо валидацию данных во время
сохранения целевого объекта Active Record. На вас лежит ответственность за валидацию любых введённых данных перед
вызовом этого метода.

Существует противоположная операция для [[yiidbActiveRecord::link()|link()]] — это операция
[[yiidbActiveRecord::unlink()|unlink()]], она снимает существующую связь с двух объектов Active Record. Например:

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

По умолчанию метод [[yiidbActiveRecord::unlink()|unlink()]] задаст вторичному ключу (или ключам), который определяет
существующую связь, значение null. Однако вы можете запросить удаление строки таблицы, которая содержит значение
вторичного ключа, передав значение true в параметре $delete для этого метода.

Если связь построена на основе промежуточной таблицы, вызов метода [[yiidbActiveRecord::unlink()|unlink()]] инициирует
очистку вторичных ключей в промежуточной таблице, или же удаление соответствующей строки данных в промежуточной таблице,
если параметр $delete равен true.

Связывание объектов из разных баз данных

Active Record позволяет вам объявить связи между классами Active Record, которые относятся к разным базам данных. Базы
данных могут быть разных типов (например, MySQL и PostgreSQL или MS SQL и MongoDB), и они могут быть запущены на разных
серверах. Вы можете использовать тот же самый синтаксис для осуществления запросов выборки связных данных. Например:

// Объект Customer соответствует таблице "customer" в реляционной базе данных (например MySQL)
class Customer extends yiidbActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // у покупателя может быть много комментариев
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

// Объект Comment соответствует коллекции "comment" в базе данных MongoDB
class Comment extends yiimongodbActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // комментарий принадлежит одному покупателю
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

Вы можете использовать большую часть возможностей запросов получения связных данных, которые были описаны в этой главе.

Note: Применимость метода [[yiidbActiveQuery::joinWith()|joinWith()]] ограничена базами данных, которые
позволяют выполнять запросы между разными базами с использованием оператора JOIN. По этой причине вы не можете
использовать этот метод в вышеприведённом примере, т.к. MongoDB не поддерживает операцию JOIN.

Тонкая настройка классов Query

По умолчанию все запросы данных для Active Record поддерживаются с помощью класса [[yiidbActiveQuery]]. Для
использования собственного класса запроса вам необходимо переопределить метод [[yiidbActiveRecord::find()]] и
возвращать из него объект вашего собственного класса запроса. Например:

namespace appmodels;

use yiidbActiveRecord;
use yiidbActiveQuery;

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

class CommentQuery extends ActiveQuery
{
    // ...
}

Теперь, когда вы будете осуществлять получение данных (например, выполните find(), findOne()) или объявите связь
(например, hasOne()) с объектом Comment, вы будете работать с объектом класса CommentQuery вместо ActiveQuery.

Tip: В больших проектах рекомендуется использовать собственные классы запросов, которые будут содержать в себе
большую часть кода, связанного с настройкой запросов, таким образом классы Active Record удастся сохранить более
чистыми.

Вы можете настроить класс запроса большим количеством различных способов для улучшения методик построения запросов.
Например, можете объявить новые методы построения запросов в собственном классе запросов:

class CommentQuery extends ActiveQuery
{
    public function active($state = true)
    {
        return $this->andWhere(['active' => $state]);
    }
}

Note: Вместо вызова метода [[yiidbActiveQuery::where()|where()]] старайтесь во время объявления новых методов
построения запросов использовать [[yiidbActiveQuery::andWhere()|andWhere()]] или
[[yiidbActiveQuery::orWhere()|orWhere()]] для добавления дополнительных условий, в этом случае уже заданные условия
выборок не будут перезаписаны.

Это позволит вам писать код построения запросов как показано ниже:

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

Вы также можете использовать новые методы построения запросов, когда объявляете связи для класса Comment или
осуществляете запрос для выборки связных данных:

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

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

// или по-другому:
 
$customers = Customer::find()->with([
    'comments' => function($q) {
        $q->active();
    }
])->all();

Info: В Yii версии 1.1 была концепция с названием scope. Она больше не поддерживается в Yii версии 2.0, и вы
можете использовать собственные классы запросов и собственные методы построения запросов, чтобы добиться той же самой
цели.

Получение дополнительных атрибутов

Когда объект Active Record заполнен результатами запроса, его атрибуты заполнены значениями соответствующих столбцов
из полученного набора данных.

Вы можете получить дополнительные столбцы или значения с помощью запроса и сохранить их внутри объекта Active Record.
Например, предположим, что у нас есть таблица ‘room’, которая содержит информацию о доступных в отеле комнатах. Каждая
комната хранит информацию о её геометрических размерах с помощью атрибутов ‘length’, ‘width’, ‘height’. Представьте, что
вам требуется получить список всех доступных комнат, отсортированных по их объёму в порядке убывания. В этом случае вы
не можете вычислять объём с помощью PHP, потому что нам требуется сортировать записи по объёму, но вы также хотите
отображать объем в списке. Для достижения этой цели, вам необходимо объявить дополнительный атрибут в вашем Active
Record классе ‘Room’, который будет хранить значение ‘volume’:

class Room extends yiidbActiveRecord
{
    public $volume;

    // ...
}

Далее вам необходимо составить запрос, который вычисляет объём комнаты и выполняет сортировку:

$rooms = Room::find()
    ->select([
        '{{room}}.*', // получить все столбцы
        '([[length]] * [[width]] * [[height]]) AS volume', // вычислить объём
    ])
    ->orderBy('volume DESC') // отсортировать
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // содержит значение, вычисленное с помощью SQL-запроса
}

Возможность выбирать дополнительные атрибуты может быть особенно полезной для агрегирующих запросов. Представьте, что
вам необходимо отображать список покупателей с количеством их заказов. Прежде всего вам потребуется объявить класс
Customer со связью ‘orders’ и дополнительным атрибутом для хранения расчётов:

class Customer extends yiidbActiveRecord
{
    public $ordersCount;

    // ...

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

После этого вы сможете составить запрос, который объединяет заказы и вычисляет их количество:

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // получить все атрибуты покупателя
        'COUNT({{order}}.id) AS ordersCount' // вычислить количество заказов
    ])
    ->joinWith('orders') // обеспечить построение промежуточной таблицы
    ->groupBy('{{customer}}.id') // сгруппировать результаты, чтобы заставить агрегацию работать
    ->all();

Недостаток этого подхода заключается в том, что если данные для поля не загружены по результатам SQL запроса, то они
должны быть вычисленны отдельно. Это означает, что запись, полученная посредством обычного запроса без дополнительных полей в
разделе ‘select’, не может вернуть реальное значения для дополнительного поля. Это же касается и только что сохранненой
записи.

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

$room->volume; // значение будет равно `null`, т.к. поле не было заполнено

Использование магических методов [[yiidbBaseActiveRecord::__get()|__get()]] и [[yiidbBaseActiveRecord::__set()|__set()]]
позволяет эмулировать поведение обычного поля:

class Room extends yiidbActiveRecord
{
    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;
    }

    // ...
}

Если результат запроса на выборку данных не содержит поле ‘volume’, то модель сможет расчитать его автоматически
используя имеющиеся атрибуты.

Вы также можете вычислять агрегируемые поля используя объявленные отношения:

class Customer extends yiidbActiveRecord
{
    private $_ordersCount;

    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // нет смысла выполнять запрос на поиск по пустым ключам
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // вычисляем агрегацию по требованию из отношения
        }

        return $this->_ordersCount;
    }

    // ...

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

При такой реализации, в случае когда ‘ordersCount’ присутсвует в разделе ‘select’ — значение ‘Customer::ordersCount’ будет
заполнено из результатов запроса, в противном случае — оно будет вычислено по первому требованию на основании отношения Customer::orders.

Этот подход также можно использовать для быстрого доступа к некоторым данным отношений, в особенности для агрегации.
Например:

class Customer extends yiidbActiveRecord
{
    /**
     * Объявляет виртуальное свойство для агрегируемых данных, доступное только на чтение.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // нет смысла выполнять запрос на поиск по пустым ключам
        }

        return $this->ordersAggregation[0]['counted'];
    }

    /**
     * Объявляет обычное отношение 'orders'.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    /**
     * Объявляет новое отношение, основанное на 'orders', которое предоставляет агрегацию.
     */
    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; // выводит агрегируемые данные из отношения без дополнительного запроса благодаря жадной загрузке
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // выводит агрегируемые данные отношения через ленивую загрузку

Не могу понять почему не сохраняются данные в БД
При вызове метода save(false) у заполненной модели, создаются пустые поля в БД (хотя вроде как все правила для полей прописал):
Уж сижу несколько часов.

Контроллер (точнее один метод из него)

Код: Выделить всё

public function actionAddDriver(){

        $modelDriver = new Driver();

        $postRequest = Yii::$app->request->post();

        $isValidate = false;
        if($modelDriver->load($postRequest)){
            if($modelDriver->validate()){
                var_dump($modelDriver, true);
                $modelDriver->save(false);
                $isValidate = true;
            }
        }
        return $this->render('addDriver',['modelDriver'=>$modelDriver, 'isValidate'=>$isValidate]);
    }
 

Модель

Код: Выделить всё

<?php
namespace appmodels;

use yiidbActiveRecord;

class Driver extends ActiveRecord{

    public $name;
    public $lastname;
    public $patronymic;
    public $percent;

    public static function tableName(){
        return 'drivers';
    }

    public function getOrder(){
        return $this->hasMany(Order::className(),['driver_id'=>'id']);
    }

    public function attributeLabels(){
        return [
            'name'=>'Имя таксиста',
            'lastname'=>'Фамилия таксиста',
            'patronymic'=>'Отчество таксиста',
            'percent' => 'Процент'
        ];
    }

    public function rules(){
        return [
            [['name','percent'],'required'],
            [['name','lastname','patronymic'], 'string', 'length'=>[3,50]],
            ['percent','number'],
        ];
    }
}
 

Вид:

Код: Выделить всё

<?php
use yiiwidgetsActiveForm;
use yiihelpersHtml;
?>
    <?php if($isValidate):?>
        <div class="alert alert-success alert-dismissable">
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
            Запись добавлена!
        </div>
    <?php else:?>
        <div class="alert alert-warning alert-dismissable">
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
            ОШИБКА добавления!
        </div>
    <?php endif?>



<?php
$form = ActiveForm::begin(['id'=>'form-edit-driver','method'=>'post']);
    echo $form->field($modelDriver,'name')->textInput();
    echo $form->field($modelDriver,'lastname')->textInput();
    echo $form->field($modelDriver,'patronymic')->textInput();
    echo $form->field($modelDriver,'percent')->textInput();
    echo Html::submitButton('Добавить',['class'=>'btn btn-success']);
ActiveForm::end();

Даже не знаю с чем может быть проблема) знаю что из-за rules может не добавлять в БД, но вроде верно написал в правилах модели.

Active Record provides an object-oriented interface
for accessing and manipulating data stored in databases. An Active Record class is associated with a database table,
an Active Record instance corresponds to a row of that table, and an attribute of an Active Record
instance represents the value of a particular column in that row. Instead of writing raw SQL statements,
you would access Active Record attributes and call Active Record methods to access and manipulate the data stored
in database tables.

For example, assume Customer is an Active Record class which is associated with the customer table
and name is a column of the customer table. You can write the following code to insert a new
row into the customer table:

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

The above code is equivalent to using the following raw SQL statement for MySQL, which is less
intuitive, more error prone, and may even have compatibility problems if you are using a different kind of database:

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii provides the Active Record support for the following relational databases:

  • MySQL 4.1 or later: via [[yiidbActiveRecord]]
  • PostgreSQL 7.3 or later: via [[yiidbActiveRecord]]
  • SQLite 2 and 3: via [[yiidbActiveRecord]]
  • Microsoft SQL Server 2008 or later: via [[yiidbActiveRecord]]
  • Oracle: via [[yiidbActiveRecord]]
  • CUBRID 9.3 or later: via [[yiidbActiveRecord]] (Note that due to a bug in
    the cubrid PDO extension, quoting of values will not work, so you need CUBRID 9.3 as the client as well as the server)
  • Sphinx: via [[yiisphinxActiveRecord]], requires the yii2-sphinx extension
  • ElasticSearch: via [[yiielasticsearchActiveRecord]], requires the yii2-elasticsearch extension

Additionally, Yii also supports using Active Record with the following NoSQL databases:

  • Redis 2.6.12 or later: via [[yiiredisActiveRecord]], requires the yii2-redis extension
  • MongoDB 1.3.0 or later: via [[yiimongodbActiveRecord]], requires the yii2-mongodb extension

In this tutorial, we will mainly describe the usage of Active Record for relational databases.
However, most content described here are also applicable to Active Record for NoSQL databases.

Declaring Active Record Classes

To get started, declare an Active Record class by extending [[yiidbActiveRecord]]. Because each Active Record
class is associated with a database table, in this class you should override the [[yiidbActiveRecord::tableName()|tableName()]]
method to specify which table the class is associated with.

In the following example, we declare an Active Record class named Customer for the customer database table.

namespace appmodels;

use yiidbActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;

    /**
     * @return string the name of the table associated with this ActiveRecord class.
     */
    public static function tableName()
    {
        return 'customer';
    }
}

Active Record instances are considered as models. For this reason, we usually put Active Record
classes under the appmodels namespace (or other namespaces for keeping model classes).

Because [[yiidbActiveRecord]] extends from [[yiibaseModel]], it inherits all model features,
such as attributes, validation rules, data serialization, etc.

Connecting to Databases

By default, Active Record uses the db application component
as the [[yiidbConnection|DB connection]] to access and manipulate the database data. As explained in
Database Access Objects, you can configure the db component in the application configuration like shown
below,

return [
    'components' => [
        'db' => [
            'class' => 'yiidbConnection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

If you want to use a different database connection other than the db component, you should override
the [[yiidbActiveRecord::getDb()|getDb()]] method:

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // use the "db2" application component
        return Yii::$app->db2;  
    }
}

Querying Data

After declaring an Active Record class, you can use it to query data from the corresponding database table.
The process usually takes the following three steps:

  1. Create a new query object by calling the [[yiidbActiveRecord::find()]] method;
  2. Build the query object by calling query building methods;
  3. Call a query method to retrieve data in terms of Active Record instances.

As you can see, this is very similar to the procedure with query builder. The only difference
is that instead of using the new operator to create a query object, you call [[yiidbActiveRecord::find()]]
to return a new query object which is of class [[yiidbActiveQuery]].

Below are some examples showing how to use Active Query to query data:

// return a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// return all active customers and order them by their IDs
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// return the number of active customers
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// return all customers in an array indexed by customer IDs
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

In the above, $customer is a Customer object while $customers is an array of Customer objects. They are
all populated with the data retrieved from the customer table.

Info: Because [[yiidbActiveQuery]] extends from [[yiidbQuery]], you can use all query building methods and
query methods as described in the Section Query Builder.

Because it is a common task to query by primary key values or a set of column values, Yii provides two shortcut
methods for this purpose:

  • [[yiidbActiveRecord::findOne()]]: returns a single Active Record instance populated with the first row of the query result.
  • [[yiidbActiveRecord::findAll()]]: returns an array of Active Record instances populated with all query result.

Both methods can take one of the following parameter formats:

  • a scalar value: the value is treated as the desired primary key value to be looked for. Yii will determine
    automatically which column is the primary key column by reading database schema information.
  • an array of scalar values: the array is treated as the desired primary key values to be looked for.
  • an associative array: the keys are column names and the values are the corresponding desired column values to
    be looked for. Please refer to Hash Format for more details.

The following code shows how theses methods can be used:

// returns a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// returns customers whose ID is 100, 101, 123 or 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// returns an active customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// returns all inactive customers
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

Note: Neither [[yiidbActiveRecord::findOne()]] nor [[yiidbActiveQuery::one()]] will add LIMIT 1 to
the generated SQL statement. If your query may return many rows of data, you should call limit(1) explicitly
to improve the performance, e.g., Customer::find()->limit(1)->one().

Besides using query building methods, you can also write raw SQLs to query data and populate the results into
Active Record objects. You can do so by calling the [[yiidbActiveRecord::findBySql()]] method:

// returns all inactive customers
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Do not call extra query building methods after calling [[yiidbActiveRecord::findBySql()|findBySql()]] as they
will be ignored.

Accessing Data

As aforementioned, the data brought back from the database are populated into Active Record instances, and
each row of the query result corresponds to a single Active Record instance. You can access the column values
by accessing the attributes of the Active Record instances, for example,

// "id" and "email" are the names of columns in the "customer" table
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Note: The Active Record attributes are named after the associated table columns in a case-sensitive manner.
Yii automatically defines an attribute in Active Record for every column of the associated table.
You should NOT redeclare any of the attributes.

Because Active Record attributes are named after table columns, you may find you are writing PHP code like
$customer->first_name, which uses underscores to separate words in attribute names if your table columns are
named in this way. If you are concerned about code style consistency, you should rename your table columns accordingly
(to use camelCase, for example.)

Data Transformation

It often happens that the data being entered and/or displayed are in a format which is different from the one used in
storing the data in a database. For example, in the database you are storing customers’ birthdays as UNIX timestamps
(which is not a good design, though), while in most cases you would like to manipulate birthdays as strings in
the format of 'YYYY/MM/DD'. To achieve this goal, you can define data transformation methods in the Customer
Active Record class like the following:

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('Y/m/d', $this->birthday);
    }

    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

Now in your PHP code, instead of accessing $customer->birthday, you would access $customer->birthdayText, which
will allow you to input and display customer birthdays in the format of 'YYYY/MM/DD'.

Tip: The above example shows a generic way of transforming data in different formats. If you are working with
date values, you may use DateValidator and [[yiijuiDatePicker|DatePicker]],
which is easier to use and more powerful.

Retrieving Data in Arrays

While retrieving data in terms of Active Record objects is convenient and flexible, it is not always desirable
when you have to bring back a large amount of data due to the big memory footprint. In this case, you can retrieve
data using PHP arrays by calling [[yiidbActiveQuery::asArray()|asArray()]] before executing a query method:

// return all customers
// each customer is returned as an associative array
$customers = Customer::find()
    ->asArray()
    ->all();

Note: While this method saves memory and improves performance, it is closer to the lower DB abstraction layer
and you will lose most of the Active Record features. A very important distinction lies in the data type of
the column values. When you return data in Active Record instances, column values will be automatically typecast
according to the actual column types; on the other hand when you return data in arrays, column values will be
strings (since they are the result of PDO without any processing), regardless their actual column types.

Retrieving Data in Batches

In Query Builder, we have explained that you may use batch query to minimize your memory
usage when querying a large amount of data from the database. You may use the same technique in Active Record. For example,

// fetch 10 customers at a time
foreach (Customer::find()->batch(10) as $customers) {
    // $customers is an array of 10 or fewer Customer objects
}

// fetch 10 customers at a time and iterate them one by one
foreach (Customer::find()->each(10) as $customer) {
    // $customer is a Customer object
}

// batch query with eager loading
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer is a Customer object
}

Saving Data

Using Active Record, you can easily save data to database by taking the following steps:

  1. Prepare an Active Record instance
  2. Assign new values to Active Record attributes
  3. Call [[yiidbActiveRecord::save()]] to save the data into database.

For example,

// insert a new row of data
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// update an existing row of data
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

The [[yiidbActiveRecord::save()|save()]] method can either insert or update a row of data, depending on the state
of the Active Record instance. If the instance is newly created via the new operator, calling
[[yiidbActiveRecord::save()|save()]] will cause insertion of a new row; If the instance is the result of a query method,
calling [[yiidbActiveRecord::save()|save()]] will update the row associated with the instance.

You can differentiate the two states of an Active Record instance by checking its
[[yiidbActiveRecord::isNewRecord|isNewRecord]] property value. This property is also used by
[[yiidbActiveRecord::save()|save()]] internally as follows:

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

Tip: You can call [[yiidbActiveRecord::insert()|insert()]] or [[yiidbActiveRecord::update()|update()]]
directly to insert or update a row.

Data Validation

Because [[yiidbActiveRecord]] extends from [[yiibaseModel]], it shares the same data validation feature.
You can declare validation rules by overriding the [[yiidbActiveRecord::rules()|rules()]] method and perform
data validation by calling the [[yiidbActiveRecord::validate()|validate()]] method.

When you call [[yiidbActiveRecord::save()|save()]], by default it will call [[yiidbActiveRecord::validate()|validate()]]
automatically. Only when the validation passes, will it actually save the data; otherwise it will simply return false,
and you can check the [[yiidbActiveRecord::errors|errors]] property to retrieve the validation error messages.

Tip: If you are certain that your data do not need validation (e.g., the data comes from trustable sources),
you can call save(false) to skip the validation.

Massive Assignment

Like normal models, Active Record instances also enjoy the massive assignment feature.
Using this feature, you can assign values to multiple attributes of an Active Record instance in a single PHP statement,
like shown below. Do remember that only safe attributes can be massively assigned, though.

$values = [
    'name' => 'James',
    'email' => 'james@example.com',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

Updating Counters

It is a common task to increment or decrement a column in a database table. We call such columns as counter columns.
You can use [[yiidbActiveRecord::updateCounters()|updateCounters()]] to update one or multiple counter columns.
For example,

$post = Post::findOne(100);

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

Note: If you use [[yiidbActiveRecord::save()]] to update a counter column, you may end up with inaccurate result,
because it is likely the same counter is being saved by multiple requests which read and write the same counter value.

Dirty Attributes

When you call [[yiidbActiveRecord::save()|save()]] to save an Active Record instance, only dirty attributes
are being saved. An attribute is considered dirty if its value has been modified since it was loaded from DB or
saved to DB most recently. Note that data validation will be performed regardless if the Active Record
instance has dirty attributes or not.

Active Record automatically maintains the list of dirty attributes. It does so by maintaining an older version of
the attribute values and comparing them with the latest one. You can call [[yiidbActiveRecord::getDirtyAttributes()]]
to get the attributes that are currently dirty. You can also call [[yiidbActiveRecord::markAttributeDirty()]]
to explicitly mark an attribute as dirty.

If you are interested in the attribute values prior to their most recent modification, you may call
[[yiidbActiveRecord::getOldAttributes()|getOldAttributes()]] or [[yiidbActiveRecord::getOldAttribute()|getOldAttribute()]].

Note: The comparison of old and new values will be done using the === operator so a value will be considered dirty
even if it has the same value but a different type. This is often the case when the model receives user input from
HTML forms where every value is represented as a string.
To ensure the correct type for e.g. integer values you may apply a validation filter:
['attributeName', 'filter', 'filter' => 'intval'].

Default Attribute Values

Some of your table columns may have default values defined in the database. Sometimes, you may want to pre-populate your
Web form for an Active Record instance with these default values. To avoid writing the same default values again,
you can call [[yiidbActiveRecord::loadDefaultValues()|loadDefaultValues()]] to populate the DB-defined default values
into the corresponding Active Record attributes:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz will be assigned the default value declared when defining the "xyz" column

Updating Multiple Rows

The methods described above all work on individual Active Record instances, causing inserting or updating of individual
table rows. To update multiple rows simultaneously, you should call [[yiidbActiveRecord::updateAll()|updateAll()]], instead,
which is a static method.

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

Similarly, you can call [[yiidbActiveRecord::updateAllCounters()|updateAllCounters()]] to update counter columns of
multiple rows at the same time.

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

Deleting Data

To delete a single row of data, first retrieve the Active Record instance corresponding to that row and then call
the [[yiidbActiveRecord::delete()]] method.

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

You can call [[yiidbActiveRecord::deleteAll()]] to delete multiple or all rows of data. For example,

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

Note: Be very careful when calling [[yiidbActiveRecord::deleteAll()|deleteAll()]] because it may totally
erase all data from your table if you make a mistake in specifying the condition.

Active Record Life Cycles

It is important to understand the life cycles of Active Record when it is used for different purposes.
During each life cycle, a certain sequence of methods will be invoked, and you can override these methods
to get a chance to customize the life cycle. You can also respond to certain Active Record events triggered
during a life cycle to inject your custom code. These events are especially useful when you are developing
Active Record behaviors which need to customize Active Record life cycles.

In the following, we will summarize various Active Record life cycles and the methods/events that are involved
in the life cycles.

New Instance Life Cycle

When creating a new Active Record instance via the new operator, the following life cycle will happen:

  1. class constructor;
  2. [[yiidbActiveRecord::init()|init()]]: triggers an [[yiidbActiveRecord::EVENT_INIT|EVENT_INIT]] event.

Querying Data Life Cycle

When querying data through one of the querying methods, each newly populated Active Record will
undergo the following life cycle:

  1. class constructor.
  2. [[yiidbActiveRecord::init()|init()]]: triggers an [[yiidbActiveRecord::EVENT_INIT|EVENT_INIT]] event.
  3. [[yiidbActiveRecord::afterFind()|afterFind()]]: triggers an [[yiidbActiveRecord::EVENT_AFTER_FIND|EVENT_AFTER_FIND]] event.

Saving Data Life Cycle

When calling [[yiidbActiveRecord::save()|save()]] to insert or update an Active Record instance, the following
life cycle will happen:

  1. [[yiidbActiveRecord::beforeValidate()|beforeValidate()]]: triggers
    an [[yiidbActiveRecord::EVENT_BEFORE_VALIDATE|EVENT_BEFORE_VALIDATE]] event. If the method returns false
    or [[yiibaseModelEvent::isValid]] is false, the rest of the steps will be skipped.
  2. Performs data validation. If data validation fails, the steps after Step 3 will be skipped.
  3. [[yiidbActiveRecord::afterValidate()|afterValidate()]]: triggers
    an [[yiidbActiveRecord::EVENT_AFTER_VALIDATE|EVENT_AFTER_VALIDATE]] event.
  4. [[yiidbActiveRecord::beforeSave()|beforeSave()]]: triggers
    an [[yiidbActiveRecord::EVENT_BEFORE_INSERT|EVENT_BEFORE_INSERT]]
    or [[yiidbActiveRecord::EVENT_BEFORE_UPDATE|EVENT_BEFORE_UPDATE]] event. If the method returns false
    or [[yiibaseModelEvent::isValid]] is false, the rest of the steps will be skipped.
  5. Performs the actual data insertion or updating;
  6. [[yiidbActiveRecord::afterSave()|afterSave()]]: triggers
    an [[yiidbActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]]
    or [[yiidbActiveRecord::EVENT_AFTER_UPDATE|EVENT_AFTER_UPDATE]] event.

Deleting Data Life Cycle

When calling [[yiidbActiveRecord::delete()|delete()]] to delete an Active Record instance, the following
life cycle will happen:

  1. [[yiidbActiveRecord::beforeDelete()|beforeDelete()]]: triggers
    an [[yiidbActiveRecord::EVENT_BEFORE_DELETE|EVENT_BEFORE_DELETE]] event. If the method returns false
    or [[yiibaseModelEvent::isValid]] is false, the rest of the steps will be skipped.
  2. perform the actual data deletion
  3. [[yiidbActiveRecord::afterDelete()|afterDelete()]]: triggers
    an [[yiidbActiveRecord::EVENT_AFTER_DELETE|EVENT_AFTER_DELETE]] event.

Note: Calling any of the following methods will NOT initiate any of the above life cycles:

  • [[yiidbActiveRecord::updateAll()]]
  • [[yiidbActiveRecord::deleteAll()]]
  • [[yiidbActiveRecord::updateCounters()]]
  • [[yiidbActiveRecord::updateAllCounters()]]

Working with Transactions

There are two ways of using transactions while working with Active Record.

The first way is to explicitly enclose Active Record method calls in a transactional block, like shown below,

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
});

// or alternatively

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
    $transaction->commit();
} catch(Exception $e) {
    $transaction->rollBack();
    throw $e;
}

The second way is to list the DB operations that require transactional support in the [[yiidbActiveRecord::transactions()]]
method. For example,

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // the above is equivalent to the following:
            // 'api' => self::OP_ALL,
        ];
    }
}

The [[yiidbActiveRecord::transactions()]] method should return an array whose keys are scenario
names and values the corresponding operations that should be enclosed within transactions. You should use the following
constants to refer to different DB operations:

  • [[yiidbActiveRecord::OP_INSERT|OP_INSERT]]: insertion operation performed by [[yiidbActiveRecord::insert()|insert()]];
  • [[yiidbActiveRecord::OP_UPDATE|OP_UPDATE]]: update operation performed by [[yiidbActiveRecord::update()|update()]];
  • [[yiidbActiveRecord::OP_DELETE|OP_DELETE]]: deletion operation performed by [[yiidbActiveRecord::delete()|delete()]].

Use | operators to concatenate the above constants to indicate multiple operations. You may also use the shortcut
constant [[yiidbActiveRecord::OP_ALL|OP_ALL]] to refer to all three operations above.

Optimistic Locks

Optimistic locking is a way to prevent conflicts that may occur when a single row of data is being
updated by multiple users. For example, both user A and user B are editing the same wiki article
at the same time. After user A saves his edits, user B clicks on the «Save» button in an attempt to
save his edits as well. Because user B was actually working on an outdated version of the article,
it would be desirable to have a way to prevent him from saving the article and show him some hint message.

Optimistic locking solves the above problem by using a column to record the version number of each row.
When a row is being saved with an outdated version number, a [[yiidbStaleObjectException]] exception
will be thrown, which prevents the row from being saved. Optimistic locking is only supported when you
update or delete an existing row of data using [[yiidbActiveRecord::update()]] or [[yiidbActiveRecord::delete()]],
respectively.

To use optimistic locking,

  1. Create a column in the DB table associated with the Active Record class to store the version number of each row.
    The column should be of big integer type (in MySQL it would be BIGINT DEFAULT 0).
  2. Override the [[yiidbActiveRecord::optimisticLock()]] method to return the name of this column.
  3. In the Web form that takes user inputs, add a hidden field to store the current version number of the row being updated. Be sure your version attribute has input validation rules and validates successfully.
  4. In the controller action that updates the row using Active Record, try and catch the [[yiidbStaleObjectException]]
    exception. Implement necessary business logic (e.g. merging the changes, prompting staled data) to resolve the conflict.

For example, assume the version column is named as version. You can implement optimistic locking with the code like
the following.

// ------ view code -------

use yiihelpersHtml;

// ...other input fields
echo Html::activeHiddenInput($model, 'version');


// ------ controller code -------

use yiidbStaleObjectException;

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) {
        // logic to resolve the conflict
    }
}

Working with Relational Data

Besides working with individual database tables, Active Record is also capable of bringing together related data,
making them readily accessible through the primary data. For example, the customer data is related with the order
data because one customer may have placed one or multiple orders. With appropriate declaration of this relation,
you may be able to access a customer’s order information using the expression $customer->orders which gives
back the customer’s order information in terms of an array of Order Active Record instances.

Declaring Relations

To work with relational data using Active Record, you first need to declare relations in Active Record classes.
The task is as simple as declaring a relation method for every interested relation, like the following,

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

In the above code, we have declared an orders relation for the Customer class, and a customer relation
for the Order class.

Each relation method must be named as getXyz. We call xyz (the first letter is in lower case) the relation name.
Note that relation names are case sensitive.

While declaring a relation, you should specify the following information:

  • the multiplicity of the relation: specified by calling either [[yiidbActiveRecord::hasMany()|hasMany()]]
    or [[yiidbActiveRecord::hasOne()|hasOne()]]. In the above example you may easily read in the relation
    declarations that a customer has many orders while an order only has one customer.
  • the name of the related Active Record class: specified as the first parameter to
    either [[yiidbActiveRecord::hasMany()|hasMany()]] or [[yiidbActiveRecord::hasOne()|hasOne()]].
    A recommended practice is to call Xyz::className() to get the class name string so that you can receive
    IDE auto-completion support as well as error detection at compiling stage.
  • the link between the two types of data: specifies the column(s) through which the two types of data are related.
    The array values are the columns of the primary data (represented by the Active Record class that you are declaring
    relations), while the array keys are the columns of the related data.

An easy rule to remember this is, as you see in the example above, you write the column that belongs to the related
Active Record directly next to it. You see there that customer_id is a property of Order and id is a property
of Customer.

Accessing Relational Data

After declaring relations, you can access relational data through relation names. This is just like accessing
an object property defined by the relation method. For this reason, we call it relation property.
For example,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders is an array of Order objects
$orders = $customer->orders;

Info: When you declare a relation named xyz via a getter method getXyz(), you will be able to access
xyz like an object property. Note that the name is case sensitive.

If a relation is declared with [[yiidbActiveRecord::hasMany()|hasMany()]], accessing this relation property
will return an array of the related Active Record instances; if a relation is declared with
[[yiidbActiveRecord::hasOne()|hasOne()]], accessing the relation property will return the related
Active Record instance or null if no related data is found.

When you access a relation property for the first time, a SQL statement will be executed, like shown in the
above example. If the same property is accessed again, the previous result will be returned without re-executing
the SQL statement. To force re-executing the SQL statement, you should unset the relation property
first: unset($customer->orders).

Note: While this concept looks similar to the object property feature, there is an
important difference. For normal object properties the property value is of the same type as the defining getter method.
A relation method however returns an [[yiidbActiveQuery]] instance, while accessing a relation property will either
return a [[yiidbActiveRecord]] instance or an array of these.

php
$customer->orders; // is an array of `Order` objects
$customer->getOrders(); // returns an ActiveQuery instance

This is useful for creating customized queries, which is described in the next section.

Dynamic Relational Query

Because a relation method returns an instance of [[yiidbActiveQuery]], you can further build this query
using query building methods before performing DB query. For example,

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

Unlike accessing a relation property, each time you perform a dynamic relational query via a relation method,
a SQL statement will be executed, even if the same dynamic relational query was performed before.

Sometimes you may even want to parametrize a relation declaration so that you can more easily perform
dynamic relational query. For example, you may declare a bigOrders relation as follows,

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

Then you will be able to perform the following relational queries:

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

Relations via a Junction Table

In database modelling, when the multiplicity between two related tables is many-to-many,
a junction table is usually introduced. For example, the order
table and the item table may be related via a junction table named order_item. One order will then correspond
to multiple order items, while one product item will also correspond to multiple order items.

When declaring such relations, you would call either [[yiidbActiveQuery::via()|via()]] or [[yiidbActiveQuery::viaTable()|viaTable()]]
to specify the junction table. The difference between [[yiidbActiveQuery::via()|via()]] and [[yiidbActiveQuery::viaTable()|viaTable()]]
is that the former specifies the junction table in terms of an existing relation name while the latter directly
the junction table. For example,

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

or alternatively,

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

The usage of relations declared with a junction table is the same as that of normal relations. For example,

// 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 (...)
// returns an array of Item objects
$items = $order->items;

Lazy Loading and Eager Loading

In Accessing Relational Data, we explained that you can access a relation property
of an Active Record instance like accessing a normal object property. A SQL statement will be executed only when
you access the relation property the first time. We call such relational data accessing method lazy loading.
For example,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// no SQL executed
$orders2 = $customer->orders;

Lazy loading is very convenient to use. However, it may suffer from a performance issue when you need to access
the same relation property of multiple Active Record instances. Consider the following code example. How many
SQL statements will be executed?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

As you can see from the code comment above, there are 101 SQL statements being executed! This is because each
time you access the orders relation property of a different Customer object in the for-loop, a SQL statement
will be executed.

To solve this performance problem, you can use the so-called eager loading approach as shown below,

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // no SQL executed
    $orders = $customer->orders;
}

By calling [[yiidbActiveQuery::with()]], you instruct Active Record to bring back the orders for the first 100
customers in one single SQL statement. As a result, you reduce the number of the executed SQL statements from 101 to 2!

You can eagerly load one or multiple relations. You can even eagerly load nested relations. A nested relation is a relation
that is declared within a related Active Record class. For example, Customer is related with Order through the orders
relation, and Order is related with Item through the items relation. When querying for Customer, you can eagerly
load items using the nested relation notation orders.items.

The following code shows different usage of [[yiidbActiveQuery::with()|with()]]. We assume the Customer class
has two relations orders and country, while the Order class has one relation items.

// eager loading both "orders" and "country"
$customers = Customer::find()->with('orders', 'country')->all();
// equivalent to the array syntax below
$customers = Customer::find()->with(['orders', 'country'])->all();
// no SQL executed 
$orders= $customers[0]->orders;
// no SQL executed 
$country = $customers[0]->country;

// eager loading "orders" and the nested relation "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// access the items of the first order of the first customer
// no SQL executed
$items = $customers[0]->orders[0]->items;

You can eagerly load deeply nested relations, such as a.b.c.d. All parent relations will be eagerly loaded.
That is, when you call [[yiidbActiveQuery::with()|with()]] using a.b.c.d, you will eagerly load
a, a.b, a.b.c and a.b.c.d.

Info: In general, when eagerly loading N relations among which M relations are defined with a
junction table, a total number of N+M+1 SQL statements will be executed.
Note that a nested relation a.b.c.d counts as 4 relations.

When eagerly loading a relation, you can customize the corresponding relational query using an anonymous function.
For example,

// find customers and bring back together their country and active orders
// 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();

When customizing the relational query for a relation, you should specify the relation name as an array key
and use an anonymous function as the corresponding array value. The anonymous function will receive a $query parameter
which represents the [[yiidbActiveQuery]] object used to perform the relational query for the relation.
In the code example above, we are modifying the relational query by appending an additional condition about order status.

Note: If you call [[yiidbQuery::select()|select()]] while eagerly loading relations, you have to make sure
the columns referenced in the relation declarations are being selected. Otherwise, the related models may not
be loaded properly. For example,

php
$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer is always null. To fix the problem, you should do the following:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

Joining with Relations

Note: The content described in this subsection is only applicable to relational databases, such as
MySQL, PostgreSQL, etc.

The relational queries that we have described so far only reference the primary table columns when
querying for the primary data. In reality we often need to reference columns in the related tables. For example,
we may want to bring back the customers who have at least one active order. To solve this problem, we can
build a join query like the following:

// 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: It is important to disambiguate column names when building relational queries involving JOIN SQL statements.
A common practice is to prefix column names with their corresponding table names.

However, a better approach is to exploit the existing relation declarations by calling [[yiidbActiveQuery::joinWith()]]:

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

Both approaches execute the same set of SQL statements. The latter approach is much cleaner and drier, though.

By default, [[yiidbActiveQuery::joinWith()|joinWith()]] will use LEFT JOIN to join the primary table with the
related table. You can specify a different join type (e.g. RIGHT JOIN) via its third parameter $joinType. If
the join type you want is INNER JOIN, you can simply call [[yiidbActiveQuery::innerJoinWith()|innerJoinWith()]], instead.

Calling [[yiidbActiveQuery::joinWith()|joinWith()]] will eagerly load the related data by default.
If you do not want to bring in the related data, you can specify its second parameter $eagerLoading as false.

Like [[yiidbActiveQuery::with()|with()]], you can join with one or multiple relations; you may customize the relation
queries on-the-fly; you may join with nested relations; and you may mix the use of [[yiidbActiveQuery::with()|with()]]
and [[yiidbActiveQuery::joinWith()|joinWith()]]. For example,

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

Sometimes when joining two tables, you may need to specify some extra conditions in the ON part of the JOIN query.
This can be done by calling the [[yiidbActiveQuery::onCondition()]] method like the following:

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

This above query brings back all customers, and for each customer it brings back all active orders.
Note that this differs from our earlier example which only brings back customers who have at least one active order.

Info: When [[yiidbActiveQuery]] is specified with a condition via [[yiidbActiveQuery::onCondition()|onCondition()]],
the condition will be put in the ON part if the query involves a JOIN query. If the query does not involve
JOIN, the on-condition will be automatically appended to the WHERE part of the query.

Inverse Relations

Relation declarations are often reciprocal between two Active Record classes. For example, Customer is related
to Order via the orders relation, and Order is related back to Customer via the customer relation.

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

Now consider the following piece of code:

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

// displays "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

We would think $customer and $customer2 are the same, but they are not! Actually they do contain the same
customer data, but they are different objects. When accessing $order->customer, an extra SQL statement
is executed to populate a new object $customer2.

To avoid the redundant execution of the last SQL statement in the above example, we should tell Yii that
customer is an inverse relation of orders by calling the [[yiidbActiveQuery::inverseOf()|inverseOf()]] method
like shown below:

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

With this modified relation declaration, we will have:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// No SQL will be executed
$customer2 = $order->customer;

// displays "same"
echo $customer2 === $customer ? 'same' : 'not the same';

Note: Inverse relations cannot be defined for relations involving a junction table.
That is, if a relation is defined with [[yiidbActiveQuery::via()|via()]] or [[yiidbActiveQuery::viaTable()|viaTable()]],
you should not call [[yiidbActiveQuery::inverseOf()|inverseOf()]] further.

Saving Relations

When working with relational data, you often need to establish relationships between different data or destroy
existing relationships. This requires setting proper values for the columns that define the relations. Using Active Record,
you may end up writing the code like the following:

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

// setting the attribute that defines the "customer" relation in Order
$order->customer_id = $customer->id;
$order->save();

Active Record provides the [[yiidbActiveRecord::link()|link()]] method that allows you to accomplish this task more nicely:

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

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

The [[yiidbActiveRecord::link()|link()]] method requires you to specify the relation name and the target Active Record
instance that the relationship should be established with. The method will modify the values of the attributes that
link two Active Record instances and save them to the database. In the above example, it will set the customer_id
attribute of the Order instance to be the value of the id attribute of the Customer instance and then save it
to the database.

Note: You cannot link two newly created Active Record instances.

The benefit of using [[yiidbActiveRecord::link()|link()]] is even more obvious when a relation is defined via
a junction table. For example, you may use the following code to link an Order instance
with an Item instance:

$order->link('items', $item);

The above code will automatically insert a row in the order_item junction table to relate the order with the item.

Info: The [[yiidbActiveRecord::link()|link()]] method will NOT perform any data validation while
saving the affected Active Record instance. It is your responsibility to validate any input data before
calling this method.

The opposite operation to [[yiidbActiveRecord::link()|link()]] is [[yiidbActiveRecord::unlink()|unlink()]]
which breaks an existing relationship between two Active Record instances. For example,

$customer = Customer::find()->with('orders')->all();
$customer->unlink('orders', $customer->orders[0]);

By default, the [[yiidbActiveRecord::unlink()|unlink()]] method will set the foreign key value(s) that specify
the existing relationship to be null. You may, however, choose to delete the table row that contains the foreign key value
by passing the $delete parameter as true to the method.

When a junction table is involved in a relation, calling [[yiidbActiveRecord::unlink()|unlink()]] will cause
the foreign keys in the junction table to be cleared, or the deletion of the corresponding row in the junction table
if $delete is true.

Cross-Database Relations

Active Record allows you to declare relations between Active Record classes that are powered by different databases.
The databases can be of different types (e.g. MySQL and PostgreSQL, or MS SQL and MongoDB), and they can run on
different servers. You can use the same syntax to perform relational queries. For example,

// Customer is associated with the "customer" table in a relational database (e.g. MySQL)
class Customer extends yiidbActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // a customer has many comments
        return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    }
}

// Comment is associated with the "comment" collection in a MongoDB database
class Comment extends yiimongodbActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // a comment has one customer
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

You can use most of the relational query features that have been described in this section.

Note: Usage of [[yiidbActiveQuery::joinWith()|joinWith()]] is limited to databases that allow cross-database JOIN queries.
For this reason, you cannot use this method in the above example because MongoDB does not support JOIN.

Customizing Query Classes

By default, all Active Record queries are supported by [[yiidbActiveQuery]]. To use a customized query class
in an Active Record class, you should override the [[yiidbActiveRecord::find()]] method and return an instance
of your customized query class. For example,

namespace appmodels;

use yiidbActiveRecord;
use yiidbActiveQuery;

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

class CommentQuery extends ActiveQuery
{
    // ...
}

Now whenever you are performing a query (e.g. find(), findOne()) or defining a relation (e.g. hasOne())
with Comment, you will be working with an instance of CommentQuery instead of ActiveQuery.

Tip: In big projects, it is recommended that you use customized query classes to hold most query-related code
so that the Active Record classes can be kept clean.

You can customize a query class in many creative ways to improve your query building experience. For example,
you can define new query building methods in a customized query class:

class CommentQuery extends ActiveQuery
{
    public function active($state = true)
    {
        return $this->andWhere(['active' => $state]);
    }
}

Note: Instead of calling [[yiidbActiveQuery::where()|where()]], you usually should call
[[yiidbActiveQuery::andWhere()|andWhere()]] or [[yiidbActiveQuery::orWhere()|orWhere()]] to append additional
conditions when defining new query building methods so that any existing conditions are not overwritten.

This allows you to write query building code like the following:

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

You can also use the new query building methods when defining relations about Comment or performing relational query:

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

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

// or alternatively

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

Info: In Yii 1.1, there is a concept called scope. Scope is no longer directly supported in Yii 2.0,
and you should use customized query classes and query methods to achieve the same goal.

When Active Record instance is populated from query results, its attributes are filled up by corresponding column
values from received data set.

You are able to fetch additional columns or values from query and store it inside the Active Record.
For example, assume we have a table named ‘room’, which contains information about rooms available in the hotel.
Each room stores information about its geometrical size using fields ‘length’, ‘width’, ‘height’.
Imagine we need to retrieve list of all available rooms with their volume in descendant order.
So you can not calculate volume using PHP, because we need to sort the records by its value, but you also want ‘volume’
to be displayed in the list.
To achieve the goal, you need to declare an extra field in your ‘Room’ Active Record class, which will store ‘volume’ value:

class Room extends yiidbActiveRecord
{
    public $volume;

    // ...
}

Then you need to compose a query, which calculates volume of the room and performs the sort:

$rooms = Room::find()
    ->select([
        '{{room}}.*', // select all columns
        '([[length]] * [[width]].* [[height]]) AS volume', // calculate a volume
    ])
    ->orderBy('volume DESC') // apply sort
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // contains value calculated by SQL
}

Ability to select extra fields can be exceptionally useful for aggregation queries.
Assume you need to display a list of customers with the count of orders they have made.
First of all, you need to declare a Customer class with ‘orders’ relation and extra field for count storage:

class Customer extends yiidbActiveRecord
{
    public $ordersCount;

    // ...

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

Then you can compose a query, which joins the orders and calculates their count:

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // select all customer fields
        'COUNT({{order}}.id) AS ordersCount' // calculate orders count
    ])
    ->joinWith('orders') // ensure table junction
    ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
    ->all();

Содержание

  1. Validation is passed, but $model->errors returns an array of error messages #9273
  2. Comments
  3. Yii2 model save error
  4. Комментарии RSS по email OK
  5. Active Record ¶
  6. Объявление классов Active Record ¶
  7. Настройка имени таблицы ¶
  8. Классы Active record называются «моделями» ¶
  9. Подключение к базам данных ¶
  10. Получение данных ¶
  11. Доступ к данным ¶
  12. Преобразование данных ¶
  13. Получение данных в виде массива ¶
  14. Пакетное получение данных ¶
  15. Сохранение данных ¶
  16. Валидация данных ¶
  17. Массовое присваивание ¶
  18. Обновление счётчиков ¶
  19. Dirty-атрибуты ¶
  20. Значения атрибутов по умолчанию ¶
  21. Приведение типов атрибутов ¶
  22. JSON в MySQL и PostgreSQL ¶
  23. Массивы в PostgreSQL ¶
  24. Обновление нескольких строк данных ¶
  25. Удаление данных ¶
  26. Жизненные циклы Active Record ¶
  27. Жизненный цикл создания нового объекта ¶
  28. Жизненный цикл получения данных ¶
  29. Жизненный цикл сохранения данных ¶
  30. Жизненный цикл удаления данных ¶
  31. Работа с транзакциями ¶
  32. Оптимистическая блокировка ¶
  33. Работа со связными данными ¶
  34. Объявление связей ¶
  35. Доступ к связным данным ¶
  36. Динамические запросы связных данных ¶
  37. Связывание посредством промежуточной таблицы ¶
  38. Отложенная и жадная загрузка ¶
  39. Использование JOIN со связями ¶
  40. Псевдонимы связанных таблиц ¶
  41. Обратные связи ¶
  42. Сохранение связных данных ¶
  43. Связывание объектов из разных баз данных ¶
  44. Тонкая настройка классов Query ¶
  45. Получение дополнительных атрибутов ¶

Validation is passed, but $model->errors returns an array of error messages #9273

I encountered a tricky bug when did file uploading. The problem is that, when form validation is passed, $model->save() returns false, and $model->getErrors() returns an array with one error message regarding file input only.

And the scenario, where its used is the following:

There are 3 columns in a table (id, name, cover) , and this is a very basic photo-album. The validation rules for this are pretty simple: when a user uploads a new photo, he must fill both name ( input type=»text» ) and cover ( input type=»file» ) fields. But when editing some existing record, he’s allowed not to upload a file, since that’s optional.

Pretty simple, right?

Now consider the relevant parts:

After filling both name and file fields in the browser manually, Yii’s client-side validator highlights all fields green, which means that validation is passed successfully.

Here come the dragons now (after submitting successfully validated form):

This bug occurs only in «Add form», while it works in «Edit form». When I remove the file input everything works as expected, so that must be its own issue.

The environment itself, if that matters in this case:

  • PHP 5.6
  • Apache 2.2
  • Yii 2.0 (downloaded that version 2 months ago)
  • Tested on Windows/Linux, and all browsers

The text was updated successfully, but these errors were encountered:

Источник

Yii2 model save error

Те, кто раньше не работал с Yii, на первых порах часто совершают очень нехорошую ошибку:

Вот такой, казалось бы, простой код убить может немало нервов. Дело в том, что save запросто может не сработать, если не выполнится правило валидации. А т.к. мы этого не проверили, произойдёт редирект без каких-либо признаков ошибки.

У меня проще, много раз об этом писал, но всем показалось неудобным 🙂 :

За метод saved отвечает поведение — EFormModelBehavior

Тут вообще вариаций много. Я предпочитаю вызывать validate сперва, да и в общем-то Gii так генерит. Иногда до валидации по событию что-то сделать надо или после валидации, а потом уже и сам save делаю. Местами вообще всё в транзакции занесено.

Но основная ошибка типична и многие натыкаются.

Вообще, по хорошему, при не прохождении валидации должно кидаться исключение, жалко что изначально в Yii так не реализовано.

Serge, непрохождение валидации — это вполне штатная ситуация. В большинстве случаев необходимо выводить ошибки в форму, а не кидать исключение, поэтому try-catch вместо if сильно раздули бы код и сделали бы его не очень красивым.

2Sam: Я говорил о том моменте когда пользователь пытается сохранить модель не прошедшую валидацию в БД, именно тогда нужно кидать исключение, в других случаях я соглашусь что кидание исключения будет излишним. 🙂

2Serge, В корне не согласен. Никто не мешает подключить поведение типа:

Тем самым мы убираем еще один if и сворачиваем все в конструкцию:

Так что если не устраивает вариант по умолчанию, выше я описал альтернативу для избежания подобных ошибок.

2Ekstazi: Внесу немного ясности про моё высказывание про исключение. Оно должно кидаться только тогда, когда вызван метод save() и модель не валидна! Это позволит избежать ошибки описанной в данной статье. И действительно жаль что такое не реализованно по умолчанию в модели Yii

А вы разве не знаете что save еще и валидацию делает ?

Поддержу Serge. Мне должно быть абсолютно пофиг, делает save валидацию, или не делает — это же реализация метода, инкапсулированная логика. То, что save возвращает false — не говорит о характере ошибки, и в этом есть запах дурного кода. Код бизнес логики должен быть скрыт за интерфейсом. Обработка ошибок через исключения — часть этого интерфейса. Безусловно, паттерн Валидатор чрезвычайно удобен. Когда мы выполняет явную проверку типа isValid, то естественно ожидается boolean. Но когда я сохраняю объект, то я ничего не ожидаю. При любой ошибке сохранения желателен Exception, так как уровней вложенности сохранения может быть очень много при развитом домене с большим числом relations. В случае ifов будет настоящий говнокод, везде придется учитывать, что же вернул save. А мог бы быть один блок try catch на всю транзакцию. Exception объясняет, что может произойти при исполнении save, это хорошая документация.

Если выполняется проверка — ожидается bool как результат. Если произошла ошибка выполнения — exception. Работа save — не проверка, следовательно более правильным считаю exception

Я работаю над проектом legacy, все ошибки на if . Без слез не взглянешь на это.

Помогите в топике пожалуйста http://www.yiiframework.com/forum/index.php?/topic/19386-%d0%bf%d0%be%d0%bb%d1%83%d1%87%d0%b5%d0%bd%d0%b8%d0%b5-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85-%d1%81%d0%b2%d1%8f%d0%b7%d0%b0%d0%bd%d0%bd%d0%be%d0%b9-%d0%bc%d0%be%d0%b4%d0%b5%d0%bb%d0%b8-many-many-%d0%b2-cactivedataprovider/

А как такое может быть, что была ошибка валидации, а $model->errors пустой? Это я про Yii 2 спрашиваю .

Виталий, лучше с вопросами на форум yiiframework.ru.

You have to use try-catch anyway, because PDO may throw exception.

Правильно ли я понимаю, проверять save() необходимо также и по причине «а вставились ли вообще данные в таблицу»?

Источник

Active Record ¶

Active Record обеспечивает объектно-ориентированный интерфейс для доступа и манипулирования данными, хранящимися в базах данных. Класс Active Record соответствует таблице в базе данных, объект Active Record соответствует строке этой таблицы, а атрибут объекта Active Record представляет собой значение отдельного столбца строки. Вместо непосредственного написания SQL-выражений вы сможете получать доступ к атрибутам Active Record и вызывать методы Active Record для доступа и манипулирования данными, хранящимися в таблицах базы данных.

Для примера предположим, что Customer — это класс Active Record, который сопоставлен с таблицей customer , а name — столбец в таблице customer . Тогда вы можете написать следующий код для вставки новой строки в таблицу customer :

Вышеприведённый код аналогичен использованию следующего SQL-выражения в MySQL, которое менее интуитивно, потенциально может вызвать ошибки и даже проблемы совместимости, если вы используете различные виды баз данных:

Yii поддерживает работу с Active Record для следующих реляционных баз данных:

  • MySQL 4.1 и выше: посредством yiidbActiveRecord
  • PostgreSQL 7.3 и выше: посредством yiidbActiveRecord
  • SQLite 2 и 3: посредством yiidbActiveRecord
  • Microsoft SQL Server 2008 и выше: посредством yiidbActiveRecord
  • Oracle: посредством yiidbActiveRecord
  • CUBRID 9.3 и выше: посредством yiidbActiveRecord (Имейте в виду, что вследствие бага в PDO-расширении для CUBRID, заключение значений в кавычки не работает, поэтому необходимо использовать CUBRID версии 9.3 как на клиентской стороне, так и на сервере)
  • Sphinx: посредством yiisphinxActiveRecord , потребуется расширение yii2-sphinx
  • ElasticSearch: посредством yiielasticsearchActiveRecord , потребуется расширение yii2-elasticsearch

Кроме того Yii поддерживает использование Active Record со следующими NoSQL базами данных:

  • Redis 2.6.12 и выше: посредством yiiredisActiveRecord , потребуется расширение yii2-redis
  • MongoDB 1.3.0 и выше: посредством yiimongodbActiveRecord , потребуется расширение yii2-mongodb

В этом руководстве мы в основном будем описывать использование Active Record для реляционных баз данных. Однако большая часть этого материала также применима при использовании Active Record с NoSQL базами данных.

Объявление классов Active Record ¶

Для начала объявите свой собственный класс, унаследовав класс yiidbActiveRecord.

Настройка имени таблицы ¶

По умолчанию каждый класс Active Record ассоциирован с таблицей в базе данных. Метод tableName() получает имя таблицы из имени класса с помощью yiihelpersInflector::camel2id(). Если таблица не названа соответственно, вы можете переопределить данный метод.

Также может быть применён tablePrefix по умолчанию. Например, если tablePrefix задан как tbl_ , Customer преобразуется в tbl_customer , а OrderItem в tbl_order_item .

Если имя таблицы указано в формате <<%TableName>> , символ % заменяется префиксом. Например <<%post>> становится <> . Фигуриные скобки используются для экранирования в SQL-запросах.

В нижеследующем примере мы объявляем класс Active Record с названием Customer для таблицы customer .

Классы Active record называются «моделями» ¶

Объекты Active Record являются моделями. Именно поэтому мы обычно задаём классам Active Record пространство имён appmodels (или другое пространство имён, предназначенное для моделей).

Т.к. класс yiidbActiveRecord наследует класс yiibaseModel, он обладает всеми возможностями моделей, такими как атрибуты, правила валидации, способы сериализации данных и т.д.

Подключение к базам данных ¶

По умолчанию Active Record для доступа и манипулирования данными БД использует компонент приложения db в качестве компонента DB connection. Как сказано в разделе Объекты доступа к данным (DAO), вы можете настраивать компонент db на уровне конфигурации приложения как показано ниже:

Если вы хотите использовать для подключения к базе данных другой компонент подключения, отличный от db , вам нужно переопределить метод getDb():

Получение данных ¶

После объявления класса Active Record вы можете использовать его для получения данных из соответствующей таблицы базы данных. Этот процесс, как правило, состоит из следующих трёх шагов:

  1. Создать новый объект запроса вызовом метода yiidbActiveRecord::find();
  2. Настроить объект запроса вызовом методов построения запросов;
  3. Вызвать один из методов получения данных для извлечения данных в виде объектов Active Record.

Как вы могли заметить, эти шаги очень похожи на работу с построителем запросов. Различие лишь в том, что для создания объекта запроса вместо оператора new используется метод yiidbActiveRecord::find(), возвращающий новый объект запроса, являющийся представителем класса yiidbActiveQuery.

Ниже приведено несколько примеров использования Active Query для получения данных:

В примерах выше $customer — это объект класса Customer , в то время как $customers — это массив таких объектов. Все эти объекты заполнены данными таблицы customer .

Информация: Т.к. класс yiidbActiveQuery наследует yiidbQuery, вы можете использовать в нём все методы построения запросов и все методы класса Query как описано в разделе Построитель запросов.

Т.к. извлечение данных по первичному ключу или значениям отдельных столбцов достаточно распространённая задача, Yii предоставляет два коротких метода для её решения:

  • yiidbActiveRecord::findOne(): возвращает один объект Active Record, заполненный первой строкой результата запроса.
  • yiidbActiveRecord::findAll(): возвращает массив объектов Active Record, заполненных всеми полученными результатами запроса.

Оба метода могут принимать параметры в одном из следующих форматов:

  • скалярное значение: значение интерпретируется как первичный ключ, по которому следует искать. Yii прочитает информацию о структуре базы данных и автоматически определит, какой столбец таблицы содержит первичные ключи.
  • массив скалярных значений: массив интерпретируется как набор первичных ключей, по которым следует искать.
  • ассоциативный массив: ключи массива интерпретируются как названия столбцов, а значения — как содержимое столбцов, которое следует искать. За подробностями вы можете обратиться к разделу Hash Format

Нижеследующий код демонстрирует, каким образом эти методы могут быть использованы:

Внимание: Если вам нужно передать в эти методы данные, полученные от пользователя, убедитесь что передаваемое значение – это скаляр, а если необходимо указать условия в формате массива – убедитесь, что пользовательские данные не могут изменить структуру этого массива.

Примечание: Ни метод yiidbActiveRecord::findOne(), ни yiidbActiveQuery::one() не добавляет условие LIMIT 1 к генерируемым SQL-запросам. Если ваш запрос может вернуть много строк данных, вы должны вызвать метод limit(1) явно в целях улучшения производительности, например: Customer::find()->limit(1)->one() .

Помимо использования методов построения запросов вы можете также писать запросы на «чистом» SQL для получения данных и заполнения ими объектов Active Record. Вы можете делать это посредством метода yiidbActiveRecord::findBySql():

Не используйте дополнительные методы построения запросов после вызова метода findBySql(), т.к. они будут проигнорированы.

Доступ к данным ¶

Как сказано выше, получаемые из базы данные заполняют объекты Active Record и каждая строка результата запроса соответствует одному объекту Active Record. Вы можете получить доступ к значениям столбцов с помощью атрибутов этих объектов. Например так:

Примечание: Атрибуты объекта Active Record названы в соответствии с названиями столбцов связной таблицы с учётом регистра. Yii автоматически объявляет для каждого столбца связной таблицы атрибут в Active Record. Вы НЕ должны переопределять какие-либо из этих атрибутов.

Атрибуты Active Record названы в соответствии с именами столбцов таблицы. Если столбцы вашей таблицы именуются через нижнее подчёркивание, то может оказаться, что вам придётся писать PHP-код вроде этого: $customer->first_name — в нём будет использоваться нижнее подчёркивание для разделения слов в названиях атрибутов. Если вы обеспокоены единообразием стиля кодирования, вам придётся переименовать столбцы вашей таблицы соответствующим образом (например, назвать столбцы в стиле camelCase).

Преобразование данных ¶

Часто бывает так, что данные вводятся и/или отображаются в формате, который отличается от формата их хранения в базе данных. Например, в базе данных вы храните дни рождения покупателей в формате UNIX timestamp (что, кстати говоря, не является хорошим дизайном), в то время как во многих случаях вы хотите манипулировать днями рождения в виде строк формата ‘ДД.ММ.ГГГГ’ . Для достижения этой цели, вы можете объявить методы преобразования данных в ActiveRecord-классе Customer как показано ниже:

Теперь в своём PHP коде вместо доступа к $customer->birthday , вы сможете получить доступ к $customer->birthdayText , что позволить вам вводить и отображать дни рождения покупателей в формате ‘ДД.ММ.ГГГГ’ .

Подсказка: Вышеприведённый пример демонстрирует общий способ преобразования данных в различные форматы. Если вы работаете с датами и временем, вы можете использовать DateValidator и yiijuiDatePicker , которые проще в использовании и являются более мощными инструментами.

Получение данных в виде массива ¶

Несмотря на то, что получение данных в виде Active Record объектов является удобным и гибким, этот способ не всегда подходит при получении большого количества данных из-за больших накладных расходов памяти. В этом случае вы можете получить данные в виде PHP-массива, используя перед выполнением запроса метод asArray():

Примечание: В то время как этот способ бережёт память и улучшает производительность, он ближе к низкому слою абстракции базы данных и вы потеряете многие возможности Active Record. Важное отличие заключается в типах данных значений столбцов. Когда вы получаете данные в виде объектов Active Record, значения столбцов автоматически приводятся к типам, соответствующим типам столбцов; с другой стороны, когда вы получаете данные в массивах, значения столбцов будут строковыми (до тех пор, пока они являются результатом работы PDO-слоя без какой-либо обработки), несмотря на настоящие типы данных соответствующих столбцов.

Пакетное получение данных ¶

В главе Построитель запросов мы объясняли, что вы можете использовать пакетную выборку для снижения расходов памяти при получении большого количества данных из базы. Вы можете использовать такой же подход при работе с Active Record. Например:

Сохранение данных ¶

Используя Active Record, вы легко можете сохранить данные в базу данных, осуществив следующие шаги:

  1. Подготовьте объект Active Record;
  2. Присвойте новые значения атрибутам Active Record;
  3. Вызовите метод yiidbActiveRecord::save() для сохранения данных в базу данных.

Метод save() может вставить или обновить строку данных в зависимости от состояния Active Record объекта. Если объект создан с помощью оператора new , вызов метода save() приведёт к вставке новой строки данных; если объект был получен с помощью запроса на получение данных, вызов save() обновит строку таблицы, соответствующую объекту Active Record.

Вы можете различать два состояния Active Record объекта с помощью проверки значения его свойства isNewRecord. Это свойство также используется внутри метода save() как показано ниже:

Подсказка: Вы можете вызвать insert() или update() непосредственно, чтобы вставить или обновить строку данных в таблице.

Валидация данных ¶

Т.к. класс yiidbActiveRecord наследует класс yiibaseModel, он обладает такими же возможностями валидации данных. Вы можете объявить правила валидации переопределив метод rules() и осуществлять валидацию данных посредством вызовов метода validate().

Когда вы вызываете метод save(), по умолчанию он автоматически вызывает метод validate(). Только после успешного прохождения валидации происходит сохранение данных; в ином случае метод save() просто возвращает false , и вы можете проверить свойство errors для получения сообщений об ошибках валидации.

Подсказка: Если вы уверены, что ваши данные не требуют валидации (например, данные пришли из доверенного источника), вы можете вызвать save(false) , чтобы пропустить валидацию.

Массовое присваивание ¶

Как и обычные модели, объекты Active Record тоже обладают возможностью массового присваивания. Как будет показано ниже, используя эту возможность, вы можете одним PHP выражением присвоить значения множества атрибутов Active Record объекту. Запомните однако, что только безопасные атрибуты могут быть массово присвоены.

Обновление счётчиков ¶

Распространённой задачей является инкремент или декремент столбца в таблице базы данных. Назовём такие столбцы столбцами-счётчиками. Вы можете использовать метод updateCounters() для обновления одного или нескольких столбцов-счётчиков. Например:

Примечание: Если вы используете метод yiidbActiveRecord::save() для обновления столбца-счётчика, вы можете прийти к некорректному результату, т.к. вполне вероятно, что этот же счётчик был сохранён сразу несколькими запросами, которые читают и записывают этот же столбец-счётчик.

Dirty-атрибуты ¶

Когда вы вызываете save() для сохранения Active Record объекта, сохраняются только dirty-атрибуты. Атрибут считается dirty-атрибутом, если его значение было изменено после чтения из базы данных или же он был сохранён в базу данных совсем недавно. Заметьте, что валидация данных осуществляется независимо от того, имеются ли dirty-атрибуты в объекте Active Record или нет.

Active Record автоматически поддерживает список dirty-атрибутов. Это достигается за счёт хранения старых значений атрибутов и сравнения их с новыми. Вы можете вызвать метод yiidbActiveRecord::getDirtyAttributes() для получения текущего списка dirty-атрибутов. Вы также можете вызвать yiidbActiveRecord::markAttributeDirty(), чтобы явно пометить атрибут в качестве dirty-атрибута.

Если вам нужны значения атрибутов, какими они были до их изменения, вы можете вызвать getOldAttributes() или getOldAttribute().

Примечание: Сравнение старых и новых значений будет осуществлено с помощью оператора === , так что значение будет считаться dirty-значением даже в том случае, если оно осталось таким же, но изменило свой тип. Это часто происходит, когда модель получает пользовательский ввод из HTML-форм, где каждое значение представлено строкой. Чтобы убедиться в корректности типа данных, например для целых значений, вы можете применить фильтрацию данных: [‘attributeName’, ‘filter’, ‘filter’ => ‘intval’] .

Значения атрибутов по умолчанию ¶

Некоторые столбцы ваших таблиц могут иметь значения по умолчанию, объявленные в базе данных. Иногда вы можете захотеть предварительно заполнить этими значениями вашу веб-форму, которая соответствует Active Record объекту. Чтобы избежать повторного указания этих значений, вы можете вызвать метод loadDefaultValues() для заполнения соответствующих Active Record атрибутов значениями по умолчанию, объявленными в базе данных:

Приведение типов атрибутов ¶

При заполнении результатами запроса yiidbActiveRecord производит автоматическое приведение типов для значений атрибутов на основе информации из схемы базы данны. Это позволяет данным, полученным из колонки таблицы объявленной как целое, заноситься в экземпляр ActiveRecord как значение целого типа PHP, булево как булево и т.д. Однако, механизм приведения типов имеет несколько ограничений:

  • Числа с плавающей точкой не будут обработаны, а будут представленны как строки, в противном случае они могут потерять точность.
  • Конвертация целых чисел зависит от разрядности используемой операционной системы. В частности: значения колонок, объявленных как ‘unsigned integer’ или ‘big integer’ будут приведены к целому типу PHP только на 64-х разрядных системах, в то время как на 32-х разрядных — они будут представленны как строки.

Имейте в виду, что преобразование типов производиться только в момент заполнения экземпляра ActiveRecord данными из результата запроса. При заполнении данных из HTTP запроса или непосредственно через механизм доступа к полям — автоматическая конвертация не производтся. Схема таблицы базы данных также используется при построении SQL запроса для сохранения данных ActiveRecord, обеспечивая соответсвие типов связываемых параметров в запросе. Однако, над атрибутами объекта ActiveRecord не будет производиться приведение типов в процессе сохранения.

Совет: вы можете использовать поведение yiibehaviorsAttributeTypecastBehavior для того, чтобы производить приведение типов для ActiveRecord во время валидации или сохранения.

Начиная с 2.0.14, Yii ActiveRecord поддерживает сложные типы данных, такие как JSON или многомерные массивы.

JSON в MySQL и PostgreSQL ¶

После заполнения данных, значение из столбца JSON будет автоматически декодировано из JSON в соответствии со стандартными правилами декодирования JSON.

Чтобы сохранить значение атрибута в столбец JSON, ActiveRecord автоматически создаст объект JsonExpression, который будет закодирован в строку JSON на уровне QueryBuilder.

Массивы в PostgreSQL ¶

После заполнения данных значение из столбца Array будет автоматически декодировано из нотации PgSQL в объект ArrayExpression. Он реализует интерфейс PHP ArrayAccess , так что вы можете использовать его в качестве массива, или вызвать ->getValue () , чтобы получить сам массив.

Чтобы сохранить значение атрибута в столбец массива, ActiveRecord автоматически создаст объект [[yiidbArray Expression|ArrayExpression]], который будет закодирован QueryBuilder в строковое представление массива PgSQL.

Можно также использовать условия для столбцов JSON:

Обновление нескольких строк данных ¶

Методы, представленные выше, работают с отдельными Active Record объектами, инициируя вставку или обновление данных для отдельной строки таблицы. Вместо них для обновления нескольких строк одновременно можно использовать метод updateAll(), который является статическим.

Подобным образом можно использовать метод updateAllCounters() для обновления значений столбцов-счётчиков в нескольких строках одновременно.

Удаление данных ¶

Для удаления одной отдельной строки данных сначала получите Active Record объект, соответствующий этой строке, а затем вызовите метод yiidbActiveRecord::delete().

Вы можете вызвать yiidbActiveRecord::deleteAll() для удаления всех или нескольких строк данных одновременно. Например:

Примечание: будьте очень осторожны, используя метод deleteAll(), потому что он может полностью удалить все данные из вашей таблицы, если вы сделаете ошибку при указании условий удаления.

Жизненные циклы Active Record ¶

Важно понимать как устроены жизненные циклы Active Record при использовании Active Record для различных целей. В течение каждого жизненного цикла вызывается определённая последовательность методов, которые вы можете переопределять, чтобы получить возможность тонкой настройки жизненного цикла. Для встраивания своего кода вы также можете отвечать на конкретные события Active Record, которые срабатывают в течение жизненного цикла. Эти события особенно полезны, когда вы разрабатываете поведения, которые требуют тонкой настройки жизненных циклов Active Record.

Ниже мы подробно опишем различные жизненные циклы Active Record и методы/события, которые участвуют в жизненных циклах.

Жизненный цикл создания нового объекта ¶

Когда создаётся новый объект Active Record с помощью оператора new , следующий жизненный цикл имеет место:

  1. Вызывается конструктор класса;
  2. Вызывается init(): инициируется событие EVENT_INIT.

Жизненный цикл получения данных ¶

Когда происходит получение данных посредством одного из методов получения данных, каждый вновь создаваемый объект Active Record при заполнении данными проходит следующий жизненный цикл:

  1. Вызывается конструктор класса.
  2. Вызывается init(): инициируется событие EVENT_INIT.
  3. Вызывается afterFind(): инициируется событие EVENT_AFTER_FIND.

Жизненный цикл сохранения данных ¶

Когда вызывается метод save() для вставки или обновления объекта Active Record, следующий жизненный цикл имеет место:

  1. Вызывается beforeValidate(): инициируется событие EVENT_BEFORE_VALIDATE. Если метод возвращает false или свойство события yiibaseModelEvent::$isValid равно false , оставшиеся шаги не выполняются.
  2. Осуществляется валидация данных. Если валидация закончилась неудачей, после 3-го шага остальные шаги не выполняются.
  3. Вызывается afterValidate(): инициируется событие EVENT_AFTER_VALIDATE.
  4. Вызывается beforeSave(): инициируется событие EVENT_BEFORE_INSERT или событие EVENT_BEFORE_UPDATE. Если метод возвращает false или свойство события yiibaseModelEvent::$isValid равно false , оставшиеся шаги не выполняются.
  5. Осуществляется фактическая вставка или обновление данных в базу данных;
  6. Вызывается afterSave(): инициируется событие EVENT_AFTER_INSERT или событие EVENT_AFTER_UPDATE.

Жизненный цикл удаления данных ¶

Когда вызывается метод delete() для удаления объекта Active Record, следующий жизненный цикл имеет место:

  1. Вызывается beforeDelete(): инициируется событие EVENT_BEFORE_DELETE. Если метод возвращает false или свойство события yiibaseModelEvent::$isValid равно false , остальные шаги не выполняются.
  2. Осуществляется фактическое удаление данных из базы данных.
  3. Вызывается afterDelete(): инициируется событие EVENT_AFTER_DELETE.

Примечание: Вызов следующих методов НЕ инициирует ни один из вышеприведённых жизненных циклов:

Работа с транзакциями ¶

Есть два способа использования транзакций при работе с Active Record.

Первый способ заключается в том, чтобы явно заключить все вызовы методов Active Record в блок транзакции как показано ниже:

Примечание: в коде выше ради совместимости с PHP 5.x и PHP 7.x использованы два блока catch. Exception реализует интерфейс Throwable interface начиная с PHP 7.0. Если вы используете только PHP 7 и новее, можете пропустить блок с Exception .

Второй способ заключается в том, чтобы перечислить операции с базой данных, которые требуют тразнакционного выполнения, в методе yiidbActiveRecord::transactions(). Например:

Метод yiidbActiveRecord::transactions() должен возвращать массив, ключи которого являются именами сценариев, а значения соответствуют операциям, которые должны быть выполнены с помощью транзакций. Вы должны использовать следующие константы для обозначения различных операций базы данных:

  • OP_INSERT: операция вставки, осуществляемая с помощью метода insert();
  • OP_UPDATE: операция обновления, осуществляемая с помощью метода update();
  • OP_DELETE: операция удаления, осуществляемая с помощью метода delete().

Используйте операторы | для объединения вышеприведённых констант при обозначении множества операций. Вы можете также использовать вспомогательную константу OP_ALL, чтобы обозначить одной константой все три вышеприведённые операции.

Оптимистическая блокировка ¶

Оптимистическая блокировка — это способ предотвращения конфликтов, которые могут возникать, когда одна и та же строка данных обновляется несколькими пользователями. Например, пользователь A и пользователь B одновременно редактируют одну и ту же wiki-статью. После того, как пользователь A сохранит свои изменения, пользователь B нажимает на кнопку «Сохранить» в попытке также сохранить свои изменения. Т.к. пользователь B работал с фактически-устаревшей версией статьи, было бы неплохо иметь способ предотвратить сохранение его варианта статьи и показать ему некоторое сообщение с подсказкой о том, что произошло.

Оптимистическая блокировка решает вышеприведённую проблему за счёт использования отдельного столбца для сохранения номера версии каждой строки данных. Когда строка данных сохраняется с использованием устаревшего номера версии, выбрасывается исключение yiidbStaleObjectException, которое предохраняет строку от сохранения. Оптимистическая блокировка поддерживается только тогда, когда вы обновляете или удаляете существующую строку данных, используя методы yiidbActiveRecord::update() или yiidbActiveRecord::delete() соответственно.

Для использования оптимистической блокировки:

  1. Создайте столбец в таблице базы данных, ассоциированной с классом Active Record, для сохранения номера версии каждой строки данных. Столбец должен быть типа big integer (в Mysql это будет BIGINT DEFAULT 0 ).
  2. Переопределите метод yiidbActiveRecord::optimisticLock() таким образом, чтобы он возвращал название этого столбца.
  3. В веб-форме, которая принимает пользовательский ввод, добавьте скрытое поле для сохранения текущей версии обновляемой строки. Убедитесь, что для вашего атрибута с версией объявлены правила валидации, и валидация проходит успешно.
  4. В действии контроллера, которое занимается обновлением строки данных с использованием Active Record, оберните в блок try. catch код и перехватывайте исключение yiidbStaleObjectException. Реализуйте необходимую бизнес-логику (например, возможность слияния изменений, подсказку о том, что данные устарели) для разрешения возникшего конфликта.

Например, предположим, что столбец с версией называется version . Вы можете реализовать оптимистическую блокировку с помощью подобного кода:

Работа со связными данными ¶

Помимо работы с отдельными таблицами баз данных, Active Record также имеет возможность объединять связные данные, что делает их легко-доступными для получения через основные объекты данных. Например, данные покупателя связаны с данными заказов, потому что один покупатель может осуществить один или несколько заказов. С помощью объявления этой связи вы можете получить возможность доступа к информации о заказе покупателя с помощью выражения $customer->orders , которое возвращает информацию о заказе покупателя в виде массива объектов класса Order , которые являются Active Record объектами.

Объявление связей ¶

Для работы со связными данными посредством Active Record вы прежде всего должны объявить связи в классе Active Record. Эта задача решается простым объявлением методов получения связных данных для каждой интересующей вас связи как показано ниже:

В вышеприведённом коде мы объявили связь orders для класса Customer и связь customer для класса Order .

Каждый метод получения связных данных должен быть назван в формате getXyz . Мы называем xyz (первая буква в нижнем регистре) именем связи. Помните, что имена связей чувствительны к регистру.

При объявлении связи, вы должны указать следующую информацию:

  • кратность связи: указывается с помощью вызова метода hasMany() или метода hasOne(). В вышеприведённом примере вы можете легко увидеть в объявлениях связей, что покупатель может иметь много заказов в то время, как заказ может быть сделан лишь одним покупателем.
  • название связного Active Record класса: указывается в качестве первого параметра для метода hasMany() или для метода hasOne(). Рекомендуется использовать код Xyz::class , чтобы получить строку с именем класса, при этом вы сможете воспользоваться возможностями авто-дополнения кода, встроенного в IDE, а также получите обработку ошибок на этапе компиляции.

связь между двумя типами данных: указываются столбцы с помощью которых два типа данных связаны. Значения массива — это столбцы основного объекта данных (представлен классом Active Record, в котором объявляется связь), в то время как ключи массива — столбцы связанных данных.

Есть простой способ запомнить это правило: как вы можете увидеть в примере выше, столбец связной Active Record указывается сразу после указания самого класса Active Record. Вы видите, что customer_id — это свойство класса Order , а id — свойство класса Customer .

Внимание: Имя связи relation зарезервировано. Его использование приведёт к ошибке ArgumentCountError .

Доступ к связным данным ¶

После объявления связей вы можете получать доступ к связным данным с помощью имён связей. Это происходит таким же образом, каким осуществляется доступ к свойству объекта объявленному с помощью метода получения связных данных. По этой причине, мы называем его свойством связи. Например:

Информация: когда вы объявляете связь с названием xyz посредством геттера getXyz() , у вас появляется возможность доступа к свойству xyz подобно свойству объекта. Помните, что название связи чувствительно к регистру.

Если связь объявлена с помощью метода hasMany(), доступ к свойству связи вернёт массив связных объектов Active Record; если связь объявлена с помощью метода hasOne(), доступ к свойству связи вернёт связный Active Record объект или null , если связные данные не найдены.

Когда вы запрашиваете свойство связи в первый раз, выполняется SQL-выражение как показано в примере выше. Если то же самое свойство запрашивается вновь, будет возвращён результат предыдущего SQL-запроса без повторного выполнения SQL-выражения. Для принудительного повторного выполнения SQL-запроса, вы можете удалить свойство связи с помощью операции: unset($customer->orders) .

Примечание: Несмотря на то, что эта концепция выглядит похожей на концепцию свойств объектов, между ними есть важное различие. Для обычных свойств объектов значения свойств имеют тот же тип, который возвращает геттер. Однако метод получения связных данных возвращает объект yiidbActiveQuery, в то время как доступ к свойству связи возвращает объект yiidbActiveRecord или массив таких объектов. ` php $customer->orders; // массив объектов Order $customer->getOrders(); // объект ActiveQuery ` Это полезно при тонкой настройке запросов к связным данным, что будет описано в следующем разделе.

Динамические запросы связных данных ¶

Т.к. метод получения связных данных возвращает объект запроса yiidbActiveQuery, вы можете в дальнейшем перед его отправкой в базу данных настроить этот запрос, используя методы построения запросов. Например:

В отличие от доступа к данным с помощью свойства связи, каждый раз при выполнении такого динамического запроса посредством метода получения связных данных будет выполняться SQL-запрос, даже если тот же самый динамический запрос был отправлен ранее.

Иногда вы можете даже захотеть настроить объявление связи таким образом, чтобы вы могли более просто осуществлять динамические запросы связных данных. Например, вы можете объявить связь bigOrders как показано ниже:

После этого вы сможете выполнять следующие запросы связных данных:

Связывание посредством промежуточной таблицы ¶

При проектировании баз данных, когда между двумя таблицами имеется кратность связи many-to-many, обычно вводится промежуточная таблица. Например, таблицы order и item могут быть связаны посредством промежуточной таблицы с названием order_item . Один заказ будет соотноситься с несколькими товарами, в то время как один товар будет также соотноситься с несколькими заказами.

При объявлении подобных связей вы можете пользоваться методом via() или методом viaTable() для указания промежуточной таблицы. Разница между методами via() и viaTable() заключается в том, что первый метод указывает промежуточную таблицу с помощью названия связи, в то время как второй метод непосредственно указывает промежуточную таблицу. Например:

Использовать связи, объявленные с помощью промежуточных таблиц, можно точно также, как и обычные связи. Например:

Отложенная и жадная загрузка ¶

В разделе Доступ к связным данным, мы показывали, что вы можете получать доступ к свойству связи объекта Active Record точно также, как получаете доступ к свойству обычного объекта. SQL-запрос будет выполнен только во время первого доступа к свойству связи. Мы называем подобный способ получения связных данных отложенной загрузкой. Например:

Отложенная загрузка очень удобна в использовании. Однако этот метод может вызвать проблемы производительности, когда вам понадобится получить доступ к тем же самым свойствам связей для нескольких объектов Active Record. Рассмотрите следующий пример кода. Сколько SQL-запросов будет выполнено?

Как вы могли заметить по вышеприведённым комментариям кода, будет выполнен 101 SQL-запрос! Это произойдёт из-за того, что каждый раз внутри цикла будет выполняться SQL-запрос при получении доступа к свойству связи orders каждого отдельного объекта Customer .

Для решения этой проблемы производительности вы можете, как показано ниже, использовать подход, который называется жадная загрузка:

Посредством вызова метода yiidbActiveQuery::with(), вы указываете объекту Active Record вернуть заказы первых 100 покупателей с помощью одного SQL-запроса. В результате снижаете количество выполняемых SQL-запросов от 101 до 2!

Вы можете жадно загружать одну или несколько связей. Вы можете даже жадно загружать вложенные связи. Вложенная связь — это связь, которая объявлена внутри связного Active Record класса. Например, Customer связан с Order посредством связи orders , а Order связан с Item посредством связи items . При формировании запроса для Customer , вы можете жадно загрузить items , используя нотацию вложенной связи orders.items .

Ниже представлен код, который показывает различные способы использования метода with(). Мы полагаем, что класс Customer имеет две связи: orders и country — в то время как класс Order имеет лишь одну связь items .

Вы можете жадно загрузить более глубокие вложенные связи, такие как a.b.c.d . Все родительские связи будут жадно загружены. Таким образом, когда вы вызываете метод with() с параметром a.b.c.d , вы жадно загрузите связи a , a.b , a.b.c и a.b.c.d .

Информация: В целом, когда жадно загружается N связей, среди которых M связей объявлено с помощью промежуточной таблицы, суммарное количество выполняемых SQL-запросов будет равно N+M+1 . Заметьте, что вложенная связь a.b.c.d насчитывает 4 связи.

Когда связь жадно загружается, вы можете настроить соответствующий запрос получения связных данных с использованием анонимной функции. Например:

Когда настраивается запрос на получение связных данных для какой-либо связи, вы можете указать название связи в виде ключа массива и использовать анонимную функцию в качестве соответствующего значения этого массива. Анонимная функция получит параметр $query , который представляет собой объект yiidbActiveQuery, используемый для выполнения запроса на получение связных данных для данной связи. В вышеприведённом примере кода мы изменили запрос на получение связных данных, наложив на него дополнительное условие выборки статуса заказов.

Примечание: Если вы вызываете метод select() в процессе жадной загрузки связей, вы должны убедиться, что будут выбраны столбцы, участвующие в объявлении связей. Иначе связные модели будут загружены неправильно. Например:

Использование JOIN со связями ¶

Примечание: Материал этого раздела применим только к реляционным базам данных, таким как MySQL, PostgreSQL, и т.д.

Запросы на получение связных данных, которые мы рассмотрели выше, ссылаются только на столбцы основной таблицы при извлечении основной информации. На самом же деле нам часто нужно ссылаться в запросах на столбцы связных таблиц. Например, мы можем захотеть получить покупателей, для которых имеется хотя бы один активный заказ. Для решения этой проблемы мы можем построить запрос с использованием JOIN как показано ниже:

Примечание: Важно однозначно указывать в SQL-выражениях имена столбцов при построении запросов на получение связных данных с участием оператора JOIN. Наиболее распространённая практика — предварять названия столбцов с помощью имён соответствующих им таблиц.

Однако лучшим подходом является использование имеющихся объявлений связей с помощью вызова метода yiidbActiveQuery::joinWith():

Оба подхода выполняют одинаковый набор SQL-запросов. Однако второй подход более прозрачен и прост.

По умолчанию, метод joinWith() будет использовать конструкцию LEFT JOIN для объединения основной таблицы со связной. Вы можете указать другой тип операции JOIN (например, RIGHT JOIN ) с помощью третьего параметра этого метода — $joinType . Если вам нужен INNER JOIN , вы можете вместо этого просто вызвать метод innerJoinWith().

Вызов метода joinWith() будет жадно загружать связные данные по умолчанию. Если вы не хотите получать связные данные, вы можете передать во втором параметре $eagerLoading значение false .

Подобно методу with() вы можете объединять данные с одной или несколькими связями; вы можете настроить запрос на получение связных данных «на лету»; вы можете объединять данные с вложенными связями; вы можете смешивать использование метода with() и метода joinWith(). Например:

Иногда во время объединения двух таблиц вам может потребоваться указать некоторые дополнительные условия рядом с оператором ON во время выполнения JOIN-запроса. Это можно сделать с помощью вызова метода yiidbActiveQuery::onCondition() как показано ниже:

Вышеприведённый запрос вернёт всех покупателей и для каждого покупателя вернёт все активные заказы. Заметьте, что это поведение отличается от нашего предыдущего примера, в котором возвращались только покупатели, у которых был как минимум один активный заказ.

Информация: Когда в объекте yiidbActiveQuery указано условие выборки с помощью метода onCondition(), это условие будет размещено в конструкции ON , если запрос содержит оператор JOIN. Если же запрос не содержит оператор JOIN, такое условие будет автоматически размещено в конструкции WHERE .

Псевдонимы связанных таблиц ¶

Как уже было отмечено, при использовании в запросе JOIN-ов, приходится явно решать конфликты имён. Поэтому часто таблицам дают псевдонимы. Задать псевдоним для реляционного запроса можно следующим образом:

Выглядит это довольно сложно. Либо приходится задавать явно имена таблиц, либо вызывать Order::tableName() . Начиная с версии 2.0.7 вы можете задать и использовать псевдоним для связанной таблицы следующим образом:

Этот синтаксис работает для простых связей. Если необходимо использовать связующую таблицу, например $query->joinWith([‘orders.product’]) , то вызовы joinWith вкладываются друг в друга:

Обратные связи ¶

Объявления связей часто взаимны между двумя Active Record классами. Например, Customer связан с Order посредством связи orders , а Order взаимно связан с Customer посредством связи customer .

Теперь рассмотрим следующий участок кода:

Мы думали, что $customer и $customer2 эквивалентны, но оказалось, что нет! Фактически они содержат одинаковые данные, но являются разными объектами. Когда мы получаем доступ к данным посредством $order->customer , выполняется дополнительный SQL-запрос для заполнения нового объекта $customer2 .

Чтобы избежать избыточного выполнения последнего SQL-запроса в вышеприведённом примере, мы должны подсказать Yii, что customer — обратная связь относительно orders , и сделаем это с помощью вызова метода inverseOf() как показано ниже:

Теперь, после этих изменений в объявлении связи, получим:

Примечание: обратные связи не могут быть объявлены для связей, использующих промежуточную таблицу. То есть, если связь объявлена с помощью методов via() или viaTable(), вы не должны вызывать после этого метод inverseOf().

Сохранение связных данных ¶

Во время работы со связными данными вам часто требуется установить связи между двумя разными видами данных или удалить существующие связи. Это требует установки правильных значений для столбцов, с помощью которых заданы связи. При использовании Active Record вам может понадобится завершить участок кода следующим образом:

Active Record предоставляет метод link(), который позволяет выполнить эту задачу более красивым способом:

Метод link() требует указать название связи и целевой объект Active Record, с которым должна быть установлена связь. Метод изменит значения атрибутов, которые связывают два объекта Active Record, и сохранит их в базу данных. В вышеприведённом примере, метод присвоит атрибуту customer_id объекта Order значение атрибута id объекта Customer и затем сохранит его в базу данных.

Примечание: Невозможно связать два свежесозданных объекта Active Record.

Преимущество метода link() становится ещё более очевидным, когда связь объявлена посредством промежуточной таблицы. Например, вы можете использовать следующий код, чтобы связать объект Order с объектом Item :

Вышеприведённый код автоматически вставит строку данных в промежуточную таблицу order_item , чтобы связать объект order с объектом item .

Информация: Метод link() не осуществляет какую-либо валидацию данных во время сохранения целевого объекта Active Record. На вас лежит ответственность за валидацию любых введённых данных перед вызовом этого метода.

Существует противоположная операция для link() — это операция unlink(), она снимает существующую связь с двух объектов Active Record. Например:

По умолчанию метод unlink() задаст вторичному ключу (или ключам), который определяет существующую связь, значение null . Однако вы можете запросить удаление строки таблицы, которая содержит значение вторичного ключа, передав значение true в параметре $delete для этого метода.

Если связь построена на основе промежуточной таблицы, вызов метода unlink() инициирует очистку вторичных ключей в промежуточной таблице, или же удаление соответствующей строки данных в промежуточной таблице, если параметр $delete равен true .

Связывание объектов из разных баз данных ¶

Active Record позволяет вам объявить связи между классами Active Record, которые относятся к разным базам данных. Базы данных могут быть разных типов (например, MySQL и PostgreSQL или MS SQL и MongoDB), и они могут быть запущены на разных серверах. Вы можете использовать тот же самый синтаксис для осуществления запросов выборки связных данных. Например:

Вы можете использовать большую часть возможностей запросов получения связных данных, которые были описаны в этой главе.

Примечание: Применимость метода joinWith() ограничена базами данных, которые позволяют выполнять запросы между разными базами с использованием оператора JOIN. По этой причине вы не можете использовать этот метод в вышеприведённом примере, т.к. MongoDB не поддерживает операцию JOIN.

Тонкая настройка классов Query ¶

По умолчанию все запросы данных для Active Record поддерживаются с помощью класса yiidbActiveQuery. Для использования собственного класса запроса вам необходимо переопределить метод yiidbActiveRecord::find() и возвращать из него объект вашего собственного класса запроса. Например:

Теперь, когда вы будете осуществлять получение данных (например, выполните find() , findOne() ) или объявите связь (например, hasOne() ) с объектом Comment , вы будете работать с объектом класса CommentQuery вместо ActiveQuery .

Подсказка: В больших проектах рекомендуется использовать собственные классы запросов, которые будут содержать в себе большую часть кода, связанного с настройкой запросов, таким образом классы Active Record удастся сохранить более чистыми.

Вы можете настроить класс запроса большим количеством различных способов для улучшения методик построения запросов. Например, можете объявить новые методы построения запросов в собственном классе запросов:

Примечание: Вместо вызова метода where() старайтесь во время объявления новых методов построения запросов использовать andWhere() или orWhere() для добавления дополнительных условий, в этом случае уже заданные условия выборок не будут перезаписаны.

Это позволит вам писать код построения запросов как показано ниже:

Вы также можете использовать новые методы построения запросов, когда объявляете связи для класса Comment или осуществляете запрос для выборки связных данных:

Информация: В Yii версии 1.1 была концепция с названием scope. Она больше не поддерживается в Yii версии 2.0, и вы можете использовать собственные классы запросов и собственные методы построения запросов, чтобы добиться той же самой цели.

Получение дополнительных атрибутов ¶

Когда объект Active Record заполнен результатами запроса, его атрибуты заполнены значениями соответствующих столбцов из полученного набора данных.

Вы можете получить дополнительные столбцы или значения с помощью запроса и сохранить их внутри объекта Active Record. Например, предположим, что у нас есть таблица ‘room’, которая содержит информацию о доступных в отеле комнатах. Каждая комната хранит информацию о её геометрических размерах с помощью атрибутов ‘length’, ‘width’, ‘height’. Представьте, что вам требуется получить список всех доступных комнат, отсортированных по их объёму в порядке убывания. В этом случае вы не можете вычислять объём с помощью PHP, потому что нам требуется сортировать записи по объёму, но вы также хотите отображать объем в списке. Для достижения этой цели, вам необходимо объявить дополнительный атрибут в вашем Active Record классе ‘Room’, который будет хранить значение ‘volume’:

Далее вам необходимо составить запрос, который вычисляет объём комнаты и выполняет сортировку:

Возможность выбирать дополнительные атрибуты может быть особенно полезной для агрегирующих запросов. Представьте, что вам необходимо отображать список покупателей с количеством их заказов. Прежде всего вам потребуется объявить класс Customer со связью ‘orders’ и дополнительным атрибутом для хранения расчётов:

После этого вы сможете составить запрос, который объединяет заказы и вычисляет их количество:

Недостаток этого подхода заключается в том, что если данные для поля не загружены по результатам SQL запроса, то они должны быть вычисленны отдельно. Это означает, что запись, полученная посредством обычного запроса без дополнительных полей в разделе ‘select’, не может вернуть реальное значения для дополнительного поля. Это же касается и только что сохранненой записи.

Использование магических методов __get() и __set() позволяет эмулировать поведение обычного поля:

Если результат запроса на выборку данных не содержит поле ‘volume’, то модель сможет расчитать его автоматически используя имеющиеся атрибуты.

Вы также можете вычислять агрегируемые поля используя объявленные отношения:

При такой реализации, в случае когда ‘ordersCount’ присутсвует в разделе ‘select’ — значение ‘Customer::ordersCount’ будет заполнено из результатов запроса, в противном случае — оно будет вычислено по первому требованию на основании отношения Customer::orders .

Этот подход также можно использовать для быстрого доступа к некоторым данным отношений, в особенности для агрегации. Например:

Источник

  1. Home

  2. php — Yii2 ActiveRecord->save() returns true, no data inserted in database

540 votes

1 answers

Get the solution ↓↓↓

I have the following function in a controller

public function actionSave_role() {

        $role = new Auth_Item;

        if ($role->load(Yii::$app->request->post()) && $role->save()) {
            //echo '<pre>'; print_r($role->getErrors());
            //yiihelpersVarDumper::dump($role->getErrors(), 10, true);
            //pring_r($role->getErrors());
            //die();
            return $this->actionGet_role($role->id);
        }
}

The POST returns true, but no data is entered in the database. I’m trying to view the errors. There has to be some kind of validation not being met the the database but I haven’t been able to figure it out. So I’m trying view the actual error, which has proven difficult, my attempts are all commented out. Still working on my PHP debugging skills. I believe that they are all ‘failing’ to print for the same reason, which has to do with where the data is actually being printed out.

Is there any way to kill the application right after $role->save() and view $role->getErrors()? Any other advice on debugging this issue would also be appreciated!

Edit1:

Tried

$role->save(false); //No Luck

Here is my record model

`auth_item`(`id`, `name`, `owner_user_id`, `type`, `description`, `rule_name`, `data`, `created_at`, `updated_at`, `admin`, `create_package`, `read_package`, `update_package`, `delete_package`)

This is my saveRole javascript function to kick things off:

function saveRole(role){
    var newRole = {
        _csrf: csrf,
        'Role[id]': role.id,
        'Role[name]': role.name,
        'Role[description]': role.description,
        'Role[admin]': role.admin ? role.admin = 1 : role.admin = 0,
        'Role[create_package]': role.create_package ? role.create_package = 1 : role.create_package = 0,
        'Role[read_package]': role.read_package ? role.read_package = 1 : role.read_package = 0,
        'Role[update_package]': role.update_package ? role.update_package = 1 : role.update_package = 0,
        'Role[delete_package]': role.delete_package ? role.delete_package = 1 : role.delete_package = 0
    };

    var request = $http({
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        url: 'roles/save_role', 
        method: "POST",
        data: $.param(newRole)
    });

    return request.then(handleSuccess, handleError);
};

This is my Auth_Item AcitveRecord model

class Auth_Item extends yiidbActiveRecord
{
public static function tableName()
{
    return 'auth_item';
}

public function rules()
{
    return [
        [['name'], 'required'],
        [['created_at', 'updated_at'], 'safe'],
        [['name'], 'string', 'max' => 255]
    ];
}

public function beforeSave($insert) {
    if ($insert) {
        $this->owner_user_id = Yii::$app->user->identity->id;
        $this->rule_name = null;
        $this->type = 1;
        $this->data = null;
        $this->created_at = new yiidbExpression('NOW()');

    }
    $this->updated_at = new yiidbExpression('NOW()');
    return true;
}
}

Here is my table structure:

    CREATE TABLE IF NOT EXISTS `auth_item` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
  `owner_user_id` int(11) DEFAULT NULL,
  `type` int(11) NOT NULL,
  `description` text COLLATE utf8_unicode_ci,
  `rule_name` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `data` text COLLATE utf8_unicode_ci,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `admin` tinyint(1) DEFAULT NULL,
  `create_package` tinyint(1) DEFAULT NULL,
  `read_package` tinyint(1) DEFAULT NULL,
  `update_package` tinyint(1) DEFAULT NULL,
  `delete_package` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`),
  KEY `rule_name` (`rule_name`),
  KEY `idx-auth_item-type` (`type`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=11 ;

ALTER TABLE `auth_item`
ADD CONSTRAINT `auth_item_ibfk_1` FOREIGN KEY (`rule_name`) REFERENCES `auth_rule` (`name`) ON DELETE SET NULL ON UPDATE CASCADE;

Edit2:

Here is where I’m at. I think its getting closer. Post is still returning 200 OK, data still not actually making it into the auth_item table.

public function actionSave_role() {
    $auth = Yii::$app->authManager;
    $id = $_POST["Role"]["id"];
        $roleModel = null;
        if ($id != null) {  // contact exists
            $roleModel = Auth_Item::findOne($id);
        } else {
            $roleModel = new Auth_Item();
        }

        if ($roleModel->load(Yii::$app->request->post())) {
            $role = $auth->createRole($roleModel->name);
            $role->data = $roleModel->data;
            $role->description = $roleModel->description;
            $role->admin = $roleModel->admin;
            $role->create_package = $roleModel->create_package;
            $role->read_package = $roleModel->read_package;
            $role->update_package = $roleModel->update_package;
            $role->delete_package = $roleModel->delete_package;
            //End Posted data
            $role->rule_name = null;
            $role->type = 1;
            $role->created_at = new yiidbExpression('NOW()');
            //$role->updated_at = new yiidbExpression('NOW()');
            $role->owner_user_id = Yii::$app->user->identity->id;

            $auth->add($role);

            return $this->actionGet_role($role->id);
        }

}

2022-05-24

Write your answer


468

votes

Answer

Solution:

For authmanager is not save to use but you shuold create a role and the add the role

I use this actionCreate

/**
 * Creates a new AuthItem model.
 * If creation is successful, the browser will be redirected to the 'view' page.
 * @return mixed
 */
public function actionCreate()
{
    $model = new AuthItem();
    $auth = Yii::$app->authManager;


    if ($model->load(Yii::$app->request->post())) {
        switch ($model->type) {
            case AuthItem::TYPE_ROLE :   // 1 = TYPE_ROLE
                $role = $auth->createRole($model->name);
                $role->data         = $model->data;
                //$role->ruleName     = $model->rule_name;
                $role->description  = $model->description;
                //$role->type         = $model->type;
                $auth->add($role);
                break;                    
             case AuthItem::TYPE_PERMISSION :  // 2 = TYPE_PERMISSION
                $permission  = $auth->createPermission($model->name);
                $permission->data         = $model->data;
                //$permission->ruleName     = $model->rule_name;
                $permission->description  = $model->description;
                //$permission->type         = $model->type;
                $auth->add($permission);
                break;              
            default:
                break;
        }
        return $this->redirect(['view', 'id' => $model->name]);
    } else {
        return $this->render('create', [
            'model' => $model,
        ]);
    }
}

this the model

<?php

    namespace vendordfenxauthmodels;

    use Yii;

    /**
     * This is the model class for table "auth_item".
     *
     * @property string $name
     * @property integer $type
     * @property string $description
     * @property string $rule_name
     * @property string $data
     * @property integer $created_at
     * @property integer $updated_at
     *
     * @property AuthAssignment[] $authAssignments
     * @property AuthRule $ruleName
     * @property AuthItemChild[] $authItemChildren
     */
    class AuthItem extends yiidbActiveRecord
    {

        const TYPE_ROLE       = '1';
        const TYPE_PERMISSION = '2';

        const TEXT_TYPE_ROLE       = 'TYPE_ROLE';
        const TEXT_TYPE_PERMISSION = 'TYPE_PERMISSION';
        const TEXT_ERROR_TYPE_NOT_ASSIGNED  = 'ERROR_TYPE_NOT_ASSIGNED';

        /**
         * @inheritdoc
         */
        public static function tableName()
        {
            return 'dfenx_auth_item';
        }

        /**
         * @inheritdoc
         */
        public function rules()
        {
            return [
                [['name', 'type'], 'required'],
                [['type', 'created_at', 'updated_at'], 'integer'],
                [['description', 'data'], 'string'],
                [['name', 'rule_name'], 'string', 'max' => 64]
            ];
        }

        /**
         * @inheritdoc
         */
        public function attributeLabels()
        {
            return [
                'name' => Yii::t('app', 'Name'),
                'type' => Yii::t('app', 'Type'),
                'description' => Yii::t('app', 'Description'),
                'rule_name' => Yii::t('app', 'Rule Name'),
                'data' => Yii::t('app', 'Data'),
                'created_at' => Yii::t('app', 'Created At'),
                'updated_at' => Yii::t('app', 'Updated At'),
            ];
        }

        /**
         * @return yiidbActiveQuery
         */
        public function getAuthAssignments()
        {
            return $this->hasMany(AuthAssignment::className(), ['item_name' => 'name']);
        }

        /**
         * @return yiidbActiveQuery
         */
        public function getRuleName()
        {
            return $this->hasOne(AuthRule::className(), ['name' => 'rule_name']);
        }

        /**
         * @return yiidbActiveQuery
         */
        public function getAuthItemChildren()
        {
            return $this->hasMany(AuthItemChild::className(), ['child' => 'name']);
        }
    }


Share solution ↓

Additional Information:

Date the issue was resolved:

2022-05-24

Link To Source

Link To Answer
People are also looking for solutions of the problem: foreign key constraint is incorrectly formed laravel

Didn’t find the answer?

Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.


Similar questions

Find the answer in similar questions on our website.

Write quick answer

Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.


About the technologies asked in this question

PHP

PHP (from the English Hypertext Preprocessor — hypertext preprocessor) is a scripting programming language for developing web applications. Supported by most hosting providers, it is one of the most popular tools for creating dynamic websites.
The PHP scripting language has gained wide popularity due to its processing speed, simplicity, cross-platform, functionality and distribution of source codes under its own license.
https://www.php.net/

Yii

Yii is a simple yet high performance generic component framework based framework. It is known for its high performance, but above all, it is famous for its simplicity. This framework appeared in December 2008. It allows you to use third-party code, and its Gii code generator allows you to quickly create basic structures from which you can build your own solutions.
https://www.yiiframework.com/

JavaScript

JavaScript is a multi-paradigm language that supports event-driven, functional, and mandatory (including object-oriented and prototype-based) programming types. Originally JavaScript was only used on the client side. JavaScript is now still used as a server-side programming language. To summarize, we can say that JavaScript is the language of the Internet.
https://www.javascript.com/



Welcome to programmierfrage.com

programmierfrage.com is a question and answer site for professional web developers, programming enthusiasts and website builders. Site created and operated by the community. Together with you, we create a free library of detailed answers to any question on programming, web development, website creation and website administration.

Get answers to specific questions

Ask about the real problem you are facing. Describe in detail what you are doing and what you want to achieve.

Help Others Solve Their Issues

Our goal is to create a strong community in which everyone will support each other. If you find a question and know the answer to it, help others with your knowledge.

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Yii2 500 internal server error
  • Yii2 404 error
  • Yii log error
  • Yii form error
  • Yii an internal server error occurred

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии