Мысли об Object-Relational Mapping в LiveStreet

Хочу предложить свои идеи для развития MVC/ORM.

Замечу, что исторически сложилось так, что MVC в LiveStreet весьма отличается от привычного представления в других фреймворках.
Модель здесь заменяется связкой модуль+сущность+маппер, причем, если сущность представляет из себя стандартный ООП-объект с набором свойств и методов, то модули и мапперы, это просто наборы функций для работы с определенными типами данных, что скорее похоже на библиотеки из структурного программирования, чем на стандартный ООП.
Я не возьмусь судить хорошо это или плохо, у меня есть лишь предложения о том, как можно воспользоваться этим для создания эффективных отношений между объектами (модулями).

1. Модули и их сущности
Обычно методы для работы с моделью описаны в одном файле класса модели, причем этот файл совмещает методы для работы и с одним объектом-моделью (записью в контексте БД) и с группой однородных объектов этого класса (таблицей в БД). Разделение же в LS на Entity и Module может быть полезно для того, чтобы в сущности описывались методы как раз объекта-записи:

getId()
getTitle()
setId()
setTitle()
Add()
Update()
Reload()

… а в модуле — методы для работы с целым классом-таблицей.

GetAll()
GetById()
GetByTitle()
GetPage(30, 2)


2. Автоматизация стандартных методов
На самом деле, все методы, которые описаны выше (а также некоторые другие) — довольно стандартны, но сейчас они дублируются во всех модулях, сущностях и мапперах. Есть смысл сделать все это автоматизированным, на уровне движка, чтобы каждый модуль и каждая сущность автоматически имели все эти геттеры, сеттеры, «апдейты» и «сейвы». Многие модели смогут вообще функционировать с пустыми файлами модуля, сущности и маппера.

3. Отношения в ORM
В Ruby on Rails для ActiveRecord для обеспечения связей и зависимостей между моделями и их таблицами в БД существуют замечательные отношения: belongs_to, has_many, has_one и т.п.
Например (в контексте синтаксиса LS) достаточно было бы прописать в prefix_topic поле `blog_id` и добавить по строчке в сущности Topic и Blog и у $oTopic появился бы метод getBlog(), возвращающий сущность Blog, а у $oBlog — getTopics() с массивом сущностей Topic.
Например так:
class ModuleBlog_EntityBlog extends Entity {
  protected $aRelations = array(
    'has_many' => array('topic')
  );
  ...
}
class ModuleTopic_EntityTopic extends Entity {
  protected $aRelations = array(
    'belongs_to' => array('blog')
  );
  ...
}

А дальше движок автоматически на основе согласований имен таблиц и полей добавлял бы новые методы к сущностям моделей.
Есть, правда, одно узкое место — единственные и множественные числа.
Например, если у нас есть модель Topic, которая «принадлежит» модели Blog, то тут надо, чтобы движок умел понимать, что по $oBlog->getTopics() нужно искать по полю `blog_id` в таблице `prefix_topic`, которое указано в единственном числе. В Ruby on Rails есть хороший механизм перевода из ед. во мн. число и обратно с учетом всех правил английского языка.
В принципе, можно создать такой алгоритм, это слегка усложнит задачу, зато добавит изящности в итоге.
Другой вариант — использовать, например постфикс Items для обозначения множественного числа, например так: $oBlog->getTopicItems() и движок уже легко достанет из этого метода имя нужной таблицы: `prefix_topic`.

4. Пример удобства создания новых моделей
Например, если нам нужно создать плагин «Фотогалерея» для организации связи между моделями Album и Photo нам потребуется всего лишь:

a) sql-запрос (или, в идеале, схема или миграции), который описывает поля для обоих моделей (Photo и Album), причем в `prefix_photo` у нас будет дополнительное поле `album_id`.
Примеры полей:
`prefix_album`
album_id | album_title

`prefix_photo`
photo_id | photo_title | photo_img_src | album_id


b) файлы модулей и мапперов изначально пустые

c) файлы сущностей содержат только по 1 строчке
class ModuleAlbum_EntityAlbum extends Entity {
  protected $aRelations = array(
    'has_many' => array('photo')
  );
}

class ModulePhoto_EntityPhoto extends Entity {
  protected $aRelations = array(
    'belongs_to' => array('album')
  );
}


Вот и все.

А в итоге у нас будут работать следующие методы:

$oAlbum = Engine::GetEntity('Album');
$oAlbum->setTitle('Test');
$oAlbum->Add();

$oPhoto = Engine::GetEntity('Photo');
$oPhoto->setImgUrl('my_image.png');
$oPhoto->setAlbumId($oAlbum->getId());
// или еще проще для пользователя:
$oPhoto->setAlbum($oAlbum);
$oPhoto->Add();

// и это ещё не всё:
$this->Album_GetById();
$this->Album_GetByTitle()
// чтобы получить фотки из конкретного альбома можно будет писать
$oAlbum = $this->Album_GetByTitle('Test');
$oAlbumPhotos = $oAlbum->getPhotoItems();

$oAlbumPhotos[0]->getAlbum()->getTitle(); // будет содержать title альбома - 'Test'


На мой взгляд этот подход на основе согласований имен очень удобен и совмещает в себе практичность, наглядность и удобство проектирования логики приложений.

Что думает уважаемое сообщество по поводу такого возможного развития MVC/ORM в движке LiveStreet?

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

