[dev] ActiveRecord в Alto CMS v.1.2. Часть 1

По сложившейся традиции пишу о наиболее интересных и важных нововведениях в движке еще до официального релиза.

Не помню, возможно, писал уже о том, что я несколько раз подступал к реализации ActiveRecord в Альто. Причем, было большое желание не писать все с нуля, а подобрать уже готовую библиотеку и адаптировать ее к своим нуждам. Но, в силу разных причин, так и не получилось это сделать. Лайвстритовскую реализацию ORM в части задания критериев для выборки данных я считаю просто ужасной.

В общем, все закончилось тем, что в Альто была выполнена своя реализация ActiveRecord, о которой сейчас и пойдет речь.
Создание сущности-записи
Для работы с ActiveRecord создан специальный набор классов, который расположен в пространстве имен \alto\engine\ar (да-да, в Альто тихой сапой начинают использоваться пространства имен), и сущность, модуль и маппер в этом случае должны наследоваться соответственно от классов EntityRecord, ArModule и ArMapper, например:
use alto\engine\ar\EntityRecord;

class PluginCompany_ModuleCompany_EntityCompany extends EntityRecord {
    // ...
}

use alto\engine\ar\ArModule;

class PluginCompany_ModuleCompany extends ArModule {
    // ...
}

use alto\engine\ar\ArMapper;

class PluginCompany_ModuleCompany_EntityCompany extends ArMapper {
    // ...
}

Как правило, имя сущности проецируется на имя таблицы базы данных. Например, в примере выше для работы с сущностью _EntityCompany будет использоваться таблица «?_company» (подстрока «?_» в начале имени таблицы будет заменена на префикс таблиц по умолчанию, который задается в конфиге). А если сущность называется, скажем, _EntityCompanyEmployee, то будет использована таблица «?_company_employee».

В качестве первичного ключа по умолчанию используется поле таблицы «id».

Но, при желании, можно задать любую таблицу и любое поле для первичного ключа, это делается в методе инициализации:
class PluginCompany_ModuleCompany_EntityEmployee extends EntityRecord {

    public function init() {
        // По умолчанию таблица для этой сущности была бы просто "?_employee",
        // но мы хотим, чтобы использовалась "?_company_employee"
        $this->setTable('?_company_employee');
        // А первичный ключ будет "company_employee_id"
        $this->setPrimaryKey('company_employee_id');
    }
}

Теперь для создания новой сущности и сохранения ее в базе данных нам понадобится минимум кода:
$oCompany = E::ModuleCompamy()->make();
$oCompany->setName('Google');
$oCompany->setEmail('info@google.com');
$oCompany->setCountryCode('US');
$oCompany->save();

А можно и еще короче:
E::ModuleCompamy()
  ->make()
  ->setName('Google')
  ->setEmail('info@google.com')
  ->setCountryCode('US')
  ->save();


По-моему, чего делается в этом коде, понятно без всяких дополнительных пояснений. Если же мы хотим создать сущность, имя которой отличается от имени модуля, то это надо явно указать:
E::ModuleCompamy()
  ->make('Employee')
  ->setFirstName('Jhon')
  ->setLastName('Smith')
  ->save();

Чтение сущностей из базы данных
Чтобы найти компанию по ее названию используется следующий синтаксис:
$oCompany = E::ModuleCompamy()
  ->find()
  ->where(['name' => 'Google'])
  ->one();

И никаких тебе SQL-запросов и прочей рутины! А вот так мы найдем все компании у которых указан код страны «US»:
$aCompanies = E::ModuleCompamy()
  ->find()
  ->where(['country_code' => 'US'])
  ->all();

А вот так можно получить список всех сотрудников, которых зовут Jhon Smith:
$aCompanies = E::ModuleCompamy()
  ->find('Employee')
  ->where(['first_name' => 'Jhon', 'last_name' => 'Smith'])
  ->all();

А вот так можно получить сущности по первичным ключам:
// Получить компанию с ID 1231
$oCompany = E::ModuleCompamy()
  ->find()
  ->one(1231);
// или более короткая форма
$oCompany = E::ModuleCompamy()->findOne(1231);

// Получить список компаний с ID 1231, 3724, 5930
$aCompanies = E::ModuleCompamy()
  ->find()
  ->all([1231, 3724, 5930]);
// или более короткая форма
$aCompanies = E::ModuleCompamy()->findAll([1231, 3724, 5930]);

Значения для задания фильтров в методе where можно задавать явно, как указано выше, а можно и через параметры, например, так:
$oQuery->where(['name' => ':name'])->bind(':name' => 'Google');
$oQuery->where(['country_code' => ':cc'])->bind(':name' => $sCountryCode);

Условия where могут задаваться разными способами:
// field = :value
$oQuery->where(['field' => ':value']); 

// field = :value (условие задается, как массив в массиве)
$oQuery->where([['field', '=', ':value']]); 

// field < :value
$oQuery->where([['field', '<', ':value']]); 

// (field1 < :value1) AND (field2 < :value2)
$oQuery->where([['field1', '<', ':value1'], ['field2', '!=', ':value2']]); 

// (field1 < :value1) OR (field2 < :value2)
$oQuery->where([['field1', '<', ':value1'])->orWhere(['field2', '!=', ':value2']]); 

// Зададим сложное вложенное условие:
// (a in (1, 2, 3)) OR (a in (10, 20, 30)) AND (b > 100 OR (c = 1 OR c > 8))
$oQuery
  ->whereBegin()
    ->where([['a', 'in', '?a:param1']])
    ->orWhere([['a', 'in', '?a:param2']])
  ->whereEnd()
  ->andWhereBegin()
    ->where([['b', '>', '?d:param3']])
	->orWhereBegin()
	  ->where(['c' => ':param4'])
	  ->orWhere([['c', '>', ':param5']])
	->whereEnd()
  ->whereEnd()
  ->bind([
    ':param1' => [1, 2, 3],
    ':param2' => [10, 20, 30],
    ':param3' => 100,
    ':param4' => 1,
    ':param5' => 1,
  ]);

Выглядит последнее выражение, на мой взгляд, не очень красиво и удобочитаемостью похвастаться не может, но добавлен такой синтаксис, скорее, для полноты функционала. Впрочем, можно все то же самое сделать и с помощью обычного SQL-выражения:
$oQuery->whereSQL('(a in (?a:param1)) OR (a in (a:param2)) AND (b > ?d:param3 OR (c = :param4 OR c > :param5))');  
  ->bind([
    ':param1' => [1, 2, 3],
    ':param2' => [10, 20, 30],
    ':param3' => 100,
    ':param4' => 1,
    ':param5' => 1,
  ]);

Наконец, можно задать сортировку возвращаемых записей:
// обычная (прямая) сортировка
$aCompanies = E::ModuleCompamy()
  ->find()
  ->orderBy('country_code')
  ->all();
  
// обратная сортировка
$aCompanies = E::ModuleCompamy()
  ->find()
  ->orderBy('country_code' => 'desc')
  ->all();
  
// сортировка по нескольким полям
$aCompanies = E::ModuleCompamy()
  ->find()
  ->orderBy('country_code' => 'desc', 'update_date')
  ->all();


Обновление и удаление
Метод save() используется и при записи новой сущности, и при изменении существующей сущности:
$oCompany = E::ModuleCompamy()->findOne(1231);
$oCompany->setName('Apple');
$oCompany->save();

А код для удаления еще проще:
E::ModuleCompamy()->findOne(1231)->delete();


В следующей части я расскажу о связях и об одной интересной фиче, которую я назвал «связанные ленивые коллекции».

Похожие статьи

  • Релиз 1.1.19 и новые подробности про версию 1.2
    Вышел релиз движка 1.1.19 Чего-то особенного он не принес, это, в основном, множественные багфиксы. За исключением одной детали — в качестве парсера текстов по умолчанию теперь используется Qevix. Поэтому если вы...
  • Alto CMS 1.2 — планы по разработке
    Решил анонсировать некоторые фичи, которые ожидаются в версии 1.2 (тем более, что меня часто в последнее время спрашивают о некоторых из них). В новой версии будет два основных направления: улучшение мультиязычности...

9 комментариев

0
Как это изменение повлияет на плагины для ветки 1.1.* ?
0
Эти изменения — никак. Это лишь новые возможности, которые не отменяют прежнего функционала.
0
И никаких тебе SQL-запросов и прочей рутины!
На очереди прослойка которая будет строиться на лозунге «И никакого кривого ActiveRecord и прочей ...».
А если серьезно то ORM который реализован в альто это полный звиздец, все равно, неизбежно, приходится разбираться и в SQL, и решать проблемы с кешированием, которого там нет, а то которое есть (типа tmp, т.е. на время выполнения запроса) умудряется устанавливать кеш в конце выполнения запроса, когда он уже не нужен.
Отредактирован:
+2
А если серьезно то ORM который реализован в альто это полный звиздец
Во-первых, уважаемый, Вы плохо читаете, ибо я в самом начале статьи сказал, что «Лайвстритовскую реализацию ORM в части задания критериев для выборки данных я считаю просто ужасной». И по этой причине ORM не используется в базовой версии движка, хотя большинство запросов к БД довольно тривиально, и, казалось бы, этот подход тут был бы вполне уместен.

Во-вторых, внедрение AR вовсе не исключает применения и прежнего подхода — использования старого доброго SQL. Если считаете, что каждый запрос в базе непременно должен руками описываться в виде SQL-выражений — Вы можете делать это и впредь, ни в чем себя не ограничивая.
0
Давно не заходил на Alto, и честно говоря приятно видеть, что проект живет и активно развивается, так держать =)
0
Доброго дня aVadim.

Возможно узнать, почему выбор пал на реализацию все-таки своего AR, нежели взять готовый орм а-ля Doctrine, Eloquent?

Ведь в них можно передать текущий пул соединения и использовать в новой ветке модулей по примеру как это сделано у 1C-Bitrix, а именно Компоненты 1.0, Компоненты 2.0?
0
Если конкретно про Doctrine и Eloquent говорить, то Doctrine отпал сразу, как чрезмерно тяжелый и излишне навороченный. А к Eloquent присматривался и подступал несколько раз. Не все мне в нем нравится, но в целом было желание просто взять и его прикрутить (или, например, Propel). Но не придумал, как это сделать без танцев с бубнами.

Чего хотелось:
1) Чтоб можно было сохранить совместимость с нынешними классами — Entity, Module и Mapper, но при этом использовать синтаксис ActiveRecords а-ля Eloquent или Yii
2) Чтоб можно было использовать просто построитель запросов, как в Yii
3) И чтоб при этом сохранялась возможность задавать SQL-запросы, как оно сейчас делается в движке

Ни с одним из сторонних ORM-движков не получалось этого реализовать.
0
А про подход с переходом?
Ведь сейчас вся вот эта «канитель» будет как клубок с нитками обрастать мусором в попытках привести все в порядок. Возможно проще начать новую ветку модулей и плагинов, где можно будет писать модуль в нормальном синтаксисе. Конечно для этого понадобится прослойка, но то, что сейчас творится в модулях это сущий ад...

Если быть точнее, я предлагаю не исправлять текущий подход в работе с БД, а начать искоренять зло начиная с корня. Таким образом и совместимость с текущими модулями останеться и можно будет создать новую ветку модулей с нормальным исправленным подходом и логикой.
0
а начать искоренять зло начиная с корня...
Если прямо с корня, то не уверен, что получится сохранить совместимость. Делать прям двойное ядро — плохое решение. Тогда уж лучше запускать ветку 2.+, в которой можно не заморачиваться с совместимостью, а делать сразу все правильно, насколько это возможно, по канонам, паттернам и все такое. Но это стопудово будет долгая история. Кто готов в это вписаться?

Если есть конкретные предложения — пишите, обсудим. Но лучше отдельным топиком.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.