Active Record


Оглавление



Введение #

К оглавлению

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



Обратите внимание, что нотации тоже должны совпадать. Если Вам необходимо соблюдать рекомендации PSR-1, то либо поля в таблице БД нужно называть в стиле CamelCase, либо свойства в snake_case. Система не конвертирует атрибуты.

Active Record использует конструтор запросов SQL, поэтому необходимо настроить его, прежде чем начать работу.



Создание классов моделей и таблиц БД #

К оглавлению

Для использования воможностей Active Record, нужно создать класс модели, отнаследовав его от класса ActiveRecord\Model.
1
2
3
4
5
6
7
8
9
10
11
<?php        

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model
 
class 
User extends Model    
{     
    




Этот класс будет сопоставлен с таблицей СУБД users. По соглашению, таблицы должны называться так же, как класс модели, только во множественном числе и нижнем регистре. Если класс назван в нотации CamelCase, имя будет представлено в стиле snake_case c перводом последнего слова во множественное число: BlogPost => blog_posts. Проверить правильность конвертации можно в этой форме:

user_dates


Если есть потребность назвать таблицу иначе, нужно в классе модели объявить свойство $table. Однако мы рекомендуем установить это свойство вручную в любом случае, так как это снижает ресурсоемкость за счет отсутствия автоматической конвертации.
1
2
3
4
5
6
7
8
9
10
11
12
<?php          

namespace App\Models;    

use 
ABC\Core\ActiveRecord\Model;  
  
class 
User extends Model     
// В таком случае название таблицы не конвертируется.    
    
protected $table "admin_role";     
}  






Настройка #

К оглавлению

В классе модели нельзя объявлять конструктор, иначе он перекроет конструктор родителя. Для начальной настройки, вместо конструктора можно прописать метод settings(), который сработает аналогично. Допустим так можно установить название таблицы, если не нравится делать это напрямую через свойства:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php        

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model