avatar
1. Что выиграем от этого?
2. Scaffold? Обязательно нужен… просто убивает написание однотипных запросов, особенно при разработке различных каталогов.
3. Не задумывался, но было бы действительно очень полезно!
avatar
1) это просто предложение по разграничению принадлежности методов модуля и сущности, т.к. сейчас, на мой взгляд некоторая путаница присутствует.
2) я говорю не о scaffold`е, а о стандартных методах типа get*(), set*(), Add(), Upload() которые должны быть встроены в движок, а не писаться для каждого нового модуля заново. Scaffold в основном используется для viewer-ов и контроллеров, я же сейчас поднимаю вопрос модели.
3) конечно, не нужно вручную писать запросы, не нужно заботится о кэшировании и т.п.
avatar
Что должно было встроено в движок решать не Вам, а разработчику.
avatar
А вы чего такой дерзкий то?
Абсолютно разумное предложение, тем более что оно принято на обсуждение.
avatar
Я не дерзкий, просто бесят такие фразы, разработчику видней как лучше для него, ибо он разработчик, и решать ему. ТС говорит что будем всем полезно… Кому полезно? Только разработчикам модулей, а это не более 3% пользователей. Зато опять же лишний наворот уменьшит производительность, с такими темпами скоро будет второй друпал
avatar
Не волнуйтесь, это не уменьшит производительность. :)

И разработчики модулей — от них напрямую зависит будущее движка, в отличие от пассивных 97% пользователей.
avatar
Во-первых, такой подход никак не повлияет на производительность, скорее наоборот, за счет автоматизации запросов к БД, будет наилучшим образом обеспечиваться кеширование, не будут подгружаться лишние данные.

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

В-третьих, вас никто не обязывает обновлять ядро движка, когда эти изменения будут внесены, их наличие/отсутствие никак не повлияет на существующий в данный момент функционал.
avatar
А где вы увидели, что я что-то «решаю»? :) Я лишь выразил свои мысли и предложил идеи к реализации.
Кроме того, мы уже ведем обсуждение с Максимом по поводу внедрения этого функционала, я думаю, в скором времени он и сам отпишется тут об этом.
avatar
Уважаемые разработчики!

Прошу вас высказать своё мнение по этому поводу. Сомневаюсь, что здесь мало разработчиков плагинов или что их не касаются вопросы развития ядра LiveStreet.

В данный момент идет планирование реализации этих концепций, которые будут в ближайшее время включены в ядро LiveStreet, я прошу вас подключаться к обсуждению, высказывать свои идеи и предложения.

У нас все постоянно сетуют, что разработка движка проходит в узком закрытом коллективе, что предложения пользователей принимаются очень редко — сейчас как раз тот момент, когда вы можете поучаствовать в планировании ветки ORM ядра.
avatar
Вообще ORM — это очень удобно, если заранее не зацикливаться над сумасшедшей гибкостью, а иметь стандартный и небольшой набор хорошо обкатаных и документированых функций для разработчиков. Это я к тому, что целиком и полностью поддерживаю инициативу Ajaxy.

Мои пять копеек к «внутренностям»:

1. Вместо предложеного дефолтного метода Add() (и, как я полагаю ещё и Update()) иметь один центральный метод Save(). Если нет ID то будет создаваться новая запись в СУБД, если есть, то будет апдейтиться старая.

2. Очень хотелось бы видеть в какой-нибудь далёкой версии LS дельную имплементацию Unit Of Work — проще говоря, я в скрипте загружаю и меняю объекты, а все эти изменения пишутся в базу «одним махом» в ходе одной транзакции.

3. Мне кажется что так же было бы крайне круто иметь стандартный magic метод для сеттеров и геттеров во всех объектах ORM, что-то типа:


<?php
//Magic Setter
public function __set($key,$val) {
    if(method_exists($this,'set'.ucfirst($key))){
        $functionName = 'set'.ucfirst($key);
        return $this->$functionName($val);
    }
    
    if(property_exists($this,$key)) {
        $this->$key = $val;
        return true;
    }
    
    throw new Exception('Property "'.$key.'" unknown');
}

//Magic getter
public function __get($key) {
        if(method_exists($this,'get'.ucfirst($key))){
            $functionName = 'get'.ucfirst($key);
            return $this->$functionName();
        }
        
        if(property_exists($this,$key)) {
            return $this->$key;
        }
        
        throw new Exception('Property "'.$key.'" unknown');
    }
?>
avatar
1. Вместо предложеного дефолтного метода Add() (и, как я полагаю ещё и Update()) иметь один центральный метод Save(). Если нет ID то будет создаваться новая запись в СУБД, если есть, то будет апдейтиться старая.
в моей реализации именно так и есть :)
avatar
3. Мне кажется что так же было бы крайне круто иметь стандартный magic метод для сеттеров и геттеров во всех объектах ORM, что-то типа:

Он есть и сейчас в ядре. Можно использовать get*() и set*() без прописывания в Entity. В моем примере он значительно расширен для работы с отношениями и для автоматического определения соответствующего имени поля в БД,
avatar
В текущих примерах получается, что сущность и модуль выполняют одну и туже роль — роль модели (активной записи). Лучше предусмотреть возможность работы модуля с разными сущностями/моделями.
Например, если сущность Photo относится к модулю Album, то возможен вызов
$this->Album_GetPhotoById(11);

В сущности можно оставить только алиасы на реальные методы работы с БД, а все действия производить в модуле. Например, вызов
$oAlbum->Save();
является алиасом для
$this->Album_SaveAlbum();

Вообще реализацию всего этого можно начать с работы только одной таблицы, т.е. реализация get* set* save* delete* find(или byFilter)*, а потом уже накручивать реляционные связи
  • ort
  • 0
avatar
хорошая идея, поддерживаю.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.