Не помню, возможно, писал уже о том, что я несколько раз подступал к реализации 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();
В следующей части я расскажу о связях и об одной интересной фиче, которую я назвал «связанные ленивые коллекции».
9 комментариев
А если серьезно то ORM который реализован в альто это полный звиздец, все равно, неизбежно, приходится разбираться и в SQL, и решать проблемы с кешированием, которого там нет, а то которое есть (типа tmp, т.е. на время выполнения запроса) умудряется устанавливать кеш в конце выполнения запроса, когда он уже не нужен.
Во-вторых, внедрение AR вовсе не исключает применения и прежнего подхода — использования старого доброго SQL. Если считаете, что каждый запрос в базе непременно должен руками описываться в виде SQL-выражений — Вы можете делать это и впредь, ни в чем себя не ограничивая.
Возможно узнать, почему выбор пал на реализацию все-таки своего AR, нежели взять готовый орм а-ля Doctrine, Eloquent?
Ведь в них можно передать текущий пул соединения и использовать в новой ветке модулей по примеру как это сделано у 1C-Bitrix, а именно Компоненты 1.0, Компоненты 2.0?
Чего хотелось:
1) Чтоб можно было сохранить совместимость с нынешними классами — Entity, Module и Mapper, но при этом использовать синтаксис ActiveRecords а-ля Eloquent или Yii
2) Чтоб можно было использовать просто построитель запросов, как в Yii
3) И чтоб при этом сохранялась возможность задавать SQL-запросы, как оно сейчас делается в движке
Ни с одним из сторонних ORM-движков не получалось этого реализовать.
Ведь сейчас вся вот эта «канитель» будет как клубок с нитками обрастать мусором в попытках привести все в порядок. Возможно проще начать новую ветку модулей и плагинов, где можно будет писать модуль в нормальном синтаксисе. Конечно для этого понадобится прослойка, но то, что сейчас творится в модулях это сущий ад...
Если быть точнее, я предлагаю не исправлять текущий подход в работе с БД, а начать искоренять зло начиная с корня. Таким образом и совместимость с текущими модулями останеться и можно будет создать новую ветку модулей с нормальным исправленным подходом и логикой.
Если есть конкретные предложения — пишите, обсудим. Но лучше отдельным топиком.