class 
User extends Model    
{  // Этот метод работает как конструктор 
    
protected function settings() 
    { 
        
$this->setTableName('admin_role'); 
    } 





Так же имеется несколько констант для глобальных настроек моделей. Со списком сеттеров можно ознакомиться здесь. Константы в этом разделе.



Создание модели #

К оглавлению

Создать модель можно с помощью статического метода create(). Если вызвать его без параметров, будет создана "заготовка" - объект без сохранения в базе данных. Чтобы сохранить запись, нужно воспользоваться методом save(). Задать или изменить атрибуты (свойства, сопоставленные с полями таблицы), можно просто задав значения соответствующим свойствам.
1
2
3
4
5
6
7
8
9
10
11
12


        
// Создаем заготовку модели
        
$user User::create();
        
// Задаем значения аттрибутам
        
$user->login 'Karl_Marx';
        
$user->password md5('qwerty');
        
// Сохраняем модель в СУБД
        
$user->save();
        
// Теперь ей присвоен идентификатор
        
var_dump($user->id); // int(1)




Стоит отметить, что по умолчанию система ожидает, что таблица снабжена автоинкрементным первичным ключем `id`. Если вы используете другое имя первичного ключа, нужно задать его в классе модели, установив свойство $primary
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php        

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model

class 
User extends Model    

// Этим свойством можно установить поле первичного ключа
    
protected $primary 'user_id'





Если в метод create() аргументом передать массив с заданными атрибутами, сохранение произойдет автоматически.

1
2
3
4
5
6
7
8
9


        
// Создаем и сохроняем модель одной командой
        
$user User::create(['login'    => 'Karl_Marx'
                              
'password' => md5('qwerty'),
                             ]);
        
        
var_dump($user->user_id);    // int(2)    






Массовая загрузка #

К оглавлению

Однако следует знать, что система защищена от массовой загрузки атрибутов, так как это небезопасно при использовании внешних данных. При таком способе нужно в метод allow() передать массив имен полей, разрешенных к масовой загрузке.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php        

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model

class 
User extends Model    
{  
    protected function 
settings() 
    { 
       
// Помечаем поля login и password разрешенными к массовой загрузке
        
$this->setAllow(['login''password']); 
    } 




Либо разрешить массовую загрузку, установив константу BULK_UPLOAD в true. Тогда методом setForbid() можно запретить отдельные поля.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php        

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model

class 
User extends Model    
{  // Эта константа разрешает всеобщую массовую загрузку
    
const BULK_UPLOAD true

    protected function 
settings() 
    { 
// Этот метод пометит атрибут role запрещенным
        
$this->setForbid(['role']); 
    } 






Получение моделей #

К оглавлению

Для получения готовых моделей ActiveRecord есть несколько различных способов.

Простые:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

      
// возвращает пользователя с идентификатором 2 
        
$user User::findById(2); 
         
      
// возвращает массив пользователей с идентификаторами 1, 2, 3, 4 
        
$users User::findById([1234]);     
         
      
// возвращает модель на основе первой записи из таблицы 
        
$user User::findOne();         
         
      
// возвращает первую модель на основе записи, удовлетворяющей условию 
        
$user User::findOne(['id'     => 2,  
                               
'status' => User::STATUS_ACTIVE]);         
         
      
// возвращает все модели в виде массива 
        
$users User::findAll();             
         
      
// возвращает все модели, удовлетворяющей условию, в виде массива 
        
$users User::findAll(['status' => User::STATUS_ACTIVE]); 

      
// то же, что и findAll(), но возвращает результат в виде генератора 
        
$users User::findEach(['status' => User::STATUS_ACTIVE]); 

      
// возвращает все модели в виде массива по произвольному запросу 
        
$users User::findBySql('SELECT * FROM {{%users}} WHERE id = :id', [':id' => 4]);





Любой из этих методов, кроме последнего, принимает вторым необязательным параметром перечень полей в таблице, которые будут помещены в модель. По умолчанию заносятся все поля. Первичный ключ указывать не обязательно, он заносится в любом случае.
1
2
3
4
5

      
// возвращает модель с атрибутами 'id', 'login' и 'password' 
      // пользователя с идентификатором 2
        
$user User::findById(2, ['login''password']);




Второй способ - c помощью конструктора запросов, методoм find() и методами выборки.
1
2
3
4
5
6
7
8
9
10
11
12
13


        
// SELECT * FROM `users` LIMIT 1
        
$user User::find()->one();     
    
        
// SELECT * FROM `users` WHERE `login` LIKE(%Karl%) LIMIT 1
        
$user User::find()->where(['like''login''%Karl%'])->one();    
    
        
// SELECT * FROM `users` WHERE `login` LIKE (%Karl%) AND `id` IN (2, 4, 6)
        
$users User::find()->where(['like''login''%Karl%'])
                             ->
andWhere(['in''id', [246]])
                             ->
all();




Пакетная выборка
При работе с большими объемами данных, метод all() не всегда подходит, потому что он требуют загрузки всех данных в память. Чтобы сохранить требования к памяти минимальными, имеются так называеме пакетные выбороки.

По сравнению с all(), пакетная выборка загружает в память столько строк за раз, сколько указано в аргументе. При этом выполняется один запрос на каждую партию. Если вы обрабатываете данные и затем сразу удаляете их, пакетная выборка может помочь уменьшить использование памяти.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


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

        
        
// получить 10 покупателей одновременно в виде генератора, и перебрать по одному
        
$users User::find()->each(10);
        
        foreach (
$users as $user) {
            
// объект User
            
var_dump($user);
        }        




Примечание! Массивы любой множественной выборки индексированы по PrimaryKey. Другими словами имеют ключами значения первичных ключей таблицы.

Есть еще несколько вспомогательных методов, облегчающих работу с получением моделей:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


        
// Вернет количество строк, удовлетворяющих условию
        
$cnt User::countByCnd(['status' => User::STATUS_ACTIVE]);
        
var_dump($cnt); // int(3)

        // Вернет количество строк по произвольному SQL выражению
        
$cnt User::countBySql("SELECT * FROM {{%users}} WHERE status = :status"
                                [
':status' => User::STATUS_ACTIVE]);
        
var_dump($cnt); // int(3)        
        
        // Определяеет, есть ли хоть одна строка, отвечающая условиям.
        
$ex User::exists(['status' => User::STATUS_ACTIVE]);
        
var_dump($ex); // bool(true)    






Работа с данными #

К оглавлению

Данные в модели называются атрибутами. Получить или изменить атрибуты можно обратившись к свойствам, соответствующим полям строки из таблицы базы данных:
1
2
3
4
5
6
7
8

      
// получаем значения полей `login` и `email` для пользователя с ID = 2
        
$user User::findById(2); 
        
$login $user->login;
        
$email $user->email;





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

Несмотря на то, что получение данных в виде Active Record объектов является удобным и гибким, этот способ не всегда подходит при получении большого количества данных из-за больших накладных расходов памяти. В этом случае вы можете получить данные в виде PHP-массива, используя перед выполнением запроса метод asArray(). Так же иногда полезно получить модели в виде ValueObject (только для чтения). За это отвечает метод asObject()
1
2
3
4
5
6
7
8
9
10
11
12

 
      
// получаем значения первой строки в виде массива 
        
$user User::find()->asArray()->one(); 
         
      
// получаем все модели в виде объектов StdClass 
        
$users User::find()->asObject()->all();    

      
// вернуть обратно вид результата можно передав аргументом false 
        
$user->asArray(false);
        
$user->asObject(false);




Еще можно получить сериализованный в JSON массив атрибутов, обратившись к объекту, как к строке:
1
2
3
4
5


        $user 
User::findById(1);
        
var_dump((string)$user); // {"id":1,"login":"Karl_Marx","password".....






Задание типов атрибутам #

К оглавлению

Бывает, что некоторые атрибуты модели вам надо приводить к определенному типу данных. Сделать это можно методом setTypes()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php      

namespace App\Models;  

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
settings()
    {
// Устанавливаем атрибуту status булев тип
        
$this->setTypes(['status' => 'boolean']);
    }
}





Несмотря на то, что в БД status хранится в целочисленном (tinyint) поле, в модели он будет хранится в виде логической переменной - true или false.

Фреймворк поддерживает следующие типы:

1. integer - значение будет приведено к типу int
2. string - значение будет приведено к типу string
3. double - значение будет приведено к типу float
4. boolean - значение будет приведено к типу boolean
5. array - значение будет сериализовано в JSON при сохранении, и раскодировано при получении в массив
6. object - значение будет сериализовано в JSON при сохранении, и раскодировано при получении в объект

Особенно полезны последние два типа. Они позволяют без предварительных преобразований хранить в базе массивы и объекты:
1
2
3
4
5
6
7
8
9
10
11
12
13


        $user 
User::find();
        
// Динамически устанавливаем атрибуту options тип array
        
$user->setTypes(['options' => 'array']);
        
$user->one();
        
// $options - массив, полученный из JSON
        
$options $user->options;
        
// options автоматически сериализуется обратно в JSON
        
$user->options = ['foo' => 'bar'];







Преобразователи #

К оглавлению

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

Для этого в классе модели нужно объявить метод-читатель (accessor).
1
2
3
4
5
6
7
8
9
10
11
<?php      

class User extends Model    
{  
    
// Метод будет вызван при обращении к свойству birthday 
    
protected function geBirthdayAttr() 
    { 
        return 
date('d.m.Y'$this->attributes['birthday']); 
    } 




Метод должен называться по такой схеме: getAttributeAttr(). Причем часть Attribute, это имя атрибута в верблюжьей нотации, не смотря на нотацию поля в базе данных. Проверить правильность написания метода можно в этой форме:


Теперь, при обращении к атрибуту birthday, будет вызван этот метод. Он вернет преобразованные данные, а данные в модели останутся прежними:
1
2
3
4
5
6
7

    
        $user 
User::findById(1);
        
// Получаем преобразованное значение атрибута birthday
        
$birthday $user->birthday// 15.06.2000




Аналогично и с мутатором (mutator): методом, изменяющим данные при помещении их в модель. Различие в том, что начинаться он должен на set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php      

namespace App\Models;  

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    
// Метод будет вызван при попытке что-либо записать 
    // в свойство first_name
    
protected function setFirstNameAttr($value)
    {
        return 
strtoupper($this->attributes['first_name']) .' - '$value;
    }
}




Теперь, если мы захотим изменить значение атрибута first_name, сработает этот метод.
1
2
3
4
5
6
7
8
9


        $user 
User::findById(1);
        echo 
$user->first_name// Karl
        // Пытаемся присвоить новое значение:
        
$user->first_name ' батоно!';
        
// Теперь значение first_name в модели изменилось 
        
echo $user->first_name;    // KARL, батоно!






Использование конструктора запросов #

К оглавлению

Иногда, при разработке бизнес-логики модели, могут потребоваться дополнительные данные. Допустим из таблиц без первичных ключей (так называемых куч), либо какие то составные сложные запросы. Для этого в Active Records имеется доступ ко всем возможностям конструктора запросов.

И, хотя это не совсем вписывается в концепцию Active Records, иногда может оказаться полезным:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


        
// Внутри модели
        
$admins self::DB()->select()
                            ->
from('admins')
                            ->
where('status=:status', [':status' => 3])
                            ->
queryAll();
                            
        
// Снаружи модели
        
$profile User::DB()->select('id, login, profile')  
                             ->
from('user u')  
                             ->
innerJoin('profile p''u.id=p.user_id')  
                             ->
where('id=:id',  [':id' => $id] )  
                             ->
queryRow();






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

К оглавлению

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

Связь один-к-одному #
К разделу

Для работы со связными данными посредством Active Record вы прежде всего должны объявить связи в классе Active Record. Эта задача решается простым объявлением методов получения связных данных для каждой интересующей вас связи как показано ниже:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   
// Метод сработает при первом обращении к свойству profile
    
protected function getProfile()
    {
        return 
$this->hasOne(Profile::class);
    }
}




Теперь, при первом обращении к свойству profile, будет задействован этот метод, который синициирует запрос для получения связанных данных. Результат будет помещен в свойство profile модели User.
1
2
3
4
5
6
7
8


        $user 
User::findById(2);
        
// Будет выполнен запрос SELECT * FROM profiles WHERE user_id = id LIMIT 1
        
$profile $user->profile;
        
// Запроса не будет. Данные получены из свойства модели.
        
$profile $user->profile;




Нужно отметить, что запрос будет выполнен только при обращении к свойству, не раньше, так называемая "отложенная выборка".

По умолчанию система ожидает, что поле для связи будет названо по имени вызывающей модели, и постфикса _id (в данном случае это user_id) Однако можно установить свое название, передав его вторым аргументом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getProfile()
    {   
// Задаем собственное поле для связи
        
return $this->hasOne(Profile::class, 'id_user');
    }
}




Если нужно сопоставить его не с id, с с собственным первичным ключем, или другим полем, то такую связь нужно передать вторым аргументом ввиде массива:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getProfile()
    {   
// Задаем связь по собственному первичному ключу, 
        // или другому полю, передав второй аргумент в виде массива
        
return $this->hasOne(Profile::class, ['user_id' => $this->primary]);
    }
}




Кроме всего этого, можно задать несколько условий для связи:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php        

namespace App\Models;    

use 
ABC\Core\ActiveRecord\Model

class 
User extends Model    
{  
    protected function 
getProfile() 
    {   
// Задаем связь с более жестким условием
        // SELECT * FROM profile WHERE id_user = (id) AND role = (status) 
        
return $this->hasOne(Profile::class, ['user_id' => 'id'
                                              
'role'    => 'status']); 
    } 




Примечание! По умолчанию будут выбраны все поля строки запроса. Третьим, необязательным аргументом, можно задать только необходимые поля.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getProfile()
    {   
// Задаем третьим параметром необходимые для выборки поля
        // Первичный ключ будет выбран автоматически
        
return $this->hasOne(Profile::class, NULL'birthday, role');
    }
}




Связь один-ко-многим. #
К разделу

Всё, что мы рассматривали выше, относится к связи один-к-одному. Но часто требуется связь один-ко-многим. Допустим нужно выбрать все сообщения одного автора. Для этого есть метод hasMany(). Он работает аналогично hasOne(), но возвращает массив моделей, удовлетворяющим условиям связи:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getMessage()
    {   
// Получаем все сообщения автора, отвечающие условию WHERE user_id = id
        
return $this->hasMany(Message::class);
    }
}




Обратная связь. #
К разделу

Если требуется найти наоборот, "хозяина" модели, допустим автора книги, нужна так называемая обратная связь. Самый простой способ сделать это - изменить порядок ключей в методе hasOne(). Но можно воспользоваться более простым и выразительным методом belongsTo(). Он работает аналогично, но порядок ключей в нем обратный.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php      

namespace App\Models;  

use 
ABC\Core\ActiveRecord\Model;

class 
Message extends Model   

    public function 
getUser()
    {
// Получаем автора сообщения по запросу 
     // SELECT * FROM users WHERE id = (user_id)
        
return $this->belongsTo(User::class);
    }
}




Многие-ко-многим. #
К разделу

Отношения типа "многие-ко-многим" - более сложные, чем остальные виды отношений. Примером может служить пользователь, имеющий много ролей, где роли также относятся ко многим пользователям. Нужны три таблицы для этой связи: users, roles и role_user. Название таблицы role_user происходит от упорядоченного по алфавиту имён связанных моделей и должна иметь поля user_id и role_id.

Вы можете определить отношение "многие ко многим" через метод belongsToMany():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getRole()
    {   
// Получаем роли пользователя через промежуточную таблицу
        
return $this->belongsToMany(Role::class);
    }
}




Теперь, при обращении к свойству role, будет выполено два запроса. Первый в промежуточную таблицу, второй на выборку моделей, отвечающим связи "многие-ко-многим".
1
2
3
4
5
6
7
8


        $user 
User::findById(1);
        
// Будет выполнено два запроса 
        // SELECT role_id FROM role_user WHERE user_id = 1 
        // SELECT * FROM roles WHERE id IN (перечень найденных role_id)
        
$role $user->role;




Связь через модель. #
К разделу

Отношение "связь-через-модель" обеспечивает удобный короткий путь для доступа к удаленным отношениям через промежуточные отношения. Например, сделаем так, чтобы модель Country могла иметь много Post через модель User. Структура таблиц такая:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string




Для того, чтобы получить $country->posts, определим вот такое отношение hasManyThrough():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;  

use 
ABC\Core\ActiveRecord\Model;

class 
Country extends Model   

    protected function 
getPost()
    {   
// Получим все посты пользователей, относящихся к текущей стране
        
return $this->hasManyThrough(Post::class, User::class);
    }
}




Eсли наименование столбцов у вас в таблице отличается от общепринятого, вы можете указать их названия явно:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;  

use 
ABC\Core\ActiveRecord\Model;

class 
Country extends Model   

    protected function 
getPost()
    {   
// Можно установить свои ключи
        
return $this->hasManyThrough(Post::class, User::class, 'country_id''user_id');
    }
}




Теперь можно просто запросить посты напрямую у модели Country
1
2
3
4
5
6
7
8

   
        $country 
Country::findById(1);
        
// Будет выполнено два запроса
        // SELECT id FROM users WHERE id_country = 1
        // SELECT * FROM posts WHERE user_id IN (перечень найденных id)
        
$post $country->post;




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

К оглавлению

При осуществдении связи, SQL запрос выполняется только один раз, при первом обращении к свойству. Дальше он будет получен из кэша. Это называется "отложенная загрузка".
1
2
3
4
5
6
7
8
9
10
11
12
13

  
        
// SELECT * FROM `users` WHERE `id` = 123 
        
$user User::findOne(123); 

        
// SELECT * FROM `posts` WHERE `user_id` = 123 
        
$post $user->post

        
// SQL-запрос не выполняется 
        
$post2 $user->post





Отложенная загрузка очень удобна в использовании. Однако этот метод может вызвать проблемы производительности, когда вам понадобится получить доступ к тем же самым свойствам связей для нескольких объектов Active Record. Рассмотрим следующий пример кода.
1
2
3
4
5
6
7
8
9
10
11
12
13
14


        
// SELECT * FROM `users` LIMIT 20 
        
$users User::find()->limit(20)->all(); 
         
        foreach (
$cusers as $user) { 
            
// При каждой итерации будет выполнен запрос
            // SELECT * FROM `posts` WHERE `user_id` = ... 
            
$posts $user->post;  
        }  






В таком случае будет выполнен 21 запрос. Это произойдёт из-за того, что при получении доступа к свойству связи orders каждого отдельного объекта User, будет выполняться новое обращение к таблице posts.

Для решения этой проблемы вы можете воспользоваться способом, который называется жадная загрузка. Для этого используется метод with()
1
2
3
4
5
6
7
8
9
10
11
12


        
// SELECT * FROM `users` LIMIT 20;  
        
$users User::find()->with('post'// добавляем связь для жадной загрузки
                             
->limit(20
                             ->
all();   
        
// При первой итерации будет выполнен запрос 
        // SELECT * FROM `posts` WHERE `user_id` IN (...) 
        
foreach ($users as $user) { 
            
$posts $user->post;     
        } 




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

Нужно отметить, что жадная загрузка работает только со связью один-ко-многим (метод hasMany())
1
2
3
4
5
6
7
8
9
10
11
12
13
14


namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getPost() 
    {   
        return 
$this->hasMany(Post::class); 
    } 
}




Примечание! Так как заранее не известно, сколько потребуется связанных моделей, в целях экономии ресурсов их число ограничено 50-ю. Если нужно больше, можно установить константу RELATIONS

Можно одновременно жадно загрузить несколько связей, перечислив из через запятую:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


        
// SELECT * FROM `users` LIMIT 20;  
        
$users User::find()->with('group''post'
                             ->
limit(20
                             ->
all();   

        
// При первой итерации будут выполнены запросы         
        
foreach ($users as $user) {
        
// SELECT * FROM `groups` WHERE `user_id` IN (...) 
            
$groups $user->group;
        
// SELECT * FROM `posts` WHERE `user_id` IN (...) 
            
$posts $user->post;    
        } 




Будет выполнено всего три запроса.

Связи должны выглядеть примерно так:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    
// Связь для групп
    
protected function getGroup() 
    {   
        return 
$this->hasMany(Group::class); 
    } 
    
// Связь для постов
    
protected function getPost() 
    {   
        return 
$this->hasMany(Post::class); 
    } 
}




Так же можно задать условия для связи, передав их анонимной функцией:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

  
        
// SELECT * FROM `users` LIMIT 20; 
        
$users User::find()->with(['profile'
                                     
'post' => function ($query) {
                                        return 
$query->andWhere('active = 1');
                                   }])
                             ->
limit(20)
                             ->
all();        
                               

        
// При первой итерации будет выполнен запрос        
        
foreach ($users as $user) {
        
// SELECT * FROM `posts` WHERE `user_id` IN (...) AND `active` = 1
            
$posts $user->post
        }




Еще поддерживается жадная загрузка вложенных связей. Она тоже являются отложенныой, запрсы на выборку будут выполняться только при обращении к вложенным связям:
1
2
3
4
5
6
7
8
9
10
11
12
13

  
        
// SELECT * FROM `users` LIMIT 20; 
        
$users User::find()->with(['post.photo'])
                             ->
limit(20)
                             ->
all();        
                               
       
        
// SELECT * FROM `posts` WHERE `user_id` IN (...)
            
$posts $users[1]->post
        
// SELECT * FROM `photo` WHERE `post_id` IN (...)            
            
$photo $posts[1]->photo;




Ну и сами связи должны быть организованы в моделях:
Главная - модель User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php      

namespace App\Models;   

use 
ABC\Core\ActiveRecord\Model;

class 
User extends Model   

    protected function 
getPost() 
    {   
        return 
$this->hasMany(Post::class); 
    } 
}




И вложенная в неё модель Post
1
2
3
4
5
6
7
8
9
10
11
12
13
14


namespace App\Models;  

use 
ABC\Core\ActiveRecord\Model;

class 
Post extends Model   

    protected function 
getPhoto() 
    {   
        return 
$this->hasMany(Photo::class); 
    } 
}




Соответственно в неё вложена модель Photo, но у неё нет связи, так как она конечная.
Вложенноcть моделей неограничена.

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

К оглавлению

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

1. Создайте заготовку модели с помощью метода create(), либо осуществите выборку нужной модели.
2. Присвойте новые значения атрибутам Active Record
3. Вызовите метод save() для сохранения данных в базу данных.

Например:
1
2
3
4
5
6
7
8
9
10
11
12
13


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




Еще имеется возможность пометить некоторые атрибуты, как игнорируемые для записи в базу данных. Это может оказаться полезным для временных атрибутов, либо для блокировки. Сделать это можно методом setIgnored()
1
2
3
4
5
6
7


        
// По одному
        
$user->setIgnored('name');
        
// Или сразу много
        
$user->setIgnored(['name''surname''phone']);






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

К оглавлению

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


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




При этом все данные из объекта удаляются и он становится заготовкой.

Транзакции #

К оглавлению

Для транзакций можно использовать штатные методы конструктора запросов.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26


    $customer 
Customer::findOne(123);
    
    
Customer::DB()->transaction(function($db) use ($customer) {
        
$customer->id 200;
        
$customer->save();
        
// ...другие операции с базой данных...
    
});
    
    
// или по-другому
    
    
$transaction Customer::DB()->beginTransaction();
    try {
        
$customer->id 200;
        
$customer->save();
        
// ...другие операции с базой данных...
        
$transaction->commit();
    } catch(\
Exception $e) {
        
$transaction->rollBack();
        throw 
$e;
    } catch(\
Throwable $e) {
        
$transaction->rollBack();
        throw 
$e;
    }





Active Record представляет для работы следующие методы #

К оглавлению

Защищенные методы
1 setAllow() Установка разрешений к заполнению для атрибутов
2 setForbid() Установка запрещений к заполнению для атрибутов
3 setTableName() Устанавливает имя связанной таблицы
4 setIgnored() Установка игнорируемых атрибутов
5 settings() Метод для первоначальных настроек
6 setTypes() Устанавливает типы атрибутов
Примечание! Эти методы работают только внутри модели.

Публичные методы
1 __get() Магический метод
2 __set() Магический метод
3 __toString() Магический метод
4 all() Возвращает семейство моделей.
5 asArray() Устанавливает тип результата (массив)
6 asObject() Устанавливает тип результата (объект)
7 batch() Возвращает семейство моделей партиями
8 belongsTo() Получение обратной связи
9 belongsToMany() Получение связи многие-ко-многим
10 command() Выборка с помощью подготовленного конструктора запросов
11 countByCnd() Возвращает количество строк, удовлетворяющих условию
12 countBySql() Возвращает количество строк по SQL выражению.
13 create() Создание новой модели
14 DB() Возвращает конструктор запросов
15 delete() Удаление записи
16 each() Возвращает семейство моделей по одной
17 exists() Проверяет, есть ли хоть одна строка, удовлетворяющая условию.
18 find() Возвращает заполненные объекты модели с помощью SQL-конструктора
19 findAll() Возвращает заполненные объекты модели, удовлетворяющие критериям
20 findById() Возвращает заполненный(ые) объект(ы) модели по первичному ключу(ам)
21 findBySql() Возвращает заполненные объекты модели по SQL выражению
22 findEach() Возвращает заполненные объекты модели, удовлетворяющие критериям, в виде генератора
23 findOne() Возвращает заполненный объект модели, удовлетворяющий условию
24 hasMany() Получение связанных моделей в виде массива
25 hasManyThrough() Получение связанных моделей "через одну"
26 hasOne() Получение связанной модели "один-к-одному"
27 one() Возвращает одну готовую модель
28 save() Сохранение модели в БД
29 with() Жадная загрузка




Методы по разделам #

К оглавлению

Настройка #
К списку разделов

settings() protected method
К разделу

Метод настроек.

Этот метод работает по принципу конструктора, так как использование штатного __construct() недопустимо.
Пример использования здесь.

protected ActiveRecord::settings ()
return void




setTableName() protected method
К разделу

Установка привязаной таблицы СУБД.

Этим методом можно установить собственное имя таблицы СУБД, если по каким то причинам не подходит внутреннее соглашение.
Пример использования здесь.

protected ActiveRecord::setTableName ($table)
$table string Название таблицы СУБД
return void




setTypes() protected method
К разделу

Задает типы атрибутам.

Задает типы, передав параметром массив с названиями и типами атрибутов.
Пример использования здесь.

protected ActiveRecord::setTypes ($types)
$types array Массив, где ключами являются имена атрибутов, значениями - их типы.
Поддерживаются следующие типы:
1. integer - значение будет приведено к типу int
2. string - значение будет приведено к типу string
3. double - значение будет приведено к типу float
4. boolean - значение будет приведено к типу boolean
5. array - значение будет сериализовано в JSON при сохранении, и раскодировано при получении в массив
6. object - значение будет сериализовано в JSON при сохранении, и раскодировано при получении в объект
return void




setIgnored() protected method
К разделу

Помечает атрибуты на игнор.

Атрибуты, имена которых были переданы аргументом в этот метод, будут проигнорированы при записи в СУБД.
Пример использования здесь.

protected ActiveRecord::setIgnored ($attributes)
$attributes string|array Имя или список имен атрибутов ввиде массива, которые нужно пометить на игнор.
return void




setAllow() protected method
К разделу

Разрешение на массовое заполнение.

Атрибуты, имена которых были переданы аргументом в этот метод, будут разрешены для массового заполнения.
Пример использования здесь.

protected ActiveRecord::setAllow ($attributes)
$attributes string|array Имя или список имен атрибутов ввиде массива, которым нужно разрешить массовое заполнение.
return void




setForbid() protected method
К разделу

Запрещение на массовое заполнение.

Атрибуты, имена которых были переданы аргументом в этот метод, будут запрещены для массового заполнения.
Пример использования здесь.

protected ActiveRecord::setForbid ($attributes)
$attributes string|array Имя или список имен атрибутов ввиде массива, которым нужно запретить массовое заполнение. Метод актуален только при установленной в true константы BULK_UPLOAD
return void


Создание и получение моделей #
К списку разделов

create() public static method
К разделу

Создание модели.

Метод создаст заготовку модели, если вызван без параметра, либо заполненную переданными аргументом атрибутами модель.
Пример использования здесь.

public static ActiveRecord::create ($attributes = [])
$attributes array Массив атрибутов к массовому заполнению
return object Объект модели Active Record




findById() public static method
К разделу

Получение моделей по ID.

