LiveStreet и ORM

Выход версии 0.5 для меня было нечто большим, чем добавление страницы активности и ленты топиков из подписанных блогов. В новой версии реализованы ORM и ActiveRecord. Вместе они дают мощнейший инструментарий для разработчика, избавляя того от кучи однотипного кода, который приходилось писать каждый раз при разработке плагина. Тот-же форум, о котором будет идти речь в статье, после обновления похудел на 2177 строк кода. В этой статье я хочу углубиться в ORM и AR на примере создания плагина для LiveStreet.

Почти год назад пользователь runawayed опубликовал статью о новшествах в транковой версии.

Отталкивался я от той статьи, но она всего-лишь вводная часть того инструментария. Там нет ни слова о кеше, постраничности, древовидного вида массива. Я постараюсь подать как можно больше информации читателю не нагружая его лишним. Начнем?

Я буду рассматривать все на примере плагина forum. Для начала нам надо создать скелет плагина:

plugin

Исходники файлов можно посмотреть на гитхабе, на описании плагина я останавливаться не буду.

Теперь создадим модуль с именем forum.

plugin

Нам надо описать модуль как ModuleORM, и, в методе Init нам надо наследовать родителя метода Init у обычного модуля:

class PluginForum_ModuleForum extends ModuleORM {

	public function Init() {
		parent::Init();
	}

}


Прежде чем описывать сущности, надо обдумать все связи между ними, чтобы было легче манипулировать через геттеры:

relations

И описать созданные сущности:

Category.entity.class.php

class PluginForum_ModuleForum_EntityCategory extends EntityORM {}


Forum.entity.class.php

class PluginForum_ModuleForum_EntityForum extends EntityORM {
	protected $aRelations = array(
		'category'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityCategory','category_id'),
		'user'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'ModuleUser_EntityUser','user_id'),
		'topic'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityTopic','topic_id'),
		'post'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityPost','post_id'),
	);
}


Topic.entity.class.php

class PluginForum_ModuleForum_EntityTopic extends EntityORM {
	protected $aRelations = array(
		'user'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'ModuleUser_EntityUser','user_id'),
		'post'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityPost','post_id'),
		'forum'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityForum','forum_id'),
	);
}


Post.entity.class.php

class PluginForum_ModuleForum_EntityPost extends EntityORM {
	protected $aRelations = array(
		'user'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'ModuleUser_EntityUser','user_id'),
		'topic'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityTopic','topic_id'),
		'forum'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'PluginForum_ModuleForum_EntityForum','forum_id'),
	);
}


Read.entity.class.php

class PluginForum_ModuleForum_EntityRead extends EntityORM {}


Теперь всем этим можно манипулировать без описания классов в модуле плагина, например:

$this->PluginForum_ModuleForum_GetCategoryItemsAll(); // получим все категории


$this->PluginForum_ModuleForum_GetForumItemsByCategoryId($oCategory->getId()); // получим все форумы по ID категории


$this->PluginForum_ModuleForum_GetTopicItemsByForumId($oForum->getId(),array('#page' => array(1,15), '#cache'=>'')); 
/**
 * Получим массив для вывода с постраничностью и без кеширования запроса вида:
 * array(
 *   'collection' => array of objects
 *   'count' => integer
 * );
 * Где array('#page' => array('номер страницы', 'элементов на страницу'))
 * Где array('#cache' => '') отказ от кеширования запроса
 */


Фильтр #cache также может принимать следующиме параметры:

array(
	'#cache' => array(
		'keys', 'tags', 'time'
	)
);


Есть несколько типов связей:

protected $aRelations = array(
	'entity'=>array(EntityORM::RELATION_TYPE_BELONGS_TO,'ModuleSome_EntitySome','field_id'), // Получаем один элемент по одному элементу. Например, в таблице prefix_forum_topic есть поле с ID'шником пользователя, тогда мы получим юзера по ID с этого поля
	'entity'=>array(EntityORM::RELATION_TYPE_HAS_MANY,'ModuleSome_EntitySome','field_id'), // Получает массив элементов
	'entity'=>array(EntityORM::RELATION_TYPE_HAS_ONE,'ModuleSome_EntitySome','field_id'), // Это, например, когда у пользователя (основная сущность) есть сессия, и она хранится в отдельной таблице (с) ort
	'entity'=>array(EntityORM::RELATION_TYPE_MANY_TO_MANY,'ModuleSome_EntitySome','field_id'), // Получает массив элементов по массиву
	EntityORM::RELATION_TYPE_TREE // Строит дерево, необходимо наличие поля parent_id в таблице
);


