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

5
Хочу предложить свои идеи для развития 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?
  • 0
  • 04 августа 2010, 15:56
  • Ajaxy

Комментарии (14)

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

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

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

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

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

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

У нас все постоянно сетуют, что разработка движка проходит в узком закрытом коллективе, что предложения пользователей принимаются очень редко — сейчас как раз тот момент, когда вы можете поучаствовать в планировании ветки ORM ядра.
0
  • avatar
  • Ajaxy
  • 07 августа 2010, 20:53
Вообще 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');
    }
?>
0
  • avatar
  • clops
  • 09 августа 2010, 14:03
1. Вместо предложеного дефолтного метода Add() (и, как я полагаю ещё и Update()) иметь один центральный метод Save(). Если нет ID то будет создаваться новая запись в СУБД, если есть, то будет апдейтиться старая.
в моей реализации именно так и есть :)
0
3. Мне кажется что так же было бы крайне круто иметь стандартный magic метод для сеттеров и геттеров во всех объектах ORM, что-то типа:

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

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

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