Еще одна реализация Django ORM на PHP. На текущий момент находится в стадии активной доработки и не
рекомендуется к использованию в production
до версии 1.0.
Приносим извинения за неполноту документации. Документация находится разработке.
Часть кода основана на yii2 фреймворке, но ввиду монолитности ядра мы были вынуждены
"распилить" фреймворк по отдельным пакетам. В Mindy ORM
активно используется Query из yii2.
Поддерживаемые типы субд: sqlite
, mysql
, pgsql
, mssql
(все то, что умеет yii2 query).
Теоретически ORM способна работать с NoSql
хранилищами за счет реализации собственного менеджера
с переопределением Lookup
'ов.
Опишем подключение к бд:
Model::setConnection([
'sqlite::memory'
]);
Опишем модель:
class MyModel extends Model
{
public function getFields()
{
return [
// Ключ - название поля при обращении к модели
'name' => [
// Тип поля
'class' => CharField::className(),
// Длинна поля
'length' => 100,
// NULL|NOT NULL
'null' => false,
// "Читабельное" имя модели. Используется в Mindy\Form\ModelForm
// для построения форм автоматически.
'verboseName' => 'Продукция',
// Пример валидаторов
'validators' => [
function ($value) {
if (mb_strlen($value, 'UTF-8') < 3) {
return "Minimal length < 3";
}
return true;
},
new UniqueValidator
]
],
'price' => [
'class' => CharField::className(),
// Значение по умолчанию
'default' => 0
],
'description' => [
'class' => TextField::className(),
'default' => ''
],
'category' => [
'class' => ForeignField::className(),
'modelClass' => Category::className(),
'null' => true
],
'lists' => [
'class' => ManyToManyField::className(),
'modelClass' => ProductList::className()
]
];
}
}
Описание модели завершено. Валидация происходит на основе валидации полей модели. Модель описана, теперь необходимо создать ее в субд. Для этого выполним следующий код:
$sync = new Sync([
new MyModel()
]);
$sync->create();
В бд создатутся таблицы всех переданных моделей а так же индексы и связи если это возможно.
Создадим несколько записей:
// Если модель с идентичными полями найдется, то ORM вернет ее, иначе создаст.
$model = MyModel::objects()->getOrCreate(['name' => 'Поросенок петр', 'price' => 1]);
$modelTwo = new MyModel;
// Массовое присвоение аттрибутов
$modelTwo->setData([
'name' => 'Рубаха',
'price' => 2
]);
// Валидация и сохранение
if($modelTwo->isValid() && $modelTwo->save()) {
echo 'Модель сохранена';
}
$modelThree = new MyModel;
$modelThree->name = 'Джинсы';
$modelThree->price = 3;
// Валидация и сохранение
if($modelThree->isValid() && $modelThree->save()) {
echo 'Модель сохранена';
}
Выборки реализованы по аналогии с Django Framework:
// SELECT * FROM my_model WHERE price >= 2
$models = MyModel::objects()->filter(['price__gte' => 2])->all();
// SELECT * FROM my_model WHERE name = 'Рубаха'
$models = MyModel::objects()->filter(['name' => 'Рубаха'])->all();
// SELECT * FROM my_model WHERE id IN (1, 2, 3)
$models = MyModel::objects()->filter(['pk__in' => [1, 2, 3]])->all();
И так далее. Более подробную информацию смотрите в разделе Lookups
Очистим базу данных:
$sync = new Sync([
new MyModel()
]);
$sync->delete();
Менеджер это интерфейс проксирующий до класса QuerySet
, который занимается обработкой
наших Lookup
'ов и выполнением запросов. Каждая модель по умолчанию имеет менеджер objects()
.
User::objects()
Менеджер обрабатывает QuerySet. Основным методом менеджера является getQuerySet()
.
Это возвращает объект, который впоследствии обрабатывается менеджером.
Если вы хотите изменить логику менеджера по умолчанию, вы можете создать свой собственный менеджер.
Например, activeUsersManager()
:
class ActiveUsersManager extends Manager
{
public function getQuerySet()
{
$qs = parent::getQuerySet();
return $qs->filter(['status' => User::STATUS_ACTIVE]);
}
}
Модель с собственным менеджером:
class User extends Model
{
public function getFields()
{
'name' => [
'class' => CharField::className(),
],
'status' => [
'class' => BooleanField::className(),
'default' => false
]
}
public static function activeUsersManager($instance = null)
{
$className = get_called_class();
return new ActiveUsersManager($instance ? $instance : new $className);
}
}
Теперь вы можете использовать ActiveUsersManager()
с помощью User::activeUsers()
.
И все запросы будут выполняться в соответствии с вашей логикой обработки только активных пользователей.
Например, этот код поможет выбрать активных пользователей имя которых начинается с 'A':
User::activeUsers()->filter(['name__startswith' => 'A'])->all();
Выборка одного объекта. В случае если объектов соответствующих условию больше 1, выбрасываем исключение, иначе возвращаем объект. Если объектов не найдено, то возвращаем null.
Найдем объект где pk == 5
.
$model = User::objects()->filter(['pk' => 5])->get();
Возвращает массив моделей класса Model
или ассоциативный массив в случае если
вызван метод asArray()
.
Выборка всех пользователей. Вернется массив моделей класса User
.
User::objects()->all();
Выборка всех пользователей. Вернется ассоциативный массив.
User::objects()->asArray()->all();
Возвращает число объектов подходящих под условия выборки
User::objects()->count();
Вы можете вызывать эти методы последовательно:
User::objects()->filter(['name' => 'Max'])->exclude(['address' => 'New York'])->order(['-address'])->all();
Данный код возвращает всех пользователей с именем Max
живущих не в New York
с сортировкой
по адресу в обратном порядке.
SELECT * FROM user WHERE name='Max' AND address != 'New York' ORDER BY address DESC
Вы можете найти всех пользователей состоящих в группе с именем example
, используя следующий код:
User::objects()->filter(['group__name' => 'example'])->all();
group
в данном случае связь m2m
до модели Group
Вы можете найти всех пользователей которые не состоят в группе с названием example
, используя следующий код:
User::objects()->exclude(['group__name' => 'users'])->all();
group
в данном случае связь m2m
до модели Group
Поиск всех пользователей и сортировка по имени:
User::objects()->order(['name'])->all();
Поиск всех пользователей и сортировка по имени в обратном порядке:
User::objects()->order(['-name'])->all();
Поиск всех пользователей и сортировка в случайном порядке
User::objects()->order(['?'])->all();
С помощью лукапов(lookups) вы можете фильтровать QuerySet.
Лукапы применяются в методах менеджера exclude()
и filter()
и передаются в них массивом,
где ключом массива являются поля (с лукапами), а значением - значение, по которому и производится фильтрация.
Пример лукапа:
'name__exact'
Данный лукап указывает на то, что поле name
должно быть равно указанному значению. Пример применения:
User::objects()->filter(['name__exact' => 'Max'])->all();
Выбираем всех пользователей имя которых равно 'Max'.
На самом деле, лукап exact
является лукапом "по умолчанию". То есть, в данном примере можно было было обойтись условием filter(['name' => 'Max'])
.
SELECT * FROM user WHERE name = 'Max'
Лукап, применяющийся для поиска значений NULL
в базе данных.
Пример применения:
User::objects()->filter(['name__isnull' => true])->all();
Произойдет выбор всех пользователей cо значением имени в БД NULL
.
SELECT * FROM user WHERE name IS NULL
Если передать false
в качестве значения то sql запрос будет выглядеть слудющим
образом:
SELECT * FROM user WHERE name IS NOT NULL
Лукапы, применяющиеся для поиска значений меньше заданного (lt
), и меньших либо равных заданному (lte
)
Пример lt
лукапа:
Product::objects()->filter(['price__lt' => 100.00])->all();
Произойдет выбор всех продуктов с ценой строго меньшей 100.00
.
Пример lte
лукапа:
Product::objects()->filter(['price__lte' => 100.00])->all();
$dateTime = new DateTime();
$dateTime->setTimestamp(strtotime('+15 days'))
Product::objects()->filter(['created_at__lte' => $dateTime])->all();
Произойдет выбор всех продуктов с ценой меньшей, либо равной 100.00
.
lt
формирует слудющий sql:
SELECT * FROM user WHERE price < 100.00
lte
формирует слудющий sql:
SELECT * FROM user WHERE price <= 100.00
Лукапы, применяющиеся для поиска значений больше заданного (gt
), и больших либо равных заданному (lte
)
Пример gt
лукапа:
Product::objects()->filter(['price__gt' => 100.00])->all();
Произойдет выбор всех продуктов с ценой строго большей 100.00
.
Пример gte
лукапа:
Product::objects()->filter(['price__gte' => 100.00])->all();
Произойдет выбор всех продуктов с ценой большей, либо равной 100.00
.
gt
формирует слудющий sql:
SELECT * FROM user WHERE price > 100.00
gte
формирует слудющий sql:
SELECT * FROM user WHERE price >= 100.00
Применяется для поиска значний строго равных заданному. Пример:
User::objects()->filter(['name__exact' => 'Max'])->all();
Произойдет выбор всех пользователей с именем 'Max'.
** Лукап является лукапом по умолчанию, то есть в предыдущем примере можно указать просто ['name' => 'Max']
**
Применяется для поиска значений в которых присутствует заданноу значение (аналог - SQL LIKE).
Регистрозависимый поиск. Для регистронезависимого используйте lookup icontains
.
Пример:
User::objects()->filter(['name__contains' => 'ax'])->all();
Произойдет выбор всех пользователей, в имени которых есть сочетание 'ax'.
SELECT * FROM user WHERE name LIKE '%ax%'
Полностью повторяет lookup contains
, но осуществляет регистронезависимый поиск.
Применяется для поиска значений, начинающихся с заданного значения:
Регистрозависимый. Для регистронезависимого используйте lookup istartswith
.
Пример:
User::objects()->filter(['name__startswith' => 'M'])->all();
Произойдет выбор всех пользователей, имя которых начинается с 'M'.
SELECT * FROM user WHERE name LIKE 'M%'
Полностью повторяет lookup startswith
, но осуществляет регистронезависимый поиск.
Применяется для поиска значний, заканчивающихся заданным значением:
Регистрозависимый. Для регистронезависимого используйте lookup iendswith
.
Пример:
User::objects()->filter(['name__endswith' => 'on'])->all();
Произойдет выбор всех пользователей, имя которых начинается с 'on'.
SELECT * FROM user WHERE name LIKE '%on'
Полностью повторяет lookup iendswith
, но осуществляет регистронезависимый поиск.
Применяется для поиска значений, попадающих в список переданных значений: Пример:
User::objects()->filter(['pk__in' => [1, 2]])->all();
Произойдет выбор всех пользователей, pk которых попадают в список [1, 2]
.
SELECT * FROM user WHERE id IN (1, 2)
Lookup может принимать QuerySet в качестве значения
Данный лукап позволяет принимать не только массив значений, но и объект QuerySet. В этом случае в запрос будет добавлен подзапрос.
Пример:
$group_qs = Group::objects()->filter(['name__startswith' => 'A']);
$users = User::objects()->filter(['groups__pk__in' => $group_qs->select('id') ])->all();
Произойдет выбор всех пользователей, имена групп которых начинаются с 'A'. Будет выполнен всего один запрос на выбор пользователей.
Данный лукап позволяет найти значения, расположенные между переданных значений. Данный лукап принимает массив из двух элементов.
Пример:
Product::objects()->filter(['price__range' => [10, 20]])
Произойдет выбор всех продуктов с ценой от 10 до 20.
SELECT * FROM product WHERE price BETWEEN 10 AND 20
Данный лукап работает только с полями типа DateTimeField
, DateField
.
Позволяет найти все значения, расположенные в заданном году.
Пример:
Product::objects()->filter(['date_added__year' => 2014])
Произойдет выбор всех продуктов, добавленных в 2014 году.
Внимание: sql запрос будет отличаться для разных субд.
Данный лукап работает только с полями типа DateTimeField
, DateField
.
Позволяет найти все значения, расположенные в заданном месяце.
Пример:
Product::objects()->filter(['date_added__year' => 12])
Произойдет выбор всех продуктов, добавленных в декабре.
Внимание: sql запрос будет отличаться для разных субд.
Данный лукап работает только с полями типа DateTimeField
, DateField
.
Позволяет найти все значения, расположенные в заданном дне месяца.
Пример:
Product::objects()->filter(['date_added__day' => 25])
Произойдет выбор всех продуктов, добавленных 25 числа любого месяца.
Внимание: sql запрос будет отличаться для разных субд.
Данный лукап работает только с полями типа DateTimeField
, DateField
.
Позволяет найти все значения, расположенные в заданном дне недели.
Значения: 1 - Воскресенье, 2 - Понедельник, ..., 7 - Суббота. Порядок дней определяет ORM и подстраивает под текущую субд по следующей причине:
Method Range
------ -----
PYTHON
datetime_object.weekday() 0-6 Sunday=6
datetime_object.isoweekday() 1-7 Sunday=7
dt_object.isoweekday() % 7 0-6 Sunday=0 # Can easily add 1 for a 1-7 week where Sunday=1
MYSQL
DAYOFWEEK(timestamp) 1-7 Sunday=1
WEEKDAY(timestamp) 0-6 Monday=0
POSTGRES
EXTRACT('dow' FROM timestamp) 0-6 Sunday=0
TO_CHAR(timestamp, 'D') 1-7 Sunday=1
ORACLE
TO_CHAR(timestamp, 'D') 1-7 Sunday=1 (US), Sunday=6 (UK)
Пример:
Product::objects()->filter(['date_added__week_day' => 2])
Произойдет выбор всех продуктов, добавленных в понедельник.
Внимание: sql запрос будет отличаться для разных субд.
Данный лукап работает только с полями типа DateTimeField
, TimeField
.
Позволяет найти все значения, расположенные в заданном часе.
Пример:
Product::objects()->filter(['date_added__hour' => 10])
Произойдет выбор всех продуктов, добавленных в 10 часов.
Внимание: sql запрос будет отличаться для разных субд.
Данный лукап работает только с полями типа DateTimeField
, TimeField
.
Позволяет найти все значения, расположенные в заданной минуте.
Пример:
Product::objects()->filter(['date_added__minute' => 35])
Произойдет выбор всех продуктов, добавленных в 35 минут.
Внимание: sql запрос будет отличаться для разных субд.
Данный лукап работает только с полями типа DateTimeField
, TimeField
.
Позволяет найти все значения, расположенные в заданной секунде.
Пример:
Product::objects()->filter(['date_added__minute' => 45])
Произойдет выбор всех продуктов, добавленных в 45 секунд.
Внимание: sql запрос будет отличаться для разных субд.
** Не реализовано **
Поиск по регулярному выражению.
Регистрозависимый. Для регистронезависимого используйте lookup iregex
.
Пример:
Product::objects()->filter(['name__regex' => '[a-z]'])
Произойдет выбор всех продуктов, соответствующих регулярному выражению [a-z]
Внимание: sql запрос будет отличаться для разных субд.
Полностью повторяет предыдущий lookup, но осуществляет регистронезависимый поиск.
Внимание: sql запрос будет отличаться для разных субд.
Q-объекты необходимы для удобного формирования условий выборки. Существует 2 Q-объекта: OrQ и AndQ.
Q-объект формируется следующим образом:
new OrQ([['status' => 1, 'user_id' => 1],['status' => 2, 'user_id' => 4]]);
И затем его можно передать в методы filter или exclude:
Requests::objects()->filter([new OrQ([['status' => 1, 'user_id' => 1],['status' => 2, 'user_id' => 4]])])->all()
Данный запрос выберет нам все заявки со статусом 1 от пользователя с id равным 1 и заявки со статусом 2 от пользователя с id равным 4.