После описания сущностей, нам будут доступные следующие методы:

/**
 * Конструкции вида [name] сделаны для примера
 */

/**
 * Работа с объектами
 */
Add();
Update();
Save(); // Совмещает и Update() и Add(), первый выполняется если сущность уже есть в бд, второй когда ее еще нет (с) ort
Delete();
Reload();
// пример:
$oParam->setTitle('Пара-пам-пам');
$oParam->Update();

/**
 * Запросы на получение объекта/массива объектов
 */
ShowColumnsFrom[Table]();
LoadTreeOf[Entity]();
Get[Entity]ItemsBy[Filter, Array, JoinTable, Gte, Lte, Gt, Lt, Like, In]();
Get[Entity]All();
Get[Entity]ItemsAll();
/**
 * Методы, доступные только для типа tree
 */
GetChildrenOf[Entity]();
GetParentOf[Entity]();
GetDescendantsOf[Entity]();
GetAncestorsOf[Entity]();

// пример:
$this->ModuleHabr_GetTopicItemsAll();

/**
 * А так-же сгенерированные через singleton Engine, работа с самим объектом
 */
get[Column]();
// пример:
$oParam->getTitle();


Таблицы в базе данных должны называться следующим образом: prefix_<module-name>_<entity-name> (или прописать их названия в конфиге), а если же название сущности совпадает с названием модуля, то достаточно назвать таблицу так: prefix_<module-name>. Поля тоже имеют свои стандарты: <entity-name>_<field-name>, или просто <field-name>.

Вот и все, господа, спасибо за уделенное внимание. Напоминаю, что свежие исходники форума можно посмотреть в git'е.

UPD: Сделал корректировки в статье, спасибо ort'у за его замечания.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 9

    +4
    Люблю LS и люблю ORM'ы, но реализациия ORM в LiveStreet мне не по душе.
    Первое, что я увидел когда смотрел ее — это то, что парсер запроса встроен в метод __call модуля а не в мэппера, что довольно странно в принципе, кроме того еще и перехватывает все обращения к другим модулям и проупскает через десяток RegExp'ов.
    Во-вторых как движок БД используется не самый быстрый DbSimple, а помноженный еще и на парсер ORM'а он вообще не выдает ничего более-менее производительного.

    Что касается ActiveRecord, как паттерна, выбранного для реализации, то тут, наверное, дело вкуса, хотя я был бы рад увидеть DataMapper на его месте.
      0
      по идеи это время ничтожно мало по сравнению с временем выполнения самого запроса к БД
        0
        Ну вообще я потестил, время работы парсера ORM на 1000 запросов 0.05с с копейками, поэтому да, больший проигрыш сам DbSimple cо своим стремительно устаревающим расширением mysql дает.
        Ну и на создание сущностей на больших объемах уходят ресурсы.
      0
      зачем делать велосипед?? Этот ОRМ подобие на Rails. Возьмите Doctrine и будет Вам счастье
        0
        Это не мне надо адресовать, не я писал, я только лишь изложил.
        0
        Спасибо за статью.
        Есть несколько замечаний:
        1. типы связей нужно указывать не напрямую, а через константы, например, EntityORM::RELATION_TYPE_BELONGS_TO
        2. 'has_one' — это, например, когда у пользователя(основная сущность) есть сессия, и она хранится в отдельной таблице, в то время когда belongs_to соответствует категории, id которой прописан в таблице пользователя
        3. Save(); совмещает и update() и add(), первый выполняется если сущность уже есть в бд, второй когда ее еще нет
        4. называть таблицы в формате prefix_<module-name>_<entity-name> не обязательно, в конфиге можно прописать соответствия любому названию
        5. поля также не обязательно именовать как <entity-name>_<field-name>, можно просто <field-name>
          0
          1. Нужно, или лучше? Ajaxy писал что есть шорт-коды, ну я и взял на заметку.

          Спасибо за замечания, завтра поправлю статью.
            0
            нужно, иначе изменения в ядре приведут к неработоспособности такого плагина
          –2
          как меня умиляет венгерка на пыхе.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое