Работа с базой данных реализована через ORM. В нашей концепции ORM существует несколько основных сущностей, а именно:
- Model. Отвечает за организацию структуры данных.
- QuerySet. Отвечает за построение и выполнение запросов к базе данных.
- Manager. Некоторая прослойка между Model и QuerySet, позволяющая "вынести" часть логики из Model.
А теперь подробнее и понятнее.
class Company extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class,
'label' => 'Company name'
],
'tagline' => [
'class' => CharField::class,
'label' => 'Tagline',
'null' => true
]
];
}
}
Как мы видим, основным является описание структуры полей.
class - Класс поля, является обязательным
label - Наименование для отображения пользователю (н-р в форме редактирования, в админ-панели и т.д.)
hint - Подсказка для отображения пользователю
virtual - Является ли поле виртуальным, то есть значение данного поля в базе данных не хранится, по-умолчанию false
null - Может ли поле принимать null-значения, по-умолчанию false
blank - Может ли поле принимать пустые значения, по-умолчанию false
unsigned, zerofill - Атрибуты, соответствующие таковым в SQL, по-умолчанию false
default - Значение по-умолчанию, по-умолчанию null
editable - Давать ли возможность добавлять данное поле в автоматически-построенные формы, по-умолчанию true
choices - Список вариантов выбора (массив ключ-значение). Cм. ниже.
Перечисленные выше атрибуты являются стандартными. Значения по-умолчанию некоторых атрибутов для отдельных полей могу меняться (н-р для BigIntField атрибут unsigned по-умолчанию установлен в true) Для каждого типа поля могут быть так же свои атрибуты, об этом будет указано для каждого поля отдельно.
Часто бывает нужно со значениям, хранящимся в базе данных в формате int или varchar сопоставить "человекопонятные" обозначения. Статусы заказов, модерации, и т.д. Атрибут choices упрощает эту задачу. Ключом массива является значение, хранимое в базе данных, значением массива является строка.
Пример поля с описанным choices-атрибутом:
class Comment extends Model
{
const STATUS_MODERATION = 1;
const STATUS_PUBLISHED = 2;
const STATUS_DECLINED = 3;
public static function getFields()
{
...
'status' => [
'class' => IntField::class,
'label' => 'Статус модерации',
'choices' => [
self::STATUS_MODERATION => 'Модерация',
self::STATUS_PUBLISHED => 'Опубликовано',
self::STATUS_DECLINED => 'Отклонено'
]
]
...
}
}
Соответственно, в базе данных хранятся только значения 1,2,3, а пользователь будет работать со значениями, понятными для него. Как получить текстовое значение от поля с описанным choices-атрибутом? Всё достаточно просто:
$comment->status__display;
Как видим, необходимо обратится не непосредственно к полю status,
для которого определен атрибут choices, а добавить "волшебное" __display
и мы получим текстовое отображение соответствующего статуса.
Так же атрибует choices при автоматической генерации формы "превратит" это поле в выпадающий список, что бывает крайне удобно.
Еще одной интересной особенностью является возможность быстрого сравнения значения в БД и указанным значением из choices.
Например, проверить что данный комментарий опубликован можно следующим образом:
$comment->isStatusPublished;
Для того, чтобы данный метод работал, необходимо соблюдать ряд условий (достаточно простых):
- Значения, доступные в choices должны быть заданы константами класса
- Именования данных значений должны быть заглавными буквами с разделителем "_", при этом имя поля должно идти сначала, затем именование значения
- Имя поля должно быть задано одним словом
При соблюдении данных условий вы сможете пользоваться данным методом сравнения значений. В качестве примера корректного описания choices можно рассмотреть модель, указанную выше.
Перечислим основые типы полей ORM. Их можно условно разбить на несколько групп.
Поля, имеющие прямые аналоги в SQL и, по-сути, являющиеся лишь "оберткой" над SQL-значениями
- IntField
- BigIntField
- DecimalField
- FloatField
- CharField
- TextField
- DateField
- TimeField
- DateTimeField
- BooleanField
Поля, отвечающие за работу с файлами
Поля, отвечающие за работу со связями
А так же некоторые дополнительные поля, упрощающие определенные действия
А теперь чуть подробнее по каждому
Принимает целочисленные значения.
Дополнительный атрибут length соответствует таковому в SQL при описании полей.
По умолчанию у IntField - 11, у BigIntField - 20.
Принимает численные значения.
Тут указываем атрибуты:
precision - общее максимальное кол-во знаков, по-умолчанию 10
и
scale - кол-во цифр после запятой, по-умолчанию 2
Принимает текстовые значения соответствующей длины.
Атрибут length отвечает за максимальную длину хранимой строки.
При указании значения более 255, убедитесь что ваша версия СУБД это поддерживает
Принимает текстовые значения.
Аналог TEXT в SQL. Дополнительных атрибутов нет
Принимает строковые значения соответствущих форматов (по маскам "Y-m-d", "H:i:s", "Y-m-d H:i:s" соответственно)
Атрибуты:
autoNowAdd - поле примет значение текущего времени/даты при первом сохранении модели
autoNow - поле примет значение текущего времени/даты при любом сохранении модели
Удобно пользоваться этими атрибутами для создания полей вида created_at, updated_at с целью отслеживания даты добавления и изменения модели.
Принимает значения 1 или 0.
Аналог TINYINT(1) в SQL. Дополнительных атрибутов нет.
Поле, отвечающее за работу с файлами
Принимает значения:
- Локальный файл (LocalFile)
$model->file = new LocalFile('/www/sitename/sync/doc_file.pdf');
// OR
$model->file = '/www/sitename/sync/doc_file.pdf';
- Удаленный (доступный по ссылке на другом сервере) файл (RemoteFile)
$model->file = new RemoteFile('http://example.com/doc_file.pdf');
- Загруженный файл (UploadedFile). Используется при работе с формой.
Доступные атрибуты:
md5Name - Хэшировать имя файла в md5. Как правило, используется для того, чтобы избежать проблем с доступностью файлов имеющих некоторые проблемы с совместимостью кодировки имени файла на сервере.
templateUploadDir - Шаблон имени папки для загрузки файлов, по-умолчанию %Module/%Model/%Y-%m-%d
storage - Наименование хранилища файлов, по умолчанию - storage (использовать хранилище по-умолчанию)
Получение значения из поля:
- Получение ссылки на файл
$model->file->url
- Получение локального пути до файла
$model->file->path
- Получение дополниельной информации о файле
$model->file->getExtension();
$model->file->getPathFilename();
$model->file->getPathBasename();
$model->file->getSize();
Поле, отвечающее за работу с изображениями. Интегрировано с Imagine, использует его методы.
Поле унаследовано от FileField, поэтому установка значений в поле и атрибуты совпадают с FileField, но имеется часть дополнительных атрибутов:
options - Основные настройки для Imagine, отвечающие за качество изображения
storeOriginal - Сохранять ли оригинальный файл, по умолчанию - true
sizes - Размеры, до которых необходимо изменить исходное изображение
Пример:
class Product extends Model
{
public static function getFields()
{
...
'image' => [
'class' => ImageField::class,
'label' => 'Изображение',
'sizes' => [
'thumb' => [
300, 250,
'method' => 'cover'
],
'preview' => [
400, 300,
'method' => 'contain'
],
]
]
...
}
}
Значение атрибута sizes представляет собой массив, где ключом является наименование размера, а значением - его параметры. Первым параметром указывается ширина итогового изображения, вторым - высота. Так же именованным параметром 'method' указывается метод изменения размера изображения. Их всего 2 - cover и contain. Работают они так же, как и соответствующие значения css-атрибута background-size.
Получить ссылку на конкретный размер изображения можно вот так:
$model->image->url_preview;
$model->image->url_thumb;
Поле, отвечающее за связь вида "Многое-к-одному"
Атрибуты:
modelClass - класс модели с которой просходит свзязь.
Пример моделей:
class Post extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
];
}
}
class Comment extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'post' => [
'class' => ForeignField::class,
'modelClass' => Post::class
],
];
}
}
Установка значения (оба варианта равнозначны)
$comment->post = $post;
$comment->post_id = $post->id;
Получение значения
$post = $comment->post;
$postId = $comment->post_id;
В данных примерах $comment - экземпляр класса Comment, $post - экземпляр класса Post
Поле, отвечающее за связь вида "Один-ко-многим". По-сути, является обратной к ForeignField.
Атрибуты:
modelClass - класс модели с которой просходит свзязь.
to - поле в таблице связанной модели, через которую происходит привязка
from - поле в таблице текущей модели, через которую происходит привязка, по-умолчанию - первичный ключ данной модели
В качестве примера возьмем модели из предыдущего поля, лишь немного изменим модель Post, указав наше поле
class Post extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'comments' => [
'class' => HasManyField::class,
'modelClass' => Comment::class,
'to' => 'post_id',
]
];
}
}
Установка значений напрямую через данное поле запрещена, для этого необходимо менять соответствующий атрибут в связанных моделях.
В качестве значения из данного поля мы получаем HasManyManager, с помощью которого можем работать с набором связанных моделей. Подробнее о Менеджерах и QuerySet cм. ниже.
Например, получение всех комменатриев к определенному посту:
$comments = $post->comments->all();
Поле, отвечающее за связь вида "Многие-ко-многим".
В первую очередь, скажем о том, что для реализации данной связи необходима дополнительная таблица. Эта таблица может содержать только идентификаторы неодходимых записей в связанных таблицах, либо еще какую-то дополнительную информацию. Поэтому, варианта работы с данным полем 2 - с определением "промежуточной" модели или без таковой
Для наглядности опишем примеры моделей и разберем работу на их основе
class User extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'groups' => [
'class' => ManyToManyField::class,
'modelClass' => Group::class
],
];
}
}
class Group extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'users' => [
'class' => ManyToManyField::class,
'modelClass' => User::class
],
];
}
}
Установка значений может производится несколькими методами:
- Передача массива объектов / идентификаторов в поле. Связь с данными моделями произойдет после вызова метода save() у принимающей модели.
$user->groups = [$group1->id, $group2];
$user->save();
Данный способ так же является универсальным, так как у модели $user на момент установки значения в поле groups может не быть идентификатора (она пока не существует в БД)
- Добавление/удаление связей через менеджер Добавление
$user->groups->link($group1);
$user->groups->link($group2);
Удаление
$user->groups->unlink($group1);
$user->groups->unlink($group2);
Для $groups->users справедливо всё это же самое, они работают с одной и той же таблицей.
В качестве значения из данного поля мы получаем ManyToManyManager, с помощью которого можем работать с набором связанных моделей. Подробнее о Менеджерах и QuerySet cм. ниже.
Например, получение всех групп к определенного пользователя:
$groups = $user->groups->all();
В данном случае определение будет выглядеть следующим образом:
class Blogger extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'subscribes' => [
'class' => ManyToManyField::class,
'modelClass' => static::class
],
'subscribers' => [
'class' => ManyToManyField::class,
'modelClass' => static::class,
'back' => 'subscribes'
]
];
}
}
Чтобы указать, что связь subscribers является "обратной стороной" связи subscribes достаточно определить параметр back соответствующим образом.
Аналогично, опишем работу на основе примера. Видоизменим предыдущий пример, добавив промежуточную модель для хранения роли пользователя в конкретной группе.
class User extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'groups' => [
'class' => ManyToManyField::class,
'modelClass' => Group::class,
'through' => Membership::class
],
];
}
}
class Group extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'users' => [
'class' => ManyToManyField::class,
'modelClass' => User::class,
'through' => Membership::class
],
];
}
}
class Membership extends Model
{
public static function getFields()
{
return [
'role' => [
'class' => CharField::class
],
'user' => [
'class' => ForeignField::class,
'modelClass' => User::class
],
'group' => [
'class' => ForeignField::class,
'modelClass' => Group::class
]
];
}
}
Работа с данным видом ManyToMany связи аналогична работе без промежуточной модели, за исключением дополнительных возможностей.
Установка значений
Очевидно, что добавлять связи можно обычным созданием промежуточной модели
$memebership = new Membership();
$membership->user = $user;
$membership->group = $group;
$membership->role = 'Director';
$membership->save();
Удалять связи можно удалением промежуточной модели
$membeship->delete();
При добавлении записей через менеджер, дополнительно можно указывать атрибуты промежуточной модели
$user->groups->link($group1, [
'role' => 'Director'
]);
Кроме этого, промежуточную модель можно использовать для работы с QuerySet. Например, выбрать всех пользователей, которые хотя бы в одной группе являются руководителями:
User::objects()->filter(['membership__role' => 'Director'])->all();
Поле, полностью повторяющее функциональность CharField, за исключением конвертирования данного поля в автоматическую форму. В автоматической форме добавляется проверка на корректность введеного email-адреса.
Поле, позволяющее хранить транслитерированное значение другого поля модели. Это может быть необходимо для построения человеко-понятных Url-адресов (ссылок). Использует сторонний компонент для транслитерации Cocur\Slugify.
Атрибуты:
tree - Строить ли древовидную ссылку для Tree-модели. Cм. ниже. По-умолчанию true для Tree-моделей
unique - Проверять ли итоговое значение на уникальность. См. ниже. По-умолчанию true
rulesets - Списки правил для Slugify. Подробнее - см. документацию по Cocur\Slugify. По-умолчанию ['russian', 'default']
lowercase - Приводить ли полученное значение к нижнему регистру. По-умолчанию true
regexp - Регулярное выражение для Slugify. Подробнее - см. документацию по Cocur\Slugify. По-умолчанию null
separator - Разделитель слов в итоговом значении
source - Имя поля в модели над значением которого будут производится манипуляции. Другими словами - откуда брать данные для транслитерации. По-умолчанию 'name'
Часто, для древовидных моделей необходимо строить ссылку, соответствующую их положению в дереве с указанием всех родительских элементов.
Например, для категорий продукции, представленных в виде дерева. Для категории "Полноразмерные наушники", расположенной в категории "Наушники", которая в свою очередь расположена в категории "Аудиотехника" будет необходима ссылка вида: "audiotehnika/naushniki/polnorazmernie-naushniki". Именно такую ссылку и построит SlugField в Tree-модели с атрибутом tree, установленном в true
Чтобы избежать коллизий с одинаковыми значениями, SlugField может проверять уникальность значения и генерировать только уникальные значения.
Например, у нас имеется модель с именем "Тест", для нее построится значение "test". Далее, мы добавляем еще одну модель с таким же названием. При указании значения true атрибута unique мы получим значение "test-1". Если и далее мы продолжим добавлять аналогичные модели данное поле будет принимать значения "test-2", "test-3", "test-4" и т.д.
Поле, позволяющее хранить в базе данных json-сериализованные данные, а работать через модель как с обыкновенными данными.
Принцип работы поля состоит в том что для сохранения БД значение данного поля преобразуется в json-строку, а после извлечения из БД json-строка десереализуется для работы через модель.
Установка данных в поле (примем поле data в модели model объявленным как JsonField)
Устанавливаются значения, которые могут быть корректно преобразованы в json-строку (строки, числовые значения, массивы):
$model->data_first = "String";
$model->data_second = 1;
$model->data_third = ['Array', 'of', 'strings'];
Получаемые значения (при корректных исходных данных) совпадут с устанавливаемыми:
$value1 = $model->data_first; // "String";
$value2 = $model->data_second; // 1
$value3 = $model->data_third; // ['Array', 'of', 'strings']
Поля data_first, data_second и data_third описаны в модели $model как JsonField.
Применяемость.
Крайне удобно хранить в данном поле списки значений (массивы), однако стоит помнить, что без модели (например, в методе QuerySet::values()) извлечь данные будет сложнее (их придется вручную десереализовывать). Так же, возникают сложности с построением запросов с применением данного поля.
Не храните в данном поле связи с другими моделями - это некорректно! Используйте поля ForeignField, HasManyField, ManyToManyField.
Поле, предназначенное для хранения положения сортировки моделей. По-умолчанию принимает следующее после максимального существующего значения.
Атрибут relations используется для хранения значения сортировки, связанного с другими моделями. Например, значение сортировки для характеристики товара не должно быть сквозным, а должно варьироваться в рамках одного товара.
В качестве примера опишем такую модель:
class ProductCharacter extends Model
{
public static function getFields()
{
return [
'product' => [
'class' => ForeignField::class,
'modelClass' => Product::class
],
'value' => [
'class' => CharField::class
],
'position' => [
'class' => PositionField::class
],
];
}
}
Теперь, при сохранении характеристики значение поля position будет сквозным, то есть для первого товара будут характеристики со значениями position 1,2,3; для второго товара 3,4,5 и так далее.
Укажем атрибут relations для поля position вот так:
...
'position' => [
'class' => PositionField::class,
'relations' => ['product']
],
...
Теперь для первого товара характеристики будут сохраняться со значениями position 1,2,3; для второго товара 1,2,3 и так далее.
Поле предназначено для упрощения вывода значений ForeignField в автоматически созданную форму. Применимо вместо ForeignField для связи с Tree-моделью. Древовидная структура будет отображаться более наглядно.
Работа с моделью подразумевает 4 привычных нам действия с данными:
- Добавление (создание)
- Выбор
- Изменение
- Удаление
Покажем на примере все эти действия.
Для примера возьмем модель компании
class Company extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class,
'label' => 'Company name'
],
'tagline' => [
'class' => CharField::class,
'label' => 'Tagline',
'null' => true
]
];
}
}
Создадим модель:
$company = new Company();
$company->name = 'Bar company';
$company->tagline = 'Go to bar, be amazing';
$company->save();
После вызова метода save(), который записывает данные нашей модели в БД, в модели $company будет доступен атрибут id, который отвечает за хранение первичного ключа (идентификатора) модели.
Выбрать модель из базы данных можно по различным признакам.
Например, найдем нашу модель по наименованию:
$company = Company::objects()->filter(['name' => 'Bar company'])->get();
Или по слогану:
$company = Company::objects()->filter(['tagline' => 'Go to bar, be amazing'])->get();
Можно выбрать не одну конкретную модель, а, например, все:
$companies = Company::objects()->all();
Подробнее про Менеджеры (в частности, про менеджер objects) и QuerySet читайте далее
Для того чтобы было что изменить нам необходимо выбрать нашу модель:
$company = Company::objects()->filter(['name' => 'Bar company'])->get();
Теперь изменим слоган нашей компании:
$company->tagline = 'Be amazing with Bar';
$company->save();
Метод save() записывает (применяет) изменения в БД.
Опять же, выберем модель нашей компании из БД:
$company = Company::objects()->filter(['name' => 'Bar company'])->get();
И удалим ее:
$company->delete();
После вызова данного метода запись о нашей компании удаляется из базы данных.
Часто бывает так что с моделью необходимо проводить манипуляции в определенные моменты ее состояния - сообщать о добавлении заказа администатору сайта после создания заказа, сообщать пользователю о том что администратор отклонил/подтвердил его отзыв, заполнить сервисные поля (н-р какое-либо состояние) перед сохранением модели и так далее. Для решения данных задач существуют события модели.
Перечислим их:
- beforeInsert, afterInsert - вызываются перед созданием и после создания модели
- beforeUpdate, afterUpdate - вызываются перед обновлением и после обновления модели
- beforeSave - вызывается перед созданием и обновлением модели (срабатывает и в момент beforeInsert и в момент beforeUpdate)
- afterSave - вызывается после создания и обновления модели (срабатывает и в момент afterInsert и в момент afterUpdate)
- beforeDelete, afterDelete - вызываются перед удалением и после удаления модели
Описываются события как обычные методы модели.
Опишем событие beforeSave, которое перед каждым сохранением будет приводить наш слоган к верхнему регистру:
class Company extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class,
'label' => 'Company name'
],
'tagline' => [
'class' => CharField::class,
'label' => 'Tagline',
'null' => true
]
];
}
public function beforeSave()
{
parent::beforeSave();
if ($this->tagline) {
$this->tagline = mb_strtoupper($this->tagline, "UTF-8");
}
}
}
Остальные события описываются аналогично.
Объект QuerySet позволяет в удобной форме формировать условия выборки (а в некоторых случаях и не только выборки) из базы данных.
Объект QuerySet можно получить из менеджера модели. Например, для нашей модели компании получить QuerySet можно следующим образом:
$querySet = Company::objects()->getQuerySet();
Так же, для объекта менеджера доступны следующие методы QuerySet, которые возвращают измененный этими методами QuerySet, либо соответствующие данные: all, count, sum, max, avg, min, get, filter, exclude, order, limit, offset, values, choices.
Например, при вызове метода filter у менеджера мы получим QuerySet с примененным filter:
$filteredQuerySet = Company::objects()->filter(['name' => 'Bar company']);
А при вызове метода values получим соответствующий результат:
$arrayOfValues = Company::objects()->values(['name']);
Методы QuerySet можно разбить на две большие группы: методы, которые возвращают измененный QuerySet и иные методы.
Методы, возвращающие измененный QuerySet:
Иные методы:
Так же существует ряд сервисных методов, возвращащих текст запроса к БД, что позволяет быстро разобраться в возникшей проблеме:
- allSql
- getSql
- aggregateSql
- minSql
- maxSql
- avgSql
- sumSql
- countSql
- valuesSql
- updateSql
- deleteSql
Алгоритм работы этих методов соответствует их аналогам, лишь в качестве результата они возвращают текст SQL-запроса.
Так же у QuerySet есть несколько возможностей:
Подробнее рассмотрим методы и возможности.
Метод, предназанченный для задания условий выборки. В него передается массив условий, по которым данные включаются в выборку.
В данном методе можно использовать запросы через связи и выражения.
Простейшией пример - выборка по полному соответствию атрибута:
Company::objects()->filter(['name' => 'Bar']);
Указанием данного условия мы включаем в выборку только те компании, имя которой равно строке 'Bar'
Для указания сложных условий необходимо использовать класс Q и его методы: Q::orQ, Q::notQ, Q::andQ.
В качестве входящих параметров в эти методы принимаются массивы условий (так же как и в методы filter и exclude).
Пример - выберем все компании с названиями Foo или Bar:
Company::objects()->filter([Q::orQ(['name' => 'Foo'], ['name' => 'Bar']]);
Пример - выберем все компании, в названии которых присутствует строка 'Foo', но не присутствует 'Incorporated':
Company::objects()->filter([['name__contains' => 'Foo', Q::notQ(['name__contains' => 'Incorporated']]);
По-умолчанию все перечисления условий в filter и exclude объединяются через AND. Необходимости в применении данного метода нет, он присутсвует для "полноты картины".
Важной частью задания условий фильтрации являются lookup ("лукапы"). Они являются средствами задания условий, указываются через двойное нижнее подчеркивание ('__') после имени поля модели. Например, чтобы выбрать компании в имени которых присутствет буквосочетание 'ar', необходимо выполнить следующее:
Company::objects()->filter(['name__contains' => 'ar']);
А для того, чтобы выбрать все компании, с идентификатором большим, чем 5 указываем такое условие:
Company::objects()->filter(['id__gt' => 5]);
Перечислим все возможные лукапы и кратко опишем их применение:
Строгое соответствие значения поля указываемому значению. Является лукапом по умолчанию, то есть его указывать не обязательно.
Пример 1. Необходимо выбрать компанию с идентификатором равным 3:
Company::objects()->filter(['id__exact' => 3]);
Или равнозначный вышеуказанному вариант:
Company::objects()->filter(['id' => 3]);
Пример 2. Необходимо выбрать компании со именем 'Acme':
Company::objects()->filter(['name' => 'Acme']);
Указываемое значение содержится в значении поля. Превращается в SQL LIKE %$value%.
Пример 1. Выбрать компании в наименовании которых присутствует буква 'a':
Company::objects()->filter(['name__contains' => 'a']);
Пример 2. Выбрать компании в слогане которых присутствует слово 'bar':
Company::objects()->filter(['tagline__contains' => 'bar']);
В качестве значения принимает массив элементов. Значение поля должно соответствовать одному из перечисленных элементов.
Для данного лукапа будет полезно изучить раздел под-запросы.
Пример 1. Выбрать компании c идентификаторами 1,15,23:
Company::objects()->filter(['id__in' => [1,15,23]]);
Пример 2. Выбрать компании имя которых соответсвует 'A-Trust', 'LM Co.' или 'FooBar':
Company::objects()->filter(['name__in' => ['A-Trust', 'LM Co.', 'FooBar']]);
Значение поля должно быть больше указываемого значения.
Пример. Выбрать компании с идентификатором больше 12:
Company::objects()->filter(['id__gt' => 12]);
Значение поля должно быть больше либо равно указываемому значению.
Пример. Выбрать компании с идентификатором большим либо равным 12:
Company::objects()->filter(['id__gte' => 12]);
Значение поля должно быть меньше указываемого значения.
Пример. Выбрать компании с идентификатором меньше 12:
Company::objects()->filter(['id__lt' => 12]);
Значение поля должно быть меньше либо равно указываемому значению.
Пример. Выбрать компании с идентификатором меньшим либо равным 12:
Company::objects()->filter(['id__lte' => 12]);
Значение поля должно начинаться с указываемого значения.
Пример 1. Выбрать все компании, имя которых начинается на букву 'A':
Company::objects()->filter(['name__startswith' => 'A']);
Пример 2. Выбрать все компании, слоган которых начинается со строки 'Bar':
Company::objects()->filter(['tagline__startswith' => 'Bar']);
Значение поля должно заканчиваться указываемым значением.
Пример 1. Выбрать все компании, имя которых заканчивается на букву 'z':
Company::objects()->filter(['name__endswith' => 'z']);
Пример 2. Выбрать все компании, слоган которых заканчивается восклицательным знаком ('!'):
Company::objects()->filter(['tagline__endswith' => '!']);
Принимает массив из двух значений - от и до. Значение поля должно входить в диапазон между этими значениями.
Пример. Выбрать все компании, с идентификатором от 10 до 100:
Company::objects()->filter(['id__range' => [10, 100]]);
Принимает boolean значение.
Если передано true, то значение поля должно быть null.
Если передано false, то значение поля не должно быть null.
Пример 1. Выбрать все компании слоган которых не задан (null):
Company::objects()->filter(['tagline__isnull' => true]);
Пример 2. Выбрать все компании слоган которых задан (не является null):
Company::objects()->filter(['tagline__isnull' => false]);
Принимает регулярное выражение для передачи в SQL-запрос.
Полностью аналогичен методу filter, за исключением того что он не включает в выборку данные по условию, а исключает их.
В данном методе можно использовать запросы через связи и выражения.
Метод, позволяющий задавать сортировку результата. Принимает массив полей или выражений по которым будет производится сортировка. Направление сортировки можно указывать двумя способами.
Способ 1: Знаком "-" перед именем поля.
Например, выберем все компании отсортированные по наименованию в прямом порядке (ASC):
Company::objects()->order(['name'])->all();
Выберем все компании отсортированные по наименованию в обратном порядке (DESC):
Company::objects()->order(['-name'])->all();
Способ 2:
Выполним равнозначные указанным выше запросы:
Company::objects()->order(['name' => 'ASC'])->all();
Company::objects()->order(['name' => 'DESC'])->all();
В данном методе можно использовать запросы через связи и выражения.
Пример использования выражения:
Company::objects()->order([ new Expression('{id} = ?', [3]) ])->all();
Пример использования запроса через связь:
Shop::objects()->order(['-city__name'])->all();
Метод, задающий условия на агрегатные функции (Count, Min, Max, Avg, Sum ...). Принимает объект Having либо Expression.
Объект Having создается с двумя входящими параметрами - объектом агрегации (Count, Min, Max, Avg, Sum ...) и условием, задаваемым строкой.
Пример 1. Выберем все города, количество магазинов в которых превышает 10:
City::objects()->having(new Having(new Count('shop__id'), '> 10'))->all();
Пример 2. Выберем все категории, средняя стоимость товаров в которых меньше либо равно 1000:
Category::objects()->having(new Having(new Avg('products__price'), '<= 1000'))->all();
Пример 3. Применение Expression. Выбрать тесты количество правильных ответов в которых более 4, сгруппированные по количеству правильных ответов.
Test::objects()->group(['correct_answers'])->having(new Expression('c > 4'))->values(['correct_answers', new Expression('count(*) as c')])
То есть если у нас 5 анкет, в двух из которых по 6 правильных ответов, в третей - 3, в четвертой - 5, в пятой - 2, то результат будет следующий:
correct_answers | c |
---|---|
6 | 2 |
5 | 1 |
Методы, задающие лимит и сдвиг выборки результатов. Работают аналогично директивам SQL. Принимают целочисленные значения.
Пример 1. Выбрать первые 100 статей:
Article::objects()->limit(100)->all();
Пример 2. Выбрать второй блок из 100 статей:
Article::objects()->limit(100)->offset(100)->all();
Метод, позволяющий выбирать связанные через ForeignField модели. Этот метод позволяет сократить количество запросов к базе данных. Принимает массив из наименований полей ForeignField.
Суть работы метода проще всего описать на примере. В качестве примера возьмем модели из раздела ForeignField.
Пример. Необходимо вывести свежие комментарии (10 единиц) на главную страницу, скажем, в раздел "Актуальные комментарии". В сниппете (шаблоне) вывода должен быть текст комментария, а так же наименование поста.
Решим задачу выбора комментариев без метода with:
$latestComments = Comment::objects()->order(['-id'])->limit(10)->all();
После выполнения данного метода выполнится 1 запрос на получение нужных нам комментариев.
Далее, при выводе комментариев списком необходимо обращаться к модели поста, для того чтобы получить наименование:
foreach ($latestComments as $comment) {
echo "Post: " . $comment->post->name;
echo "Comment: " . $comment->name;
}
И для вывода каждого комментария будет выполняться еще по 1 запросу. Таким образом, для вывода 10 комментариев будет выполнено 11 запросов (1 на получение комментариев и еще 10 на получение поста к каждому комментарию). Непорядок. А если комментариев будет 100? Кошмар.
Можно получить сначала весь список необходимых нам комментариев, затем список необходимых постов и при выводе комментариев искать пост (например, по идентификатору) в массиве выбранных постов. Метод рабочий, но это всё равно 2 запроса (комментарии и посты), усложненный вывод, усложненные выборки.
Теперь применим метод with:
$latestComments = Comment::objects()->order(['-id'])->with(['post'])->limit(10)->all();
Теперь выполнится 1 запрос, который "подтянет" из БД информацию и о комментарии и о его посте. Вывод менять не потребуется, при обращении к посту комментария запроса не произойдет, так как данные уже будут загружены. Perfect!
Метод, задающий группировку результатов. Принимает массив, который содержит наименования полей либо Expression.
Выбрать тесты сгруппированные по количеству правильных ответов.
Test::objects()->group(['correct_answers'])->values(['correct_answers', new Expression('count(*) as c')])
То есть если у нас 5 анкет, в двух из которых по 6 правильных ответов, в третей - 3, в четвертой - 5, в пятой - 2, то результат будет следующий:
correct_answers | c |
---|---|
6 | 2 |
3 | 1 |
5 | 1 |
2 | 1 |
Метод, позволяющий указать массив выбираемых результатов. Принимает массив, который содержит наименования полей либо Expression.
В качестве примера можно рассмотреть запрос из раздела Под-запросы.
Метод, осуществяющий выбор нескольких объектов.
Пример 1. Выбрать все компании:
$companies = Company::objects()->all();
После выполнения данной операции в массиве $companies будут содержатся все доступные экземпляры класса Company.
Пример 2. Выбрать компании с id, большим чем 5, отсортированные по наименованию:
$companies = Company::objects()->filter(['id__gt' => 5])->order(['name'])->all();
После выполнения данной операции в массиве $companies будут содержатся все экземпляры класса Company отсортированные по наименованию, id которых больше 5.
Метод, осуществляющий выбор одного объекта. Если под условие попадает более 1 объекта, то выберется первый, если не попадает ни один, то метод вернет NULL.
Пример 1. Выбрать компанию с id, равным 7:
$company = Company::objects()->filter(['id' => 7])->get();
Пример 2. Выбрать первую компанию из отсортированных по имени:
$company = Company::objects()->order(['name'])->get();
После выполнения данных примеров переменная $company будет содержать экземпляр класса Company, либо NULL, если таковые не нашлись по заданным условиям.
Метод, позволяющий использовать SQL-агрегацию. Позволяют получить некоторую сводную (общую) информацию для какого-либо поля (Min, Max, Avg, Sum), либо для всего количества строк (Count). Для каждой из функций Min, Max, Avg, Sum, Count есть свой соотвествующий метод в QuerySet, позволяющий работать с данными агрегационными функциями чуть более удобно.
В агрегационных методах при указании наименования поля можно использовать запросы через связи.
Пример 1. Найти максимальную площадь квартиры:
$maxArea = Flat::objects()->aggregate(new Max('area'));
или (используя соответствующий метод QuerySet):
$maxArea = Flat::objects()->max('area');
Значение переменной $maxArea будет соответствовать максимальной площади квартиры, если такие квартиры будут существовать. В противном случае - в результате мы получим значение NULL.
Пример 2. Найти минимальную цену среди товаров производителя с именем 'Acme':
$minPrice = Product::objects()->filter(['vendor__name' => 'Acme'])->aggregate(new Min('price'));
или (используя соответствующий метод QuerySet):
$minPrice = Product::objects()->filter(['vendor__name' => 'Acme'])->min('price');
Значение переменной $minPrice будет соответствовать минимальной цене среди товаров производителя с именем 'Acme', если такие товары будут существовать. В противном случае - в результате мы получим значение NULL.
Пример 3. Найти среднее арифметическое ширины среди всех книжных полок:
$avgWidth = Bookshelf::objects()->aggregate(new Avg('width'));
или (используя соответствующий метод QuerySet):
$avgWidth = Bookshelf::objects()->avg('width');
Значение переменной $avgWidth будет соответствовать среднему арифметическому ширины среди всех книжных полок. Если в выборку не попадет ни одна полка, то в результате мы получим значение NULL.
Пример 4. Найти сумму всех заказов:
$sumTotal = Order::objects()->aggregate(new Sum('total'));
или (используя соответствующий метод QuerySet):
$sumTotal = Order::objects()->sum('total');
Значение переменной $sumTotal будет равно сумме итогов (поздразумеваем, что поле total хранит итоговую сумму заказа) всех заказов. Если в выборку не попадет ни один заказ, то в результате мы получим значение NULL.
Пример 5. Подсчитать количество общее количество компаний:
$countCompanies = Company::objects()->aggregate(new Count());
или (используя соответствующий метод QuerySet):
$countCompanies = Company::objects()->count();
Значение переменной $countCompanies будет содержать общее количество всех компаний.
Нахождение максимального значения указанного поля. В качестве входящего параметра принимает имя поля.
См aggregate, пример 1.
Нахождение минимального значения указанного поля. В качестве входящего параметра принимает имя поля.
См aggregate, пример 2.
Нахождение среднего арифметического значения указанного поля. В качестве входящего параметра принимает имя поля.
См aggregate, пример 3.
Нахождение суммы значений указанного поля. В качестве входящего параметра принимает имя поля.
См aggregate, пример 4.
Подсчитывает количество объектов.
См aggregate, пример 5.
Метод, позволяющий выбирать "сырые" данные из БД, не конвертируя их в объекты.
Первым входящим параметром принимает массив полей (строк и/или выражений). Вторым входящим параметром принимает Boolean-переменную, указывающую формат данных: false (по-умолчанию) - формат ключ-значение, true - только значения.
Если в качестве ключа в первом входящем параметре использовать строку, то в результирующем массиве она так же будет ключом соответствующих значений (см. Пример 5).
Если первым входящим параметром указать пустой массив, либо не указывать ничего, то будут выбраны все атрибуты из таблицы, соответствующей указанной модели.
В данном методе можно использовать запросы через связи и выражения.
Пример 1. Получить идентификаторы и наименования компаний.
$companies = Company::objects()->values(['id', 'name']);
В результате получим массив следующего характера:
[
[
'id' => 1,
'name' => 'Company 1'
],
[
'id' => 2,
'name' => 'Second company'
],
...
]
Пример 2. Получить идентификаторы комментариев, их наименования, а так же наименования постов (используя модели из раздела запросы через связи):
$comments = Comment::objects()->values(['id', 'name', 'post__name']);
В результате получим массив следующего характера:
[
[
'id' => 1,
'name' => 'Comment 1',
'post__name' => 'First post'
],
[
'id' => 2,
'name' => 'Second comment',
'post__name' => 'Third post'
],
...
]
Пример 3. Получить идентификаторы всех товаров.
$ids = Product::objects()->values(['id'], true);
В результате получим массив следующего характера:
[
1,
2,
3,
5,
...
]
Пример 4. Получить идентификаторы фильмов, их наименования и сумму рейтингов администраторов и пользователей:
$info = Film::objects()->values(['id', 'name', new Expression('({rating_admin} + {rating_users}) as rating')]);
[
[
'id' => 1,
'name' => 'Some film',
'rating' => 5
],
[
'id' => 2,
'name' => 'Second film',
'rating' => 7
],
...
]
Пример 5. Выполняем предыдущий пример, но с передачей ключа для Expression:
$info = Film::objects()->values(['id', 'name', 'rating' => new Expression('({rating_admin} + {rating_users})')]);
[
[
'id' => 1,
'name' => 'Some film',
'rating' => 5
],
[
'id' => 2,
'name' => 'Second film',
'rating' => 7
],
...
]
Метод, позволяющий быстро построить массив ключ-значение.
Принимает 2 входящих параметра - наименования полей.
Пример 1. Получить массив ключ-значение, где ключом будет идентификатор компании, а значением - ее наименование.
$data = Company::objects()->choices('id', 'name');
В результате мы получим массив следующего вида:
[
1 => 'First company',
2 => 'Second company with long name',
4 => 'Another company',
...
]
Пример 2. Получить массив ключ-значение, где ключом будет идентификатор комментария, а значением - наименование поста, к которому относится данный комментарий.
$data = Comment::objects()->choices('id', 'post__name');
Метод, позволяющий обновить данные в выбранных по условиям записях.
В качестве входящего параметра принимает массив ключ-значение, где ключом является имя поля в соответствующей таблице БД, а значением - значение, которое будет установлено в данное поле для выбранных записей.
ВАЖНО! Данное изменение записей не вызовет событий в модели, так как выполняется непосредственно в БД. Будьте аккуратны и внимательны!
Пример 1. Изменить статус всех работ, которые имеют процент выполнения больше 50 на "Выполено":
Work::objects()->filter(['percent__gt' => 50])->update([
'status' => Work::STATUS_DONE
]);
Пример 2. Установить имя "No name" для всех компаний, id которых меньше либо равен 10:
Work::objects()->filter(['id__lte' => 10])->update([
'name' => "No name"
]);
Аналогичен методу update, но не изменяет записи, а удаляет их.
ВАЖНО! Данное удаление записей не вызовет событий в модели, так как выполняется непосредственно в БД. Будьте аккуратны и внимательны!
Пример. Удалить все отклоненные комментарии:
Comment::objects()->filter(['status' => Comment::STATUS_DECLINED])->delete();
В качестве имени поля в методах QuerySet (filter, exclude, order, values и т.д.) можно использовать имена связанных моделей.
Опишем связанные модели комментария, поста и категории постов:
class Category extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'posts' => [
'class' => ManyToMany::class,
'modelClass' => Comment::class
]
];
}
}
class Post extends Model
{
public static function getFields()
{
return [
'name' => [
'class' => CharField::class
],
'comments' => [
'class' => HasManyField::class,
'modelClass' => Comment::class
],
'categories' => [
'class' => ManyToMany::class,
'modelClass' => Category::class
]
];
}
}
class Comment extends Model
{
public static function getFields()
{
return [
'email' => [
'class' => EmailField::class
],
'name' => [
'class' => CharField::class
],
'post' => [
'class' => ForeignField::class,
'modelClass' => Post::class
],
];
}
}
И в качестве условий выбора, например, поста, мы можем использовать поля комментариев.
Например, выберем все посты, email комментариев в которых заканчивается на '@gmail.com':
Post::objects()->filter(['comments__email__endswith' => '@gmail.com'])->all();
Выберем все комментарии, в имени поста которых содержится слово 'example':
Comment::objects()->filter(['post__name__contains' => 'example'])->all();
При этом, можно использовать не только данной модели, но и связанной с ней.
Выберем все категории в которых содержатся посты, в имени комментариев которых есть строка 'alex':
Category::objects()->filter(['posts__comments__name__contains' => 'alex'])->all();
Такие имена полей можно использовать не только в filter и exclude.
Нарпример, отсортируем комментарии по наименованиям постов:
Comment::objects()->order(['post__name']);
Или выберем массив значений состоящих из идентификаторов комментариев и наименований постов:
Comment::objects()->values(['id', 'post__name']);
В некоторых случаях необходимо использовать более сложную логику, чем сопоставление значения в базе указанному значению в методе filter или проводить какую-то особую сортировку в методе order. В этом случае нам помогут выражения.
Например, необходимо выбрать все заказы где сумма стоимости товаров и стоимости доставки больше 2000:
Order::objects()->filter([ new Expression("{products_sum} + {delivery_sum} > ?", [2000])])->all();
В выражении мы можем использовать поля данной модели, а так же поля связанных моделей, через двойное нижнее подчеркивание ("__"), точно так же как описано в разделе запросы через связи. Наименования полей указываются в фигурных скобках.
В выражениях можно использовать и параметры (в примере выше это значение 2000). Параметры указываются в тексте выражения вопросительным знаком и передаются в соответствующем порядке в массиве параметров вторым входящим параметром в конструктор выражения.
Вот еще несколько примеров использования выражений:
Page::objects()->order([ new Expression('{id} = ?', [3])])->all();
Order::objects()->filter(['products_sum__lt' => new Expression("{total_sum} - {delivery_sum}")])->all();
В методах filter и exclude возможно использование под-запросов. Используются они следующим образом:
Product::objects()->filter([
'category_id__in' => Category::objects()->filter(['name__startswith' => 'A'])->select(['id'])
])->all();
Внимание! Использование под-запросов может существенно замедлить скорость выполнения запроса, поэтому пользоваться ими стоит только по необходимости. Например, запрос выше можно описать и без под-запроса:
Product::objects()->filter([
'category__name__startswith' => 'A'
])->all();