Метод возвращает модель по первичному ключу, или семейство моделей по списку ключей.
Пример использования здесь.

public static ActiveRecord::findById ($ids, $columns = null )
$ids int|array Идентификатор (первичны ключ) или список идентификаторов в виде массива
$columns string|array Строка через запятую или массив с перечнем полей для выборки. Необязательный аргумент. При отсутствии будут выбраны все поля.
return object|array Объект или массив объектов Active Record




findOne() public static method
К разделу

Получение модели по условию.

Метод возвращает модель по условию, переданному аргументом. Условием должен быть массив [название атрибута => значение]. Если условия не заданы, будут возвращена модель первой строки.
Пример использования здесь.

public static ActiveRecord::findOne ($conditions = null , $columns = null )
$conditions array Условия выборки в виде массива
$columns string|array Строка через запятую или массив с перечнем полей для выборки. Необязательный аргумент. При отсутствии будут выбраны все поля.
return object Объект Active Record




findAll() public static method
К разделу

Получение семейства моделей по условию.

Метод возвращает массив моделей, отвечающих условиям, переданными аргументом. Условием должен быть массив [название атрибута => значение]. Если условия не заданы, будут возвращены все модели.
Пример использования здесь.

public static ActiveRecord::findAll ($conditions = null , $columns = null )
$conditions array Условия выборки в виде массива
$columns string|array Строка через запятую или массив с перечнем полей для выборки. Необязательный аргумент. При отсутствии будут выбраны все поля.
return array Массив объектов Active Record




findEach() public static method
К разделу

Получение семейства моделей по условию.

Метод возвращает семейство моделей, отвечающих условиям, переданными аргументом, в виде генератора, для перебора по одной. Условием должен быть массив [название атрибута => значение]. Если условия не заданы, будут возвращены все модели.
Пример использования здесь.

public static ActiveRecord::findEach ($conditions = null , $columns = null )
$conditions array Условия выборки в виде массива
$columns string|array Строка через запятую или массив с перечнем полей для выборки. Необязательный аргумент. При отсутствии будут выбраны все поля.
return object Генератор объектов Active Record для перебора по одному




findBySql() public static method
К разделу

Получение моделей по SQL.

Метод возвращает модель по произвольному SQL-запросу.
Пример использования здесь.

public static ActiveRecord::findBySql ($sql, $params = [])
$sql string Произвольный SQL запрос
$params array Параметры для запроса
return object|array Объект или массив объектов Active Record




find() public static method
К разделу

Получение моделей с помощью SQL-конструктора.

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

public static ActiveRecord::find ($columns = null )
$columns string|array Строка через запятую или массив с перечнем полей для выборки. Необязательный аргумент. При отсутствии будут выбраны все поля.
return object Объект SQL-конструктора




Методы выборки #
К списку разделов

one() public method
К разделу

Возвращает одну готовую модель.

Завершающий метод, возвращающий модель найденную с помощью метода find() и заданных условий.
Пример использования здесь.

public ActiveRecord::one ()
return object Объект Active Record




all() public method
К разделу

Возвращает cемейство готовых моделей.

Завершающий метод, возвращающий массив с моделями, найденными с помощью метода find() и заданных условий.
Пример использования здесь.

public ActiveRecord::all ()
return array Массив объектов Active Record




each() public method
К разделу

Возвращает cемейство готовых моделей.

Завершающий метод, возвращающий модели, найденные с помощью метода find() и заданных условий. Отличие от all() в том, что результат возвращается по одной за проход.
Пример использования здесь.

public ActiveRecord::each ($amount = 1)
$amount int Количество возвращаемых моделей.
return array Объект генератора, возвращающего объекты Active Record по одной за проход




batch() public method
К разделу

Возвращает cемейство готовых моделей.

Завершающий метод, возвращающий массив с моделями, найденными с помощью метода find() и заданных условий. Отличие от all() в том, что результат возвращается партиями.
Пример использования здесь.

public ActiveRecord::batch ($amount = 1)
$amount int Размер партии, возвращаемой за один проход.
return array Объект генератора, возвращающего объекты Active Record партиями




Работа с данными #
К списку разделов



__get() public method
К разделу

Магический метод.

ЗМетод сработает при обращении к несуществующему свойству (атрибуту). Вернет либо модифицированный атрибут, либо атрибуты связанного объекта, если организована связь.
public ActiveRecord::__get ($name)
$name string Имя атрибута.
return mix Значение модифицированного атрибута, либо данные из связанного